diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..d39b7ca
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,17 @@
+# Set the default behavior, in case people don't have core.autocrlf set.
+* text=auto
+
+# Declare OSX files that will always have LF line endings on checkout.
+Info.plist text eol=lf
+
+# Declare script files that will always have LF line endings on checkout.
+*.sh text eol=lf
+
+# Declare script files that will always have CR/LF line endings on checkout.
+*.bat text eol=crlf
+
+# Denote all files that are truly binary and should not be modified.
+*.png binary
+*.jpg binary
+*.gif binary
+*.icns binary
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f2fa73d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,40 @@
+# Java
+*.class
+
+# JD
+debug*
+
+# JD-GUI
+src-generated/
+jd-gui.cfg
+
+# Idea
+.idea/
+out/
+*.ipr
+*.iml
+*.iws
+
+# Eclipse
+.settings/
+classes/
+.classpath
+.project
+
+# Mac
+.DS_Store
+
+#Windows
+Thumbs.db
+
+# Maven
+log/
+target/
+
+# Gradle
+.gradle/
+build/
+!gradle/wrapper/*
+
+# WinMerge
+*.bak
diff --git a/LICENSE b/LICENSE
index 12f5d13..2902717 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,676 +1,676 @@
-                     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.
-
-    JD-GUI, a standalone graphical utility that displays Java sources from
-	CLASS files
-    Copyright (C) 2008-2019  Emmanuel Dupuy
-
-    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:
-
-    JD-GUI  Copyright (C) 2008-2019  Emmanuel Dupuy
-    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>.
+                     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.
+
+    JD-GUI, a standalone graphical utility that displays Java sources from
+	CLASS files
+    Copyright (C) 2008-2019  Emmanuel Dupuy
+
+    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:
+
+    JD-GUI  Copyright (C) 2008-2019  Emmanuel Dupuy
+    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/NOTICE b/NOTICE
index 839e954..6c3ce53 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,16 +1,16 @@
-JD-GUI license - GPLv3
-
-Libraries used:
-
-Groovy - Apache License 2.0
-Gradle - Apache License 2.0
-JD-Core Java Release - GPLv3
-RSyntaxTextArea - Modified BSD license
-
-JD-GUI Mac OSX distribution:
-
-universalJavaApplicationStub - MIT License
-
-JD-GUI Windows distribution:
-
-Launch4j - MIT License
+JD-GUI license - GPLv3
+
+Libraries used:
+
+Groovy - Apache License 2.0
+Gradle - Apache License 2.0
+JD-Core Java Release - GPLv3
+RSyntaxTextArea - Modified BSD license
+
+JD-GUI Mac OSX distribution:
+
+universalJavaApplicationStub - MIT License
+
+JD-GUI Windows distribution:
+
+Launch4j - MIT License
diff --git a/README.md b/README.md
index c2dfbc5..0f04fa8 100644
--- a/README.md
+++ b/README.md
@@ -1,65 +1,65 @@
-# JD-GUI
-
-JD-GUI, a standalone graphical utility that displays Java sources from CLASS files.
-
-![](https://raw.githubusercontent.com/java-decompiler/jd-gui/master/src/website/img/jd-gui.png)
-
-- Java Decompiler projects home page: [http://java-decompiler.github.io](http://java-decompiler.github.io)
-- JD-GUI source code: [https://github.com/java-decompiler/jd-gui](https://github.com/java-decompiler/jd-gui)
-
-## Description
-JD-GUI is a standalone graphical utility that displays Java source codes of 
-".class" files. You can browse the reconstructed source code with the JD-GUI
-for instant access to methods and fields.
-
-## How to build JD-GUI ?
-```
-> git clone https://github.com/java-decompiler/jd-gui.git
-> cd jd-gui
-> ./gradlew build 
-```
-generate :
-- _"build/libs/jd-gui-x.y.z.jar"_
-- _"build/libs/jd-gui-x.y.z-min.jar"_
-- _"build/distributions/jd-gui-windows-x.y.z.zip"_
-- _"build/distributions/jd-gui-osx-x.y.z.tar"_
-- _"build/distributions/jd-gui-x.y.z.deb"_
-- _"build/distributions/jd-gui-x.y.z.rpm"_
-
-## How to launch JD-GUI ?
-- Double-click on _"jd-gui-x.y.z.jar"_
-- Double-click on _"jd-gui.exe"_ application from Windows
-- Double-click on _"JD-GUI"_ application from Mac OSX
-- Execute _"java -jar jd-gui-x.y.z.jar"_ or _"java -classpath jd-gui-x.y.z.jar org.jd.gui.App"_
-
-## How to use JD-GUI ?
-- Open a file with menu "File > Open File..."
-- Open recent files with menu "File > Recent Files"
-- Drag and drop files from your file explorer
-
-## How to extend JD-GUI ?
-```
-> ./gradlew idea 
-```
-generate Idea Intellij project
-```
-> ./gradlew eclipse
-```
-generate Eclipse project
-```
-> java -classpath jd-gui-x.y.z.jar;myextension1.jar;myextension2.jar org.jd.gui.App
-```
-launch JD-GUI with your extensions
-
-## How to uninstall JD-GUI ?
-- Java: Delete "jd-gui-x.y.z.jar" and "jd-gui.cfg".
-- Mac OSX: Drag and drop "JD-GUI" application into the trash.
-- Windows: Delete "jd-gui.exe" and "jd-gui.cfg".
-
-## License
-Released under the [GNU GPL v3](LICENSE).
-
-## Donations
-Did JD-GUI help you to solve a critical situation? Do you use JD-Eclipse daily? What about making a donation?
-
-[![paypal](https://raw.githubusercontent.com/java-decompiler/jd-gui/master/src/website/img/btn_donate_euro.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=C88ZMVZ78RF22) [![paypal](https://raw.githubusercontent.com/java-decompiler/jd-gui/master/src/website/img/btn_donate_usd.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CRMXT4Y4QLQGU)
+# JD-GUI
+
+JD-GUI, a standalone graphical utility that displays Java sources from CLASS files.
+
+![](https://raw.githubusercontent.com/java-decompiler/jd-gui/master/src/website/img/jd-gui.png)
+
+- Java Decompiler projects home page: [http://java-decompiler.github.io](http://java-decompiler.github.io)
+- JD-GUI source code: [https://github.com/java-decompiler/jd-gui](https://github.com/java-decompiler/jd-gui)
+
+## Description
+JD-GUI is a standalone graphical utility that displays Java source codes of 
+".class" files. You can browse the reconstructed source code with the JD-GUI
+for instant access to methods and fields.
+
+## How to build JD-GUI ?
+```
+> git clone https://github.com/java-decompiler/jd-gui.git
+> cd jd-gui
+> ./gradlew build 
+```
+generate :
+- _"build/libs/jd-gui-x.y.z.jar"_
+- _"build/libs/jd-gui-x.y.z-min.jar"_
+- _"build/distributions/jd-gui-windows-x.y.z.zip"_
+- _"build/distributions/jd-gui-osx-x.y.z.tar"_
+- _"build/distributions/jd-gui-x.y.z.deb"_
+- _"build/distributions/jd-gui-x.y.z.rpm"_
+
+## How to launch JD-GUI ?
+- Double-click on _"jd-gui-x.y.z.jar"_
+- Double-click on _"jd-gui.exe"_ application from Windows
+- Double-click on _"JD-GUI"_ application from Mac OSX
+- Execute _"java -jar jd-gui-x.y.z.jar"_ or _"java -classpath jd-gui-x.y.z.jar org.jd.gui.App"_
+
+## How to use JD-GUI ?
+- Open a file with menu "File > Open File..."
+- Open recent files with menu "File > Recent Files"
+- Drag and drop files from your file explorer
+
+## How to extend JD-GUI ?
+```
+> ./gradlew idea 
+```
+generate Idea Intellij project
+```
+> ./gradlew eclipse
+```
+generate Eclipse project
+```
+> java -classpath jd-gui-x.y.z.jar;myextension1.jar;myextension2.jar org.jd.gui.App
+```
+launch JD-GUI with your extensions
+
+## How to uninstall JD-GUI ?
+- Java: Delete "jd-gui-x.y.z.jar" and "jd-gui.cfg".
+- Mac OSX: Drag and drop "JD-GUI" application into the trash.
+- Windows: Delete "jd-gui.exe" and "jd-gui.cfg".
+
+## License
+Released under the [GNU GPL v3](LICENSE).
+
+## Donations
+Did JD-GUI help you to solve a critical situation? Do you use JD-Eclipse daily? What about making a donation?
+
+[![paypal](https://raw.githubusercontent.com/java-decompiler/jd-gui/master/src/website/img/btn_donate_euro.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=C88ZMVZ78RF22) [![paypal](https://raw.githubusercontent.com/java-decompiler/jd-gui/master/src/website/img/btn_donate_usd.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CRMXT4Y4QLQGU)
diff --git a/api/build.gradle b/api/build.gradle
new file mode 100644
index 0000000..8cd11af
--- /dev/null
+++ b/api/build.gradle
@@ -0,0 +1,3 @@
+apply plugin: 'java'
+
+version = '1.0.0'
diff --git a/api/src/main/java/org/jd/gui/api/API.java b/api/src/main/java/org/jd/gui/api/API.java
new file mode 100644
index 0000000..1ac3c76
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/API.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api;
+
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.spi.*;
+
+import javax.swing.*;
+import java.io.File;
+import java.net.URI;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.Future;
+
+public interface API {
+    boolean openURI(URI uri);
+
+    boolean openURI(int x, int y, Collection<Container.Entry> entries, String query, String fragment);
+
+    void addURI(URI uri);
+
+    <T extends JComponent & UriGettable> void addPanel(String title, Icon icon, String tip, T component);
+
+    Collection<Action> getContextualActions(Container.Entry entry, String fragment);
+
+    UriLoader getUriLoader(URI uri);
+
+    FileLoader getFileLoader(File file);
+
+    ContainerFactory getContainerFactory(Path rootPath);
+
+    PanelFactory getMainPanelFactory(Container container);
+
+    TreeNodeFactory getTreeNodeFactory(Container.Entry entry);
+
+    TypeFactory getTypeFactory(Container.Entry entry);
+
+    Indexer getIndexer(Container.Entry entry);
+
+    SourceSaver getSourceSaver(Container.Entry entry);
+
+    Map<String, String> getPreferences();
+
+    Collection<Future<Indexes>> getCollectionOfFutureIndexes();
+
+    interface LoadSourceListener {
+        void sourceLoaded(String source);
+    }
+
+    String getSource(Container.Entry entry);
+
+    void loadSource(Container.Entry entry, LoadSourceListener listener);
+
+    File loadSourceFile(Container.Entry entry);
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/ContainerEntryGettable.java b/api/src/main/java/org/jd/gui/api/feature/ContainerEntryGettable.java
new file mode 100644
index 0000000..0413e9a
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/ContainerEntryGettable.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+import org.jd.gui.api.model.Container;
+
+public interface ContainerEntryGettable {
+    Container.Entry getEntry();
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/ContentCopyable.java b/api/src/main/java/org/jd/gui/api/feature/ContentCopyable.java
new file mode 100644
index 0000000..9522cc2
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/ContentCopyable.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+public interface ContentCopyable {
+    void copy();
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/ContentIndexable.java b/api/src/main/java/org/jd/gui/api/feature/ContentIndexable.java
new file mode 100644
index 0000000..39cd7c2
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/ContentIndexable.java
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Indexes;
+
+public interface ContentIndexable {
+    Indexes index(API api);
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/ContentSavable.java b/api/src/main/java/org/jd/gui/api/feature/ContentSavable.java
new file mode 100644
index 0000000..6b646ac
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/ContentSavable.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+import org.jd.gui.api.API;
+
+import java.io.OutputStream;
+
+public interface ContentSavable {
+    String getFileName();
+
+    void save(API api, OutputStream os);
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/ContentSearchable.java b/api/src/main/java/org/jd/gui/api/feature/ContentSearchable.java
new file mode 100644
index 0000000..51d44ba
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/ContentSearchable.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+public interface ContentSearchable {
+    boolean highlightText(String text, boolean caseSensitive);
+
+    void findNext(String text, boolean caseSensitive);
+
+    void findPrevious(String text, boolean caseSensitive);
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/ContentSelectable.java b/api/src/main/java/org/jd/gui/api/feature/ContentSelectable.java
new file mode 100644
index 0000000..0b6d87d
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/ContentSelectable.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+public interface ContentSelectable {
+    void selectAll();
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/FocusedTypeGettable.java b/api/src/main/java/org/jd/gui/api/feature/FocusedTypeGettable.java
new file mode 100644
index 0000000..1ef2205
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/FocusedTypeGettable.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+public interface FocusedTypeGettable extends ContainerEntryGettable {
+    String getFocusedTypeName();
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/IndexesChangeListener.java b/api/src/main/java/org/jd/gui/api/feature/IndexesChangeListener.java
new file mode 100644
index 0000000..73a06a9
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/IndexesChangeListener.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+import org.jd.gui.api.model.Indexes;
+
+import java.util.Collection;
+import java.util.concurrent.Future;
+
+public interface IndexesChangeListener {
+    void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes);
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/LineNumberNavigable.java b/api/src/main/java/org/jd/gui/api/feature/LineNumberNavigable.java
new file mode 100644
index 0000000..bb4fe0b
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/LineNumberNavigable.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+public interface LineNumberNavigable {
+    int getMaximumLineNumber();
+
+    void goToLineNumber(int lineNumber);
+
+    boolean checkLineNumber(int lineNumber);
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/PageChangeListener.java b/api/src/main/java/org/jd/gui/api/feature/PageChangeListener.java
new file mode 100644
index 0000000..35cfa1e
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/PageChangeListener.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+import javax.swing.*;
+
+public interface PageChangeListener {
+    <T extends JComponent & UriGettable> void pageChanged(T page);
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/PageChangeable.java b/api/src/main/java/org/jd/gui/api/feature/PageChangeable.java
new file mode 100644
index 0000000..7beed86
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/PageChangeable.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+public interface PageChangeable {
+    void addPageChangeListener(PageChangeListener listener);
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/PageClosable.java b/api/src/main/java/org/jd/gui/api/feature/PageClosable.java
new file mode 100644
index 0000000..fec1bc6
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/PageClosable.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+public interface PageClosable {
+    boolean closePage();
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/PageCreator.java b/api/src/main/java/org/jd/gui/api/feature/PageCreator.java
new file mode 100644
index 0000000..34373d2
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/PageCreator.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+import org.jd.gui.api.API;
+
+import javax.swing.*;
+
+public interface PageCreator {
+    <T extends JComponent & UriGettable> T createPage(API api);
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/PreferencesChangeListener.java b/api/src/main/java/org/jd/gui/api/feature/PreferencesChangeListener.java
new file mode 100644
index 0000000..7999d46
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/PreferencesChangeListener.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+import java.util.Map;
+
+public interface PreferencesChangeListener {
+    void preferencesChanged(Map<String, String> preferences);
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/SourcesSavable.java b/api/src/main/java/org/jd/gui/api/feature/SourcesSavable.java
new file mode 100644
index 0000000..e47a869
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/SourcesSavable.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+import org.jd.gui.api.API;
+
+import java.nio.file.Path;
+
+public interface SourcesSavable {
+    String getSourceFileName();
+
+    int getFileCount();
+
+    void save(API api, Controller controller, Listener listener, Path path);
+
+    interface Controller {
+        boolean isCancelled();
+    }
+
+    interface Listener {
+        void pathSaved(Path path);
+    }
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/TreeNodeExpandable.java b/api/src/main/java/org/jd/gui/api/feature/TreeNodeExpandable.java
new file mode 100644
index 0000000..948bca1
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/TreeNodeExpandable.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+import org.jd.gui.api.API;
+
+public interface TreeNodeExpandable {
+    void populateTreeNode(API api);
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/UriGettable.java b/api/src/main/java/org/jd/gui/api/feature/UriGettable.java
new file mode 100644
index 0000000..93fa033
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/UriGettable.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+import java.net.URI;
+
+public interface UriGettable {
+    URI getUri();
+}
diff --git a/api/src/main/java/org/jd/gui/api/feature/UriOpenable.java b/api/src/main/java/org/jd/gui/api/feature/UriOpenable.java
new file mode 100644
index 0000000..c5d8c74
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/feature/UriOpenable.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.feature;
+
+import java.net.URI;
+
+/**
+ * uri                : scheme '://' path ('?' query)? ('#' fragment)?<br>
+ * scheme             : 'generic' | 'jar' | 'war' | 'ear' | 'dex' | ...<br>
+ * path               : singlePath('!' singlePath)*<br>
+ * singlePath         : [path/to/dir/] | [path/to/file]<br>
+ * query              : queryLineNumber | queryPosition | querySearch<br>
+ * queryLineNumber    : 'lineNumber=' [numeric]<br>
+ * queryPosition      : 'position=' [numeric]<br>
+ * querySearch        : 'highlightPattern=' queryPattern '&highlightFlags=' queryFlags ('&highlightScope=' typeName)?<br>
+ * queryPattern       : [start of string] | [start of type name] | [start of field name] | [start of method name]<br>
+ * queryFlags         : 'd'? // Match declarations<br>
+ *                      'r'? // Match references<br>
+ *                      't'? // Match types<br>
+ *                      'c'? // Match constructors<br>
+ *                      'm'? // Match methods<br>
+ *                      'f'? // Match fields<br>
+ *                      's'? // Match strings<br>
+ * fragment            : fragmentType | fragmentField | fragmentMethod<br>
+ * fragmentType        : typeName<br>
+ * fragmentField       : typeName '-' [field name] '-' descriptor<br>
+ * fragmentMethod      : typeName '-' [method name] '-' methodDescriptor<br>
+ * methodDescriptor    : '(*)?' | // Match all method descriptors<br>
+ *                       '(' descriptor* ')' descriptor<br>
+ * descriptor          : '?' | // Match a primitive or a type name<br>
+ *                       '['* primitiveOrTypeName<br>
+ * primitiveOrTypeName : 'B' | 'C' | 'D' | 'F' | 'I' | 'J' | 'L' typeName ';' | 'S' | 'Z'<br>
+ * typeName            : [internal qualified name] | '*\/' [name]<br>
+ * <br>
+ * Examples:<br>
+ * <ul>
+ *  <li>file://dir1/dir2/</li>
+ *  <li>file://dir1/dir2/file</li>
+ *  <li>jar://dir1/dir2/</li>
+ *  <li>jar://dir1/dir2/file</li>
+ *
+ *  <li>jar://dir1/dir2/javafile</li>
+ *  <li>jar://dir1/dir2/javafile#type</li>
+ *  <li>jar://dir1/dir2/javafile#type-fieldName-descriptor</li>
+ *  <li>jar://dir1/dir2/javafile#type-methodName-descriptor</li>
+ *  <li>jar://dir1/dir2/javafile#innertype</li>
+ *  <li>jar://dir1/dir2/javafile#innertype-fieldName-?</li>
+ *  <li>jar://dir1/dir2/javafile#innertype-methodName-(*)?</li>
+ *  <li>jar://dir1/dir2/javafile#innertype-methodName-(?JZLjava/lang/Sting;C)I</li>
+ *  <li>jar://dir1/dir2/javafile#innertype-fieldName-descriptor</li>
+ *  <li>jar://dir1/dir2/javafile#innertype-methodName-descriptor</li>
+ *
+ *  <li>file://dir1/dir2/file?lineNumber=numeric</li>
+ *  <li>file://dir1/dir2/file?position=numeric</li>
+ *  <li>file://dir1/dir2/file?highlightPattern=hello&highlightFlags=drtcmfs&highlightScope=java/lang/String</li>
+ *  <li>file://dir1/dir2/file?highlightPattern=hello&highlightFlags=drtcmfs&highlightScope=*\/String</li>
+ * </ul>
+ */
+public interface UriOpenable {
+    boolean openUri(URI uri);
+}
diff --git a/api/src/main/java/org/jd/gui/api/model/Container.java b/api/src/main/java/org/jd/gui/api/model/Container.java
new file mode 100644
index 0000000..c898e59
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/model/Container.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.model;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.util.Collection;
+
+public interface Container {
+    String getType();
+
+    Entry getRoot();
+
+    /**
+     * File or directory
+     */
+    interface Entry {
+        Container getContainer();
+
+        Entry getParent();
+
+        URI getUri();
+
+        String getPath();
+
+        boolean isDirectory();
+
+        long length();
+
+        InputStream getInputStream();
+
+        Collection<Entry> getChildren();
+    }
+}
diff --git a/api/src/main/java/org/jd/gui/api/model/Indexes.java b/api/src/main/java/org/jd/gui/api/model/Indexes.java
new file mode 100644
index 0000000..df8c729
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/model/Indexes.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.model;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Whatever the language/file format (Java|Groovy|Scala/Class|DEX, Java|Javascript/Source, C#/CIL, ...), type names,
+ * stored in the indexes, use the JVM internal format (package separator = '/', inner class separator = '$').<br>
+ * <br>
+ * List of default indexes:
+ * <ul>
+ *     <li>
+ *         Map "strings"<br>
+ *         key: a string<br>
+ *         value: a list of entries containing the string
+ *     </li>
+ *     <li>
+ *         Map "typeDeclarations"<br>
+ *         key: a type name using internal JVM internal format<br>
+ *         value: a list of entries containing the type declaration
+ *     </li>
+ *     <li>
+ *         Map "constructorDeclarations"<br>
+ *         key: a type name using internal JVM internal format<br>
+ *         value: a list of entries containing the constructor declaration
+ *     </li>
+ *     <li>
+ *         Map "constructorReferences"<br>
+ *         key: a type name using internal JVM internal format<br>
+ *         value: a list of entries containing the constructor reference
+ *     </li>
+ *     <li>
+ *         Map "methodDeclarations"<br>
+ *         key: a method name<br>
+ *         value: a list of entries containing the method declaration
+ *     </li>
+ *     <li>
+ *         Map "methodReferences"<br>
+ *         key: a method name<br>
+ *         value: a list of entries containing the method reference
+ *     </li>
+ *     <li>
+ *         Map "fieldDeclarations"<br>
+ *         key: a field name<br>
+ *         value: a list of entries containing the field declaration
+ *     </li>
+ *     <li>
+ *         Map "fieldReferences"<br>
+ *         key: a field name<br>
+ *         value: a list of entries containing the field reference
+ *     </li>
+ *     <li>
+ *         Map "subTypeNames"<br>
+ *         key: a super type name using internal JVM internal format<br>
+ *         value: a list of sub type names using internal JVM internal format
+ *     </li>
+ * </ul>
+ */
+public interface Indexes {
+    Map<String, Collection> getIndex(String name);
+}
diff --git a/api/src/main/java/org/jd/gui/api/model/TreeNodeData.java b/api/src/main/java/org/jd/gui/api/model/TreeNodeData.java
new file mode 100644
index 0000000..6020b7d
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/model/TreeNodeData.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.model;
+
+import javax.swing.*;
+
+public interface TreeNodeData {
+    String getLabel();
+
+    String getTip();
+
+    Icon getIcon();
+
+    Icon getOpenIcon();
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/jd/gui/api/model/Type.java b/api/src/main/java/org/jd/gui/api/model/Type.java
new file mode 100644
index 0000000..9b3d252
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/api/model/Type.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.api.model;
+
+import javax.swing.*;
+import java.util.Collection;
+
+public interface Type {
+    int FLAG_PUBLIC = 1;
+    int FLAG_PRIVATE = 2;
+    int FLAG_PROTECTED = 4;
+    int FLAG_STATIC = 8;
+    int FLAG_FINAL = 16;
+    int FLAG_VARARGS = 128;
+    int FLAG_INTERFACE = 512;
+    int FLAG_ABSTRACT = 1024;
+    int FLAG_ANNOTATION = 8192;
+    int FLAG_ENUM = 16384;
+
+    int getFlags();
+
+    String getName();
+
+    String getSuperName();
+
+    String getOuterName();
+
+    String getDisplayTypeName();
+
+    String getDisplayInnerTypeName();
+
+    String getDisplayPackageName();
+
+    Icon getIcon();
+
+    Collection<Type> getInnerTypes();
+
+    Collection<Field> getFields();
+
+    Collection<Method> getMethods();
+
+    interface Field {
+        int getFlags();
+
+        String getName();
+
+        String getDescriptor();
+
+        String getDisplayName();
+
+        Icon getIcon();
+    }
+
+    interface Method {
+        int getFlags();
+
+        String getName();
+
+        String getDescriptor();
+
+        String getDisplayName();
+
+        Icon getIcon();
+    }
+}
diff --git a/api/src/main/java/org/jd/gui/spi/ContainerFactory.java b/api/src/main/java/org/jd/gui/spi/ContainerFactory.java
new file mode 100644
index 0000000..bb5f227
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/ContainerFactory.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+
+import java.nio.file.Path;
+
+public interface ContainerFactory {
+    String getType();
+
+    boolean accept(API api, Path rootPath);
+
+    Container make(API api, Container.Entry parentEntry, Path rootPath);
+}
diff --git a/api/src/main/java/org/jd/gui/spi/ContextualActionsFactory.java b/api/src/main/java/org/jd/gui/spi/ContextualActionsFactory.java
new file mode 100644
index 0000000..964513d
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/ContextualActionsFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+
+import javax.swing.*;
+import java.util.Collection;
+
+public interface ContextualActionsFactory {
+    String GROUP_NAME = "GroupNameKey";
+
+    /**
+     * Build a collection of actions for 'entry' and 'fragment', grouped by GROUP_NAME and sorted by NAME. Null values
+     * are added for separators.
+     *
+     * @param fragment @see jd.gui.api.feature.UriOpenable
+     * @return a collection of actions
+     */
+    Collection<Action> make(API api, Container.Entry entry, String fragment);
+}
diff --git a/api/src/main/java/org/jd/gui/spi/FileLoader.java b/api/src/main/java/org/jd/gui/spi/FileLoader.java
new file mode 100644
index 0000000..464ae1c
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/FileLoader.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import org.jd.gui.api.API;
+
+import java.io.File;
+
+public interface FileLoader {
+	String[] getExtensions();
+	
+	String getDescription();
+	
+	boolean accept(API api, File file);
+	
+	boolean load(API api, File file);
+}
diff --git a/api/src/main/java/org/jd/gui/spi/Indexer.java b/api/src/main/java/org/jd/gui/spi/Indexer.java
new file mode 100644
index 0000000..db097fc
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/Indexer.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+
+import java.util.regex.Pattern;
+
+public interface Indexer {
+    String[] getSelectors();
+
+    Pattern getPathPattern();
+
+    void index(API api, Container.Entry entry, Indexes indexes);
+}
diff --git a/api/src/main/java/org/jd/gui/spi/PanelFactory.java b/api/src/main/java/org/jd/gui/spi/PanelFactory.java
new file mode 100644
index 0000000..28b3daf
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/PanelFactory.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+
+import javax.swing.*;
+
+public interface PanelFactory {
+	String[] getTypes();
+	
+	<T extends JComponent & UriGettable> T make(API api, Container container);
+}
diff --git a/api/src/main/java/org/jd/gui/spi/PasteHandler.java b/api/src/main/java/org/jd/gui/spi/PasteHandler.java
new file mode 100644
index 0000000..c4b8834
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/PasteHandler.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import org.jd.gui.api.API;
+
+public interface PasteHandler {
+    boolean accept(Object obj);
+
+    void paste(API api, Object obj);
+}
diff --git a/api/src/main/java/org/jd/gui/spi/PreferencesPanel.java b/api/src/main/java/org/jd/gui/spi/PreferencesPanel.java
new file mode 100644
index 0000000..33e5bd6
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/PreferencesPanel.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Map;
+
+public interface PreferencesPanel {
+    String getPreferencesGroupTitle();
+
+    String getPreferencesPanelTitle();
+
+    JComponent getPanel();
+
+    void init(Color errorBackgroundColor);
+
+    boolean isActivated();
+
+    void loadPreferences(Map<String, String> preferences);
+
+    void savePreferences(Map<String, String> preferences);
+
+    boolean arePreferencesValid();
+
+    void addPreferencesChangeListener(PreferencesPanelChangeListener listener);
+
+    interface PreferencesPanelChangeListener {
+        void preferencesPanelChanged(PreferencesPanel source);
+    }
+}
diff --git a/api/src/main/java/org/jd/gui/spi/SourceLoader.java b/api/src/main/java/org/jd/gui/spi/SourceLoader.java
new file mode 100644
index 0000000..f34dd61
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/SourceLoader.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+
+import java.io.File;
+
+public interface SourceLoader {
+    String getSource(API api, Container.Entry entry);
+
+    String loadSource(API api, Container.Entry entry);
+
+    File loadSourceFile(API api, Container.Entry entry);
+}
diff --git a/api/src/main/java/org/jd/gui/spi/SourceSaver.java b/api/src/main/java/org/jd/gui/spi/SourceSaver.java
new file mode 100644
index 0000000..e1a414a
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/SourceSaver.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+
+import java.nio.file.Path;
+import java.util.regex.Pattern;
+
+public interface SourceSaver {
+    String[] getSelectors();
+
+    Pattern getPathPattern();
+
+    String getSourcePath(Container.Entry entry);
+
+    int getFileCount(API api, Container.Entry entry);
+
+    /**
+     * Check parent path, build source file name, create NIO path and save the content.
+     */
+    void save(API api, Controller controller, Listener listener, Path rootPath, Container.Entry entry);
+
+    /**
+     * Save content:
+     * <ul>
+     * <li>For file, save the source content.</li>
+     * <li>For directory, call 'save' for each children.</li>
+     * </ul>
+     */
+    void saveContent(API api, Controller controller, Listener listener, Path rootPath, Path path, Container.Entry entry);
+
+    interface Controller {
+        boolean isCancelled();
+    }
+
+    interface Listener {
+        void pathSaved(Path path);
+    }
+}
diff --git a/api/src/main/java/org/jd/gui/spi/TreeNodeFactory.java b/api/src/main/java/org/jd/gui/spi/TreeNodeFactory.java
new file mode 100644
index 0000000..ad8fa6a
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/TreeNodeFactory.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.util.regex.Pattern;
+
+public interface TreeNodeFactory {
+    String[] getSelectors();
+
+    Pattern getPathPattern();
+
+    <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry);
+}
diff --git a/api/src/main/java/org/jd/gui/spi/TypeFactory.java b/api/src/main/java/org/jd/gui/spi/TypeFactory.java
new file mode 100644
index 0000000..b71bfbd
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/TypeFactory.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Type;
+
+import java.util.Collection;
+import java.util.regex.Pattern;
+
+public interface TypeFactory {
+	String[] getSelectors();
+
+    Pattern getPathPattern();
+
+	/**
+	 * @return all root types contains in 'entry'
+	 */
+	Collection<Type> make(API api, Container.Entry entry);
+
+	/**
+     * @param fragment @see jd.gui.api.feature.UriOpenable
+	 * @return if 'fragment' is null, return the main type in 'entry',
+	 *         otherwise, return the type or sub-type matching with 'fragment'
+	 */
+	Type make(API api, Container.Entry entry, String fragment);
+}
diff --git a/api/src/main/java/org/jd/gui/spi/UriLoader.java b/api/src/main/java/org/jd/gui/spi/UriLoader.java
new file mode 100644
index 0000000..ba55002
--- /dev/null
+++ b/api/src/main/java/org/jd/gui/spi/UriLoader.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.spi;
+
+import org.jd.gui.api.API;
+
+import java.net.URI;
+
+public interface UriLoader {
+	String[] getSchemes();
+	
+	boolean accept(API api, URI uri);
+	
+	boolean load(API api, URI uri);
+}
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..6366e3b
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,9 @@
+apply plugin: 'java'
+
+dependencies {
+    provided 'com.yuvimasory:orange-extensions:1.3.0'   // OSX support
+    compile project(':api')
+    runtime project(':services')
+}
+
+version = parent.version
diff --git a/app/src/main/java/org/jd/gui/App.java b/app/src/main/java/org/jd/gui/App.java
new file mode 100644
index 0000000..3327d86
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/App.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui;
+
+import org.jd.gui.controller.MainController;
+import org.jd.gui.model.configuration.Configuration;
+import org.jd.gui.service.configuration.ConfigurationPersister;
+import org.jd.gui.service.configuration.ConfigurationPersisterService;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.net.InterProcessCommunicationUtil;
+
+import javax.swing.*;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class App {
+    protected static final String SINGLE_INSTANCE = "UIMainWindowPreferencesProvider.singleInstance";
+
+    protected static MainController controller;
+
+    public static void main(String[] args) {
+		if (checkHelpFlag(args)) {
+			JOptionPane.showMessageDialog(null, "Usage: jd-gui [option] [input-file] ...\n\nOption:\n -h Show this help message and exit", Constants.APP_NAME, JOptionPane.INFORMATION_MESSAGE);
+		} else {
+            // Load preferences
+            ConfigurationPersister persister = ConfigurationPersisterService.getInstance().get();
+            Configuration configuration = persister.load();
+            Runtime.getRuntime().addShutdownHook(new Thread(() -> persister.save(configuration)));
+
+            if ("true".equals(configuration.getPreferences().get(SINGLE_INSTANCE))) {
+                InterProcessCommunicationUtil ipc = new InterProcessCommunicationUtil();
+                try {
+                    ipc.listen(receivedArgs -> controller.openFiles(newList(receivedArgs)));
+                } catch (Exception notTheFirstInstanceException) {
+                    // Send args to main windows and exit
+                    ipc.send(args);
+                    System.exit(0);
+                }
+            }
+
+            // Create SwingBuilder, set look and feel
+            try {
+                UIManager.setLookAndFeel(configuration.getLookAndFeel());
+            } catch (Exception e) {
+                configuration.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+                try {
+                    UIManager.setLookAndFeel(configuration.getLookAndFeel());
+                } catch (Exception ee) {
+                    assert ExceptionUtil.printStackTrace(ee);
+                }
+           }
+
+            // Create main controller and show main frame
+            controller = new MainController(configuration);
+            controller.show(newList(args));
+		}
+	}
+
+    protected static boolean checkHelpFlag(String[] args) {
+        if (args != null) {
+            for (String arg : args) {
+                if ("-h".equals(arg)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    protected static List<File> newList(String[] paths) {
+        if (paths == null) {
+            return Collections.emptyList();
+        } else {
+            ArrayList<File> files = new ArrayList<>(paths.length);
+            for (String path : paths) {
+                files.add(new File(path));
+            }
+            return files;
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/Constants.java b/app/src/main/java/org/jd/gui/Constants.java
new file mode 100644
index 0000000..75d5cdd
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/Constants.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui;
+
+public class Constants {
+	public static final String APP_NAME = "JD-GUI";
+
+	public static final int DEFAULT_WIDTH = 600;
+	public static final int DEFAULT_HEIGHT = 400;
+
+	public static final int MINIMAL_WIDTH = 500;
+	public static final int MINIMAL_HEIGHT = 160;
+
+	public static final String CONFIG_FILENAME = "jd-gui.cfg";
+
+	public static final int MAX_RECENT_FILES = 10;
+	public static final int RECENT_FILE_MAX_LENGTH = 200;
+}
diff --git a/app/src/main/java/org/jd/gui/OsxApp.java b/app/src/main/java/org/jd/gui/OsxApp.java
new file mode 100644
index 0000000..b8ac598
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/OsxApp.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui;
+
+import com.apple.eawt.Application;
+
+public class OsxApp extends App {
+
+    @SuppressWarnings("unchecked")
+    public static void main(String[] args) {
+        // Create an instance of the mac OSX Application class
+        Application application = Application.getApplication();
+
+        App.main(args);
+
+        // Add an handle invoked when the application is asked to open a list of files
+        application.setOpenFileHandler(e -> controller.openFiles(e.getFiles()));
+
+        // Add an handle invoked when the application is asked to quit
+        application.setQuitHandler((e, r) -> System.exit(0));
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/controller/AboutController.java b/app/src/main/java/org/jd/gui/controller/AboutController.java
new file mode 100644
index 0000000..64952e8
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/controller/AboutController.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.controller;
+
+import org.jd.gui.view.AboutView;
+
+import javax.swing.*;
+
+public class AboutController {
+    protected AboutView aboutView;
+
+    public AboutController(JFrame mainFrame) {
+        // Create UI
+        aboutView = new AboutView(mainFrame);
+    }
+
+    public void show() {
+        // Show
+        aboutView.show();
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/controller/GoToController.java b/app/src/main/java/org/jd/gui/controller/GoToController.java
new file mode 100644
index 0000000..04c92b2
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/controller/GoToController.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.controller;
+
+import org.jd.gui.api.feature.LineNumberNavigable;
+import org.jd.gui.model.configuration.Configuration;
+import org.jd.gui.view.GoToView;
+
+import javax.swing.*;
+import java.util.function.IntConsumer;
+
+public class GoToController {
+    protected GoToView goToView;
+
+    public GoToController(Configuration configuration, JFrame mainFrame) {
+        // Create UI
+        goToView = new GoToView(configuration, mainFrame);
+    }
+
+    public void show(LineNumberNavigable navigator, IntConsumer okCallback) {
+        // Show
+        goToView.show(navigator, okCallback);
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/controller/MainController.java b/app/src/main/java/org/jd/gui/controller/MainController.java
new file mode 100644
index 0000000..94b0e88
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/controller/MainController.java
@@ -0,0 +1,704 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.controller;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.*;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.model.configuration.Configuration;
+import org.jd.gui.model.history.History;
+import org.jd.gui.service.actions.ContextualActionsFactoryService;
+import org.jd.gui.service.container.ContainerFactoryService;
+import org.jd.gui.service.fileloader.FileLoaderService;
+import org.jd.gui.service.indexer.IndexerService;
+import org.jd.gui.service.mainpanel.PanelFactoryService;
+import org.jd.gui.service.pastehandler.PasteHandlerService;
+import org.jd.gui.service.platform.PlatformService;
+import org.jd.gui.service.preferencespanel.PreferencesPanelService;
+import org.jd.gui.service.sourceloader.SourceLoaderService;
+import org.jd.gui.service.sourcesaver.SourceSaverService;
+import org.jd.gui.service.treenode.TreeNodeFactoryService;
+import org.jd.gui.service.type.TypeFactoryService;
+import org.jd.gui.service.uriloader.UriLoaderService;
+import org.jd.gui.spi.*;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.net.UriUtil;
+import org.jd.gui.util.swing.SwingUtil;
+import org.jd.gui.view.MainView;
+
+import javax.swing.*;
+import javax.swing.filechooser.FileNameExtensionFilter;
+import javax.swing.filechooser.FileSystemView;
+import java.awt.*;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class MainController implements API {
+    protected Configuration configuration;
+    protected MainView mainView;
+
+    protected GoToController goToController;
+    protected OpenTypeController openTypeController;
+    protected OpenTypeHierarchyController openTypeHierarchyController;
+    protected PreferencesController preferencesController;
+    protected SearchInConstantPoolsController searchInConstantPoolsController;
+    protected SaveAllSourcesController saveAllSourcesController;
+    protected SelectLocationController selectLocationController;
+    protected AboutController aboutController;
+    protected SourceLoaderService sourceLoaderService;
+
+    protected History history = new History();
+    protected JComponent currentPage = null;
+    protected ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
+    protected ArrayList<IndexesChangeListener> containerChangeListeners = new ArrayList<>();
+
+    @SuppressWarnings("unchecked")
+    public MainController(Configuration configuration) {
+        this.configuration = configuration;
+
+        SwingUtil.invokeLater(() -> {
+            if (PlatformService.getInstance().isLinux()) {
+                // Fix for GTKLookAndFeel
+                SwingUtil.installGtkPopupBugWorkaround();
+            }
+
+            // Create main frame
+            mainView = new MainView(
+                configuration, this, history,
+                e -> onOpen(),
+                e -> onClose(),
+                e -> onSaveSource(),
+                e -> onSaveAllSources(),
+                e -> System.exit(0),
+                e -> onCopy(),
+                e -> onPaste(),
+                e -> onSelectAll(),
+                e -> onFind(),
+                e -> onFindPrevious(),
+                e -> onFindNext(),
+                e -> onFindCriteriaChanged(),
+                () -> onFindCriteriaChanged(),
+                e -> onOpenType(),
+                e -> onOpenTypeHierarchy(),
+                e -> onGoTo(),
+                e -> openURI(history.backward()),
+                e -> openURI(history.forward()),
+                e -> onSearch(),
+                e -> onJdWebSite(),
+                e -> onJdGuiIssues(),
+                e -> onJdCoreIssues(),
+                e -> onPreferences(),
+                e -> onAbout(),
+                () -> panelClosed(),
+                page -> onCurrentPageChanged((JComponent)page),
+                file -> openFile((File)file));
+        });
+	}
+
+	// --- Show GUI --- //
+    @SuppressWarnings("unchecked")
+	public void show(List<File> files) {
+        SwingUtil.invokeLater(() -> {
+            // Show main frame
+            mainView.show(configuration.getMainWindowLocation(), configuration.getMainWindowSize(), configuration.isMainWindowMaximize());
+            if (!files.isEmpty()) {
+                openFiles(files);
+            }
+        });
+
+        // Background initializations
+        executor.schedule(() -> {
+            // Background service initialization
+            UriLoaderService.getInstance();
+            FileLoaderService.getInstance();
+            ContainerFactoryService.getInstance();
+            IndexerService.getInstance();
+            TreeNodeFactoryService.getInstance();
+            TypeFactoryService.getInstance();
+
+            SwingUtil.invokeLater(() -> {
+                // Populate recent files menu
+                mainView.updateRecentFilesMenu(configuration.getRecentFiles());
+                // Background controller creation
+                JFrame mainFrame = mainView.getMainFrame();
+                saveAllSourcesController = new SaveAllSourcesController(MainController.this, mainFrame);
+                containerChangeListeners.add(openTypeController = new OpenTypeController(MainController.this, executor, mainFrame));
+                containerChangeListeners.add(openTypeHierarchyController = new OpenTypeHierarchyController(MainController.this, executor, mainFrame));
+                goToController = new GoToController(configuration, mainFrame);
+                containerChangeListeners.add(searchInConstantPoolsController = new SearchInConstantPoolsController(MainController.this, executor, mainFrame));
+                preferencesController = new PreferencesController(configuration, mainFrame, PreferencesPanelService.getInstance().getProviders());
+                selectLocationController = new SelectLocationController(MainController.this, mainFrame);
+                aboutController = new AboutController(mainFrame);
+                sourceLoaderService = new SourceLoaderService();
+                // Add listeners
+                mainFrame.addComponentListener(new MainFrameListener(configuration));
+                // Set drop files transfer handler
+                mainFrame.setTransferHandler(new FilesTransferHandler());
+                // Background class loading
+                new JFileChooser().addChoosableFileFilter(new FileNameExtensionFilter("", "dummy"));
+                FileSystemView.getFileSystemView().isFileSystemRoot(new File("dummy"));
+                new JLayer();
+            });
+        }, 400, TimeUnit.MILLISECONDS);
+
+        PasteHandlerService.getInstance();
+        PreferencesPanelService.getInstance();
+        ContextualActionsFactoryService.getInstance();
+        SourceSaverService.getInstance();
+    }
+
+	// --- Actions --- //
+    protected void onOpen() {
+        Map<String, FileLoader> loaders = FileLoaderService.getInstance().getMapProviders();
+        StringBuilder sb = new StringBuilder();
+        ArrayList<String> extensions = new ArrayList<>(loaders.keySet());
+
+        extensions.sort(null);
+
+        for (String extension : extensions) {
+            sb.append("*.").append(extension).append(", ");
+        }
+
+        sb.setLength(sb.length()-2);
+
+        String description = sb.toString();
+        String[] array = extensions.toArray(new String[0]);
+        JFileChooser chooser = new JFileChooser();
+
+        chooser.removeChoosableFileFilter(chooser.getFileFilter());
+        chooser.addChoosableFileFilter(new FileNameExtensionFilter("All files (" + description + ")", array));
+
+        for (String extension : extensions) {
+            FileLoader loader = loaders.get(extension);
+            chooser.addChoosableFileFilter(new FileNameExtensionFilter(loader.getDescription(), loader.getExtensions()));
+        }
+
+        chooser.setCurrentDirectory(configuration.getRecentLoadDirectory());
+
+        if (chooser.showOpenDialog(mainView.getMainFrame()) == JFileChooser.APPROVE_OPTION) {
+            configuration.setRecentLoadDirectory(chooser.getCurrentDirectory());
+            openFile(chooser.getSelectedFile());
+        }
+	}
+
+    protected void onClose() {
+        mainView.closeCurrentTab();
+    }
+
+    protected void onSaveSource() {
+        if (currentPage instanceof ContentSavable) {
+            JFileChooser chooser = new JFileChooser();
+            JFrame mainFrame = mainView.getMainFrame();
+
+            chooser.setSelectedFile(new File(configuration.getRecentSaveDirectory(), ((ContentSavable)currentPage).getFileName()));
+
+            if (chooser.showSaveDialog(mainFrame) == JFileChooser.APPROVE_OPTION) {
+                File selectedFile = chooser.getSelectedFile();
+
+                configuration.setRecentSaveDirectory(chooser.getCurrentDirectory());
+
+                if (selectedFile.exists()) {
+                    String title = "Are you sure?";
+                    String message = "The file '" + selectedFile.getAbsolutePath() + "' already isContainsIn.\n Do you want to replace the existing file?";
+
+                    if (JOptionPane.showConfirmDialog(mainFrame, message, title, JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
+                        save(selectedFile);
+                    }
+                } else {
+                    save(selectedFile);
+                }
+            }
+        }
+    }
+
+    protected void save(File selectedFile) {
+        try (OutputStream os = new FileOutputStream(selectedFile)) {
+            ((ContentSavable)currentPage).save(this, os);
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    protected void onSaveAllSources() {
+        if (! saveAllSourcesController.isActivated()) {
+            JComponent currentPanel = mainView.getSelectedMainPanel();
+
+            if (currentPanel instanceof SourcesSavable) {
+                SourcesSavable sourcesSavable = (SourcesSavable)currentPanel;
+                JFileChooser chooser = new JFileChooser();
+                JFrame mainFrame = mainView.getMainFrame();
+
+                chooser.setSelectedFile(new File(configuration.getRecentSaveDirectory(), sourcesSavable.getSourceFileName()));
+
+                if (chooser.showSaveDialog(mainFrame) == JFileChooser.APPROVE_OPTION) {
+                    File selectedFile = chooser.getSelectedFile();
+
+                    configuration.setRecentSaveDirectory(chooser.getCurrentDirectory());
+
+                    if (selectedFile.exists()) {
+                        String title = "Are you sure?";
+                        String message = "The file '" + selectedFile.getAbsolutePath() + "' already isContainsIn.\n Do you want to replace the existing file?";
+
+                        if (JOptionPane.showConfirmDialog(mainFrame, message, title, JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
+                            saveAllSourcesController.show(executor, sourcesSavable, selectedFile);
+                        }
+                    } else {
+                        saveAllSourcesController.show(executor, sourcesSavable, selectedFile);
+                    }
+                }
+            }
+        }
+    }
+
+    protected void onCopy() {
+        if (currentPage instanceof ContentCopyable) {
+            ((ContentCopyable)currentPage).copy();
+        }
+    }
+
+    protected void onPaste() {
+        try {
+            Transferable transferable = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
+
+            if ((transferable != null) && transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) {
+                Object obj = transferable.getTransferData(DataFlavor.stringFlavor);
+                PasteHandler pasteHandler = PasteHandlerService.getInstance().get(obj);
+
+                if (pasteHandler != null) {
+                    pasteHandler.paste(this, obj);
+                }
+            }
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    protected void onSelectAll() {
+        if (currentPage instanceof ContentSelectable) {
+            ((ContentSelectable)currentPage).selectAll();
+        }
+    }
+
+    protected void onFind() {
+        if (currentPage instanceof ContentSearchable) {
+            mainView.showFindPanel();
+        }
+    }
+
+    protected void onFindCriteriaChanged() {
+        if (currentPage instanceof ContentSearchable) {
+            mainView.setFindBackgroundColor(((ContentSearchable)currentPage).highlightText(mainView.getFindText(), mainView.getFindCaseSensitive()));
+        }
+    }
+
+    protected void onFindNext() {
+        if (currentPage instanceof ContentSearchable) {
+            ((ContentSearchable)currentPage).findNext(mainView.getFindText(), mainView.getFindCaseSensitive());
+        }
+    }
+
+    protected void onOpenType() {
+        openTypeController.show(getCollectionOfFutureIndexes(), uri -> openURI(uri));
+    }
+
+    protected void onOpenTypeHierarchy() {
+        if (currentPage instanceof FocusedTypeGettable) {
+            FocusedTypeGettable ftg = (FocusedTypeGettable)currentPage;
+            openTypeHierarchyController.show(getCollectionOfFutureIndexes(), ftg.getEntry(), ftg.getFocusedTypeName(), uri -> openURI(uri));
+        }
+    }
+
+    protected void onGoTo() {
+        if (currentPage instanceof LineNumberNavigable) {
+            LineNumberNavigable lnn = (LineNumberNavigable)currentPage;
+            goToController.show(lnn, lineNumber -> lnn.goToLineNumber(lineNumber));
+        }
+    }
+
+    protected void onSearch() {
+        searchInConstantPoolsController.show(getCollectionOfFutureIndexes(), uri -> openURI(uri));
+    }
+
+    protected void onFindPrevious() {
+        if (currentPage instanceof ContentSearchable) {
+            ContentSearchable cs = (ContentSearchable)currentPage;
+            cs.findPrevious(mainView.getFindText(), mainView.getFindCaseSensitive());
+        }
+    }
+
+    protected void onJdWebSite() {
+        if (Desktop.isDesktopSupported()) {
+            Desktop desktop = Desktop.getDesktop();
+            if (desktop.isSupported(Desktop.Action.BROWSE)) {
+                try {
+                    desktop.browse(URI.create("http://java-decompiler.github.io"));
+                } catch (IOException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            }
+        }
+    }
+
+    protected void onJdGuiIssues() {
+        if (Desktop.isDesktopSupported()) {
+            Desktop desktop = Desktop.getDesktop();
+            if (desktop.isSupported(Desktop.Action.BROWSE)) {
+                try {
+                    desktop.browse(URI.create("https://github.com/java-decompiler/jd-gui/issues"));
+                } catch (IOException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            }
+        }
+    }
+
+    protected void onJdCoreIssues() {
+        if (Desktop.isDesktopSupported()) {
+            Desktop desktop = Desktop.getDesktop();
+            if (desktop.isSupported(Desktop.Action.BROWSE)) {
+                try {
+                    desktop.browse(URI.create("https://github.com/java-decompiler/jd-core/issues"));
+                } catch (IOException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void onPreferences() {
+        preferencesController.show(() -> {
+            checkPreferencesChange(currentPage);
+            mainView.preferencesChanged(getPreferences());
+        });
+    }
+
+    protected void onAbout() {
+        aboutController.show();
+    }
+
+    protected void onCurrentPageChanged(JComponent page) {
+        currentPage = page;
+        checkPreferencesChange(page);
+        checkIndexesChange(page);
+    }
+
+    protected void checkPreferencesChange(JComponent page) {
+        if (page instanceof PreferencesChangeListener) {
+            Map<String, String> preferences = configuration.getPreferences();
+            Integer currentHashcode = Integer.valueOf(preferences.hashCode());
+            Integer lastHashcode = (Integer)page.getClientProperty("preferences-hashCode");
+
+            if (!currentHashcode.equals(lastHashcode)) {
+                ((PreferencesChangeListener)page).preferencesChanged(preferences);
+                page.putClientProperty("preferences-hashCode", currentHashcode);
+            }
+        }
+    }
+
+    protected void checkIndexesChange(JComponent page) {
+        if (page instanceof IndexesChangeListener) {
+            Collection<Future<Indexes>> collectionOfFutureIndexes = getCollectionOfFutureIndexes();
+            Integer currentHashcode = Integer.valueOf(collectionOfFutureIndexes.hashCode());
+            Integer lastHashcode = (Integer)page.getClientProperty("collectionOfFutureIndexes-hashCode");
+
+            if (!currentHashcode.equals(lastHashcode)) {
+                ((IndexesChangeListener)page).indexesChanged(collectionOfFutureIndexes);
+                page.putClientProperty("collectionOfFutureIndexes-hashCode", currentHashcode);
+            }
+        }
+    }
+
+    // --- Operations --- //
+    public void openFile(File file) {
+        openFiles(Collections.singletonList(file));
+    }
+
+    @SuppressWarnings("unchecked")
+    public void openFiles(List<File> files) {
+        ArrayList<String> errors = new ArrayList<>();
+
+        for (File file : files) {
+            // Check input file
+            if (file.exists()) {
+                FileLoader loader = getFileLoader(file);
+                if ((loader != null) && !loader.accept(this, file)) {
+                    errors.add("Invalid input fileloader: '" + file.getAbsolutePath() + "'");
+                }
+            } else {
+                errors.add("File not found: '" + file.getAbsolutePath() + "'");
+            }
+        }
+
+        if (errors.isEmpty()) {
+            for (File file : files) {
+                if (openURI(file.toURI())) {
+                    configuration.addRecentFile(file);
+                    mainView.updateRecentFilesMenu(configuration.getRecentFiles());
+                }
+            }
+        } else {
+            StringBuilder messages = new StringBuilder();
+            int index = 0;
+
+            for (String error : errors) {
+                if (index > 0) {
+                    messages.append('\n');
+                }
+                if (index >= 20) {
+                    messages.append("...");
+                    break;
+                }
+                messages.append(error);
+                index++;
+            }
+
+            JOptionPane.showMessageDialog(mainView.getMainFrame(), messages.toString(), "Error", JOptionPane.ERROR_MESSAGE);
+        }
+    }
+
+    // --- Drop files transfer handler --- //
+    protected class FilesTransferHandler extends TransferHandler {
+        @Override
+        public boolean canImport(TransferHandler.TransferSupport info) {
+            return info.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public boolean importData(TransferHandler.TransferSupport info) {
+            if (info.isDrop() && info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
+                try {
+                    openFiles((List<File>)info.getTransferable().getTransferData(DataFlavor.javaFileListFlavor));
+                    return true;
+                } catch (Exception e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            }
+            return false;
+        }
+    }
+
+    // --- ComponentListener --- //
+    protected class MainFrameListener extends ComponentAdapter {
+        protected Configuration configuration;
+
+        public MainFrameListener(Configuration configuration) {
+            this.configuration = configuration;
+        }
+
+        @Override
+        public void componentMoved(ComponentEvent e) {
+            JFrame mainFrame = mainView.getMainFrame();
+
+            if ((mainFrame.getExtendedState() & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH) {
+                configuration.setMainWindowMaximize(true);
+            } else {
+                configuration.setMainWindowLocation(mainFrame.getLocation());
+                configuration.setMainWindowMaximize(false);
+            }
+        }
+
+        @Override
+        public void componentResized(ComponentEvent e) {
+            JFrame mainFrame = mainView.getMainFrame();
+
+            if ((mainFrame.getExtendedState() & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH) {
+                configuration.setMainWindowMaximize(true);
+            } else {
+                configuration.setMainWindowSize(mainFrame.getSize());
+                configuration.setMainWindowMaximize(false);
+            }
+        }
+    }
+
+    protected void panelClosed() {
+        SwingUtil.invokeLater(() -> {
+            // Fire 'indexesChanged' event
+            Collection<Future<Indexes>> collectionOfFutureIndexes = getCollectionOfFutureIndexes();
+            for (IndexesChangeListener listener : containerChangeListeners) {
+                listener.indexesChanged(collectionOfFutureIndexes);
+            }
+            if (currentPage instanceof IndexesChangeListener) {
+                ((IndexesChangeListener)currentPage).indexesChanged(collectionOfFutureIndexes);
+            }
+        });
+    }
+
+    // --- API --- //
+    @Override
+    @SuppressWarnings("unchecked")
+    public boolean openURI(URI uri) {
+        if (uri != null) {
+            boolean success = mainView.openUri(uri);
+
+            if (success == false) {
+                UriLoader uriLoader = getUriLoader(uri);
+                if (uriLoader != null) {
+                    success = uriLoader.load(this, uri);
+                }
+            }
+
+            if (success) {
+                addURI(uri);
+            }
+
+            return success;
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean openURI(int x, int y, Collection<Container.Entry> entries, String query, String fragment) {
+        if (entries != null) {
+            if (entries.size() == 1) {
+                // Open the single entry uri
+                Container.Entry entry = entries.iterator().next();
+                return openURI(UriUtil.createURI(this, getCollectionOfFutureIndexes(), entry, query, fragment));
+            } else {
+                // Multiple entries -> Open a "Select location" popup
+                Collection<Future<Indexes>> collectionOfFutureIndexes = getCollectionOfFutureIndexes();
+                selectLocationController.show(
+                    new Point(x+(16+2), y+2),
+                    entries,
+                    entry -> openURI(UriUtil.createURI(this, collectionOfFutureIndexes, entry, query, fragment)), // entry selected closure
+                    () -> {});                                                                                    // popup close closure
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public void addURI(URI uri) {
+        history.add(uri);
+        SwingUtil.invokeLater(() -> {
+            mainView.updateHistoryActions();
+        });
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends JComponent & UriGettable> void addPanel(String title, Icon icon, String tip, T component) {
+        mainView.addMainPanel(title, icon, tip, component);
+
+        if (component instanceof ContentIndexable) {
+            Future<Indexes> futureIndexes = executor.submit(() -> {
+                Indexes indexes = ((ContentIndexable)component).index(this);
+
+                SwingUtil.invokeLater(() -> {
+                    // Fire 'indexesChanged' event
+                    Collection<Future<Indexes>> collectionOfFutureIndexes = getCollectionOfFutureIndexes();
+                    for (IndexesChangeListener listener : containerChangeListeners) {
+                        listener.indexesChanged(collectionOfFutureIndexes);
+                    }
+                    if (currentPage instanceof IndexesChangeListener) {
+                        ((IndexesChangeListener) currentPage).indexesChanged(collectionOfFutureIndexes);
+                    }
+                });
+
+                return indexes;
+            });
+
+            component.putClientProperty("indexes", futureIndexes);
+        }
+    }
+
+    @Override public Collection<Action> getContextualActions(Container.Entry entry, String fragment) { return ContextualActionsFactoryService.getInstance().get(this, entry, fragment); }
+
+    @Override public FileLoader getFileLoader(File file) { return FileLoaderService.getInstance().get(this, file); }
+
+    @Override public UriLoader getUriLoader(URI uri) { return UriLoaderService.getInstance().get(this, uri); }
+
+    @Override public PanelFactory getMainPanelFactory(Container container) { return PanelFactoryService.getInstance().get(container); }
+
+    @Override public ContainerFactory getContainerFactory(Path rootPath) { return ContainerFactoryService.getInstance().get(this, rootPath); }
+
+    @Override public TreeNodeFactory getTreeNodeFactory(Container.Entry entry) { return TreeNodeFactoryService.getInstance().get(entry); }
+
+    @Override public TypeFactory getTypeFactory(Container.Entry entry) { return TypeFactoryService.getInstance().get(entry); }
+
+    @Override public Indexer getIndexer(Container.Entry entry) { return IndexerService.getInstance().get(entry); }
+
+    @Override public SourceSaver getSourceSaver(Container.Entry entry) { return SourceSaverService.getInstance().get(entry); }
+
+    @Override public Map<String, String> getPreferences() { return configuration.getPreferences(); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Collection<Future<Indexes>> getCollectionOfFutureIndexes() {
+        List<JComponent> mainPanels = mainView.getMainPanels();
+        ArrayList<Future<Indexes>> list = new ArrayList<Future<Indexes>>(mainPanels.size()) {
+            @Override
+            public int hashCode() {
+                int hashCode = 1;
+
+                try {
+                    for (Future<Indexes> futureIndexes : this) {
+                        hashCode *= 31;
+
+                        if (futureIndexes.isDone()) {
+                            hashCode += futureIndexes.get().hashCode();
+                        }
+                    }
+                } catch (Exception e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+
+                return hashCode;
+            }
+        };
+
+        for (JComponent panel : mainPanels) {
+            Future<Indexes> futureIndexes = (Future<Indexes>)panel.getClientProperty("indexes");
+            if (futureIndexes != null) {
+                list.add(futureIndexes);
+            }
+        }
+
+        return list;
+    }
+
+    @Override
+    public String getSource(Container.Entry entry) {
+        return sourceLoaderService.getSource(this, entry);
+    }
+
+    @Override
+    public void loadSource(Container.Entry entry, LoadSourceListener listener) {
+        executor.execute(() -> {
+            String source = sourceLoaderService.loadSource(this, entry);
+
+            if ((source != null) && !source.isEmpty()) {
+                listener.sourceLoaded(source);
+            }
+        });
+    }
+
+    @Override
+    public File loadSourceFile(Container.Entry entry) {
+        return sourceLoaderService.getSourceFile(this, entry);
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/controller/OpenTypeController.java b/app/src/main/java/org/jd/gui/controller/OpenTypeController.java
new file mode 100644
index 0000000..b79674a
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/controller/OpenTypeController.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.controller;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.IndexesChangeListener;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.net.UriUtil;
+import org.jd.gui.view.OpenTypeView;
+
+import javax.swing.*;
+import java.awt.*;
+import java.net.URI;
+import java.util.*;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+
+public class OpenTypeController implements IndexesChangeListener {
+    protected static final int CACHE_MAX_ENTRIES = 5*20;
+
+    protected API api;
+    protected ScheduledExecutorService executor;
+    protected Collection<Future<Indexes>> collectionOfFutureIndexes;
+    protected Consumer<URI> openCallback;
+
+    protected JFrame mainFrame;
+    protected OpenTypeView openTypeView;
+    protected SelectLocationController selectLocationController;
+
+    protected long indexesHashCode = 0L;
+    protected Map<String, Map<String, Collection>> cache;
+
+    public OpenTypeController(API api, ScheduledExecutorService executor, JFrame mainFrame) {
+        this.api = api;
+        this.executor = executor;
+        this.mainFrame = mainFrame;
+        // Create UI
+        openTypeView = new OpenTypeView(api, mainFrame, this::updateList, this::onTypeSelected);
+        selectLocationController = new SelectLocationController(api, mainFrame);
+        // Create result cache
+        cache = new LinkedHashMap<String, Map<String, Collection>>(CACHE_MAX_ENTRIES*3/2, 0.7f, true) {
+            @Override
+            protected boolean removeEldestEntry(Map.Entry<String, Map<String, Collection>> eldest) {
+                return size() > CACHE_MAX_ENTRIES;
+            }
+        };
+    }
+
+    public void show(Collection<Future<Indexes>> collectionOfFutureIndexes, Consumer<URI> openCallback) {
+        // Init attributes
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        this.openCallback = openCallback;
+        // Refresh view
+        long hashCode = collectionOfFutureIndexes.hashCode();
+        if (hashCode != indexesHashCode) {
+            // List of indexes has changed -> Refresh result list
+            updateList(openTypeView.getPattern());
+            indexesHashCode = hashCode;
+        }
+        // Show
+        openTypeView.show();
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void updateList(String pattern) {
+        int patternLength = pattern.length();
+
+        if (patternLength == 0) {
+            // Display
+            openTypeView.updateList(Collections.emptyMap());
+        } else {
+            executor.execute(() -> {
+                // Waiting the end of indexation...
+                openTypeView.showWaitCursor();
+
+                Pattern regExpPattern = createRegExpPattern(pattern);
+                Map<String, Collection<Container.Entry>> result = new HashMap<>();
+
+                try {
+                    for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
+                        if (futureIndexes.isDone()) {
+                            Indexes indexes = futureIndexes.get();
+                            String key = String.valueOf(indexes.hashCode()) + "***" + pattern;
+                            Map<String, Collection> matchingEntries = cache.get(key);
+
+                            if (matchingEntries != null) {
+                                // Merge 'result' and 'matchingEntries'
+                                for (Map.Entry<String, Collection> mapEntry : matchingEntries.entrySet()) {
+                                    Collection<Container.Entry> collection = result.get(mapEntry.getKey());
+                                    if (collection == null) {
+                                        result.put(mapEntry.getKey(), collection = new HashSet<>());
+                                    }
+                                    collection.addAll(mapEntry.getValue());
+                                }
+                            } else {
+                                // Waiting the end of indexation...
+                                Map<String, Collection> index = indexes.getIndex("typeDeclarations");
+
+                                if ((index != null) && !index.isEmpty()) {
+                                    matchingEntries = new HashMap<>();
+
+                                    // Filter
+                                    if (patternLength == 1) {
+                                        match(pattern.charAt(0), index, matchingEntries);
+                                    } else {
+                                        String lastKey = key.substring(0, patternLength - 1);
+                                        Map<String, Collection> lastResult = cache.get(lastKey);
+
+                                        if (lastResult != null) {
+                                            match(regExpPattern, lastResult, matchingEntries);
+                                        } else {
+                                            match(regExpPattern, index, matchingEntries);
+                                        }
+                                    }
+
+                                    // Store 'matchingEntries'
+                                    cache.put(key, matchingEntries);
+
+                                    // Merge 'result' and 'matchingEntries'
+                                    for (Map.Entry<String, Collection> mapEntry : matchingEntries.entrySet()) {
+                                        Collection<Container.Entry> collection = result.get(mapEntry.getKey());
+                                        if (collection == null) {
+                                            result.put(mapEntry.getKey(), collection = new HashSet<>());
+                                        }
+                                        collection.addAll(mapEntry.getValue());
+                                    }
+                                }
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+
+                SwingUtilities.invokeLater(() -> {
+                    openTypeView.hideWaitCursor();
+                    // Display
+                    openTypeView.updateList(result);
+                });
+            });
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected static void match(char c, Map<String, Collection> index, Map<String, Collection> result) {
+        // Filter
+        if (Character.isLowerCase(c)) {
+            char upperCase = Character.toUpperCase(c);
+
+            for (Map.Entry<String, Collection> mapEntry : index.entrySet()) {
+                String typeName = mapEntry.getKey();
+                Collection<Container.Entry> entries = mapEntry.getValue();
+                // Search last package separator
+                int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1;
+                int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1;
+                int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex);
+
+                if (lastIndex < typeName.length()) {
+                    char first = typeName.charAt(lastIndex);
+
+                    if ((first == c) || (first == upperCase)) {
+                        add(result, typeName, entries);
+                    }
+                }
+            }
+        } else {
+            for (Map.Entry<String, Collection> mapEntry : index.entrySet()) {
+                String typeName = mapEntry.getKey();
+                Collection<Container.Entry> entries = mapEntry.getValue();
+                // Search last package separator
+                int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1;
+                int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1;
+                int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex);
+
+                if ((lastIndex < typeName.length()) && (typeName.charAt(lastIndex) == c)) {
+                    add(result, typeName, entries);
+                }
+            }
+        }
+    }
+
+    /**
+     * Create a regular expression to match package, type and inner type name.
+     *
+     * Rules:
+     *  '*'        matches 0 ou N characters
+     *  '?'        matches 1 character
+     *  lower case matches insensitive case
+     *  upper case matches upper case
+     */
+    protected static Pattern createRegExpPattern(String pattern) {
+        // Create regular expression
+        int patternLength = pattern.length();
+        StringBuilder sbPattern = new StringBuilder(patternLength * 4);
+
+        for (int i=0; i<patternLength; i++) {
+            char c = pattern.charAt(i);
+
+            if (Character.isUpperCase(c)) {
+                if (i > 1) {
+                    sbPattern.append(".*");
+                }
+                sbPattern.append(c);
+            } else if (Character.isLowerCase(c)) {
+                sbPattern.append('[').append(c).append(Character.toUpperCase(c)).append(']');
+            } else if (c == '*') {
+                sbPattern.append(".*");
+            } else if (c == '?') {
+                sbPattern.append(".");
+            } else {
+                sbPattern.append(c);
+            }
+        }
+
+        sbPattern.append(".*");
+
+        return Pattern.compile(sbPattern.toString());
+    }
+
+    @SuppressWarnings("unchecked")
+    protected static void match(Pattern regExpPattern, Map<String, Collection> index, Map<String, Collection> result) {
+        for (Map.Entry<String, Collection> mapEntry : index.entrySet()) {
+            String typeName = mapEntry.getKey();
+            Collection<Container.Entry> entries = mapEntry.getValue();
+            // Search last package separator
+            int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1;
+            int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1;
+            int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex);
+
+            if (regExpPattern.matcher(typeName.substring(lastIndex)).matches()) {
+                add(result, typeName, entries);
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected static void add(Map<String, Collection> map, String key, Collection value) {
+        Collection<Container.Entry> collection = map.get(key);
+
+        if (collection == null) {
+            map.put(key, collection = new HashSet<>());
+        }
+
+        collection.addAll(value);
+    }
+
+    protected void onTypeSelected(Point leftBottom, Collection<Container.Entry> entries, String typeName) {
+        if (entries.size() == 1) {
+            // Open the single entry uri
+            openCallback.accept(UriUtil.createURI(api, collectionOfFutureIndexes, entries.iterator().next(), null, typeName));
+        } else {
+            // Multiple entries -> Open a "Select location" popup
+            selectLocationController.show(
+                new Point(leftBottom.x+(16+2), leftBottom.y+2),
+                entries,
+                (entry) -> openCallback.accept(UriUtil.createURI(api, collectionOfFutureIndexes, entry, null, typeName)), // entry selected callback
+                () -> openTypeView.focus());                                                                              // popup close callback
+        }
+    }
+
+    // --- IndexesChangeListener --- //
+    public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        if (openTypeView.isVisible()) {
+            // Update the list of containers
+            this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+            // And refresh
+            updateList(openTypeView.getPattern());
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/controller/OpenTypeHierarchyController.java b/app/src/main/java/org/jd/gui/controller/OpenTypeHierarchyController.java
new file mode 100644
index 0000000..1b51a22
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/controller/OpenTypeHierarchyController.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.controller;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.IndexesChangeListener;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.net.UriUtil;
+import org.jd.gui.view.OpenTypeHierarchyView;
+
+import javax.swing.*;
+import java.awt.*;
+import java.net.URI;
+import java.util.Collection;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+
+public class OpenTypeHierarchyController implements IndexesChangeListener {
+    protected API api;
+    private ScheduledExecutorService executor;
+
+    protected JFrame mainFrame;
+    protected OpenTypeHierarchyView openTypeHierarchyView;
+    protected SelectLocationController selectLocationController;
+
+    protected Collection<Future<Indexes>> collectionOfFutureIndexes;
+    protected Consumer<URI> openCallback;
+
+    public OpenTypeHierarchyController(API api, ScheduledExecutorService executor, JFrame mainFrame) {
+        this.api = api;
+        this.executor = executor;
+        this.mainFrame = mainFrame;
+        // Create UI
+        openTypeHierarchyView = new OpenTypeHierarchyView(api, mainFrame, this::onTypeSelected);
+        selectLocationController = new SelectLocationController(api, mainFrame);
+    }
+
+    public void show(Collection<Future<Indexes>> collectionOfFutureIndexes, Container.Entry entry, String typeName, Consumer<URI> openCallback) {
+        // Init attributes
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        this.openCallback = openCallback;
+        executor.execute(() -> {
+            // Waiting the end of indexation...
+            openTypeHierarchyView.showWaitCursor();
+            SwingUtilities.invokeLater(() -> {
+                openTypeHierarchyView.hideWaitCursor();
+                // Show
+                openTypeHierarchyView.show(collectionOfFutureIndexes, entry, typeName);
+            });
+        });
+    }
+
+    protected void onTypeSelected(Point leftBottom, Collection<Container.Entry> entries, String typeName) {
+        if (entries.size() == 1) {
+            // Open the single entry uri
+            openCallback.accept(UriUtil.createURI(api, collectionOfFutureIndexes, entries.iterator().next(), null, typeName));
+        } else {
+            // Multiple entries -> Open a "Select location" popup
+            selectLocationController.show(
+                new Point(leftBottom.x+(16+2), leftBottom.y+2),
+                entries,
+                (entry) -> openCallback.accept(UriUtil.createURI(api, collectionOfFutureIndexes, entry, null, typeName)), // entry selected
+                () -> openTypeHierarchyView.focus());                                                               // popup closeClosure
+        }
+    }
+
+    // --- IndexesChangeListener --- //
+    public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        if (openTypeHierarchyView.isVisible()) {
+            // Update the list of containers
+            this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+            // And refresh
+            openTypeHierarchyView.updateTree(collectionOfFutureIndexes);
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/controller/PreferencesController.java b/app/src/main/java/org/jd/gui/controller/PreferencesController.java
new file mode 100644
index 0000000..1d3fbec
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/controller/PreferencesController.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.controller;
+
+import org.jd.gui.model.configuration.Configuration;
+import org.jd.gui.spi.PreferencesPanel;
+import org.jd.gui.view.PreferencesView;
+
+import javax.swing.*;
+import java.util.Collection;
+
+public class PreferencesController {
+    protected PreferencesView preferencesView;
+
+    public PreferencesController(Configuration configuration, JFrame mainFrame, Collection<PreferencesPanel> panels) {
+        // Create UI
+        preferencesView = new PreferencesView(configuration, mainFrame, panels);
+    }
+
+    public void show(Runnable okCallback) {
+        // Show
+        preferencesView.show(okCallback);
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/controller/SaveAllSourcesController.java b/app/src/main/java/org/jd/gui/controller/SaveAllSourcesController.java
new file mode 100644
index 0000000..5e232bf
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/controller/SaveAllSourcesController.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.controller;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.SourcesSavable;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.view.SaveAllSourcesView;
+
+import javax.swing.*;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.ScheduledExecutorService;
+
+public class SaveAllSourcesController implements SourcesSavable.Controller, SourcesSavable.Listener {
+    protected API api;
+    protected SaveAllSourcesView saveAllSourcesView;
+    protected boolean cancel;
+    protected int counter;
+    protected int mask;
+
+    public SaveAllSourcesController(API api, JFrame mainFrame) {
+        this.api = api;
+        // Create UI
+        this.saveAllSourcesView = new SaveAllSourcesView(mainFrame, this::onCanceled);
+    }
+
+    public void show(ScheduledExecutorService executor, SourcesSavable savable, File file) {
+        // Show
+        this.saveAllSourcesView.show(file);
+        // Execute background task
+        executor.execute(() -> {
+            int fileCount = savable.getFileCount();
+
+            saveAllSourcesView.updateProgressBar(0);
+            saveAllSourcesView.setMaxValue(fileCount);
+
+            cancel = false;
+            counter = 0;
+            mask = 2;
+
+            while (fileCount > 64) {
+                fileCount >>= 1;
+                mask <<= 1;
+            }
+
+            mask--;
+
+            try {
+                Path path = Paths.get(file.toURI());
+                Files.deleteIfExists(path);
+
+                try {
+                    savable.save(api, this, this, path);
+                } catch (Exception e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                    saveAllSourcesView.showActionFailedDialog();
+                    cancel = true;
+                }
+
+                if (cancel) {
+                    Files.deleteIfExists(path);
+                }
+            } catch (Throwable t) {
+                assert ExceptionUtil.printStackTrace(t);
+            }
+
+            saveAllSourcesView.hide();
+        });
+    }
+
+    public boolean isActivated() { return saveAllSourcesView.isVisible(); }
+
+    protected void onCanceled() { cancel = true; }
+
+    // --- SourcesSavable.Controller --- //
+    @Override public boolean isCancelled() { return cancel; }
+
+    // --- SourcesSavable.Listener --- //
+    @Override
+    public void pathSaved(Path path) {
+        if (((counter++) & mask) == 0) {
+            saveAllSourcesView.updateProgressBar(counter);
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/controller/SearchInConstantPoolsController.java b/app/src/main/java/org/jd/gui/controller/SearchInConstantPoolsController.java
new file mode 100644
index 0000000..d87182b
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/controller/SearchInConstantPoolsController.java
@@ -0,0 +1,484 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.controller;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.IndexesChangeListener;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.api.model.Type;
+import org.jd.gui.model.container.DelegatingFilterContainer;
+import org.jd.gui.service.type.TypeFactoryService;
+import org.jd.gui.spi.TypeFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.function.TriConsumer;
+import org.jd.gui.view.SearchInConstantPoolsView;
+
+import javax.swing.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+
+public class SearchInConstantPoolsController implements IndexesChangeListener {
+    protected static final int CACHE_MAX_ENTRIES = 5*20*9;
+
+    protected API api;
+    protected ScheduledExecutorService executor;
+
+    protected JFrame mainFrame;
+    protected SearchInConstantPoolsView searchInConstantPoolsView;
+    protected Map<String, Map<String, Collection>> cache;
+    protected Set<DelegatingFilterContainer> delegatingFilterContainers = new HashSet<>();
+    protected Collection<Future<Indexes>> collectionOfFutureIndexes;
+    protected Consumer<URI> openCallback;
+    protected long indexesHashCode = 0L;
+
+    @SuppressWarnings("unchecked")
+    public SearchInConstantPoolsController(API api, ScheduledExecutorService executor, JFrame mainFrame) {
+        this.api = api;
+        this.executor = executor;
+        this.mainFrame = mainFrame;
+        // Create UI
+        this.searchInConstantPoolsView = new SearchInConstantPoolsView(
+            api, mainFrame,
+            new BiConsumer<String, Integer>() {
+                @Override public void accept(String pattern, Integer flags) { updateTree(pattern, flags); }
+            },
+            new TriConsumer<URI, String, Integer>() {
+                @Override public void accept(URI uri, String pattern, Integer flags) { onTypeSelected(uri, pattern, flags); }
+            }
+        );
+        // Create result cache
+        this.cache = new LinkedHashMap<String, Map<String, Collection>>(CACHE_MAX_ENTRIES*3/2, 0.7f, true) {
+            @Override
+            protected boolean removeEldestEntry(Map.Entry<String, Map<String, Collection>> eldest) {
+                return size() > CACHE_MAX_ENTRIES;
+            }
+        };
+    }
+
+    public void show(Collection<Future<Indexes>> collectionOfFutureIndexes, Consumer<URI> openCallback) {
+        // Init attributes
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        this.openCallback = openCallback;
+        // Refresh view
+        long hashCode = collectionOfFutureIndexes.hashCode();
+        if (hashCode != indexesHashCode) {
+            // List of indexes has changed
+            updateTree(searchInConstantPoolsView.getPattern(), searchInConstantPoolsView.getFlags());
+            indexesHashCode = hashCode;
+        }
+        // Show
+        searchInConstantPoolsView.show();
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void updateTree(String pattern, int flags) {
+        delegatingFilterContainers.clear();
+
+        executor.execute(() -> {
+            // Waiting the end of indexation...
+            searchInConstantPoolsView.showWaitCursor();
+
+            int matchingTypeCount = 0;
+            int patternLength = pattern.length();
+
+            if (patternLength > 0) {
+                try {
+                    for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
+                        if (futureIndexes.isDone()) {
+                            Indexes indexes = futureIndexes.get();
+                            HashSet<Container.Entry> matchingEntries = new HashSet<>();
+                            // Find matched entries
+                            filter(indexes, pattern, flags, matchingEntries);
+
+                            if (!matchingEntries.isEmpty()) {
+                                // Search root container with first matching entry
+                                Container.Entry parentEntry = matchingEntries.iterator().next();
+                                Container container = null;
+
+                                while (parentEntry.getContainer().getRoot() != null) {
+                                    container = parentEntry.getContainer();
+                                    parentEntry = container.getRoot().getParent();
+                                }
+
+                                // TODO In a future release, display matching strings, types, inner-types, fields and methods, not only matching files
+                                matchingEntries = getOuterEntries(matchingEntries);
+
+                                matchingTypeCount += matchingEntries.size();
+
+                                // Create a filtered container
+                                delegatingFilterContainers.add(new DelegatingFilterContainer(container, matchingEntries));
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            }
+
+            final int count = matchingTypeCount;
+
+            searchInConstantPoolsView.hideWaitCursor();
+            searchInConstantPoolsView.updateTree(delegatingFilterContainers, count);
+        });
+    }
+
+    protected HashSet<Container.Entry> getOuterEntries(Set<Container.Entry> matchingEntries) {
+        HashMap<Container.Entry, Container.Entry> innerTypeEntryToOuterTypeEntry = new HashMap<>();
+        HashSet<Container.Entry> matchingOuterEntriesSet = new HashSet<>();
+
+        for (Container.Entry entry : matchingEntries) {
+            TypeFactory typeFactory = TypeFactoryService.getInstance().get(entry);
+
+            if (typeFactory != null) {
+                Type type = typeFactory.make(api, entry, null);
+
+                if ((type != null) && (type.getOuterName() != null)) {
+                    Container.Entry outerTypeEntry = innerTypeEntryToOuterTypeEntry.get(entry);
+
+                    if (outerTypeEntry == null) {
+                        HashMap<String, Container.Entry> typeNameToEntry = new HashMap<>();
+                        HashMap<String, String> innerTypeNameToOuterTypeName = new HashMap<>();
+
+                        // Populate "typeNameToEntry" and "innerTypeNameToOuterTypeName"
+                        for (Container.Entry e : entry.getParent().getChildren()) {
+                            typeFactory = TypeFactoryService.getInstance().get(e);
+
+                            if (typeFactory != null) {
+                                type = typeFactory.make(api, e, null);
+
+                                if (type != null) {
+                                    typeNameToEntry.put(type.getName(), e);
+                                    if (type.getOuterName() != null) {
+                                        innerTypeNameToOuterTypeName.put(type.getName(), type.getOuterName());
+                                    }
+                                }
+                            }
+                        }
+
+                        // Search outer type entries and populate "innerTypeEntryToOuterTypeEntry"
+                        for (Map.Entry<String, String> e : innerTypeNameToOuterTypeName.entrySet()) {
+                            Container.Entry innerTypeEntry = typeNameToEntry.get(e.getKey());
+
+                            if (innerTypeEntry != null) {
+                                String outerTypeName = e.getValue();
+
+                                for (;;) {
+                                    String typeName = innerTypeNameToOuterTypeName.get(outerTypeName);
+                                    if (typeName != null) {
+                                        outerTypeName = typeName;
+                                    } else {
+                                        break;
+                                    }
+                                }
+
+                                outerTypeEntry = typeNameToEntry.get(outerTypeName);
+
+                                if (outerTypeEntry != null) {
+                                    innerTypeEntryToOuterTypeEntry.put(innerTypeEntry, outerTypeEntry);
+                                }
+                            }
+                        }
+
+                        // Get outer type entry
+                        outerTypeEntry = innerTypeEntryToOuterTypeEntry.get(entry);
+
+                        if (outerTypeEntry == null) {
+                            outerTypeEntry = entry;
+                        }
+                    }
+
+                    matchingOuterEntriesSet.add(outerTypeEntry);
+                } else{
+                    matchingOuterEntriesSet.add(entry);
+                }
+            }
+        }
+
+        return matchingOuterEntriesSet;
+    }
+
+    protected void filter(Indexes indexes, String pattern, int flags, Set<Container.Entry> matchingEntries) {
+        boolean declarations = ((flags & SearchInConstantPoolsView.SEARCH_DECLARATION) != 0);
+        boolean references = ((flags & SearchInConstantPoolsView.SEARCH_REFERENCE) != 0);
+
+        if ((flags & SearchInConstantPoolsView.SEARCH_TYPE) != 0) {
+            if (declarations)
+                match(indexes, "typeDeclarations", pattern,
+                      SearchInConstantPoolsController::matchTypeEntriesWithChar,
+                      SearchInConstantPoolsController::matchTypeEntriesWithString, matchingEntries);
+            if (references)
+                match(indexes, "typeReferences", pattern,
+                      SearchInConstantPoolsController::matchTypeEntriesWithChar,
+                      SearchInConstantPoolsController::matchTypeEntriesWithString, matchingEntries);
+        }
+
+        if ((flags & SearchInConstantPoolsView.SEARCH_CONSTRUCTOR) != 0) {
+            if (declarations)
+                match(indexes, "constructorDeclarations", pattern,
+                      SearchInConstantPoolsController::matchTypeEntriesWithChar,
+                      SearchInConstantPoolsController::matchTypeEntriesWithString, matchingEntries);
+            if (references)
+                match(indexes, "constructorReferences", pattern,
+                      SearchInConstantPoolsController::matchTypeEntriesWithChar,
+                      SearchInConstantPoolsController::matchTypeEntriesWithString, matchingEntries);
+        }
+
+        if ((flags & SearchInConstantPoolsView.SEARCH_METHOD) != 0) {
+            if (declarations)
+                match(indexes, "methodDeclarations", pattern,
+                      SearchInConstantPoolsController::matchWithChar,
+                      SearchInConstantPoolsController::matchWithString, matchingEntries);
+            if (references)
+                match(indexes, "methodReferences", pattern,
+                      SearchInConstantPoolsController::matchWithChar,
+                      SearchInConstantPoolsController::matchWithString, matchingEntries);
+        }
+
+        if ((flags & SearchInConstantPoolsView.SEARCH_FIELD) != 0) {
+            if (declarations)
+                match(indexes, "fieldDeclarations", pattern,
+                      SearchInConstantPoolsController::matchWithChar,
+                      SearchInConstantPoolsController::matchWithString, matchingEntries);
+            if (references)
+                match(indexes, "fieldReferences", pattern,
+                      SearchInConstantPoolsController::matchWithChar,
+                      SearchInConstantPoolsController::matchWithString, matchingEntries);
+        }
+
+        if ((flags & SearchInConstantPoolsView.SEARCH_STRING) != 0) {
+            if (declarations || references)
+                match(indexes, "strings", pattern,
+                      SearchInConstantPoolsController::matchWithChar,
+                      SearchInConstantPoolsController::matchWithString, matchingEntries);
+        }
+
+        if ((flags & SearchInConstantPoolsView.SEARCH_MODULE) != 0) {
+            if (declarations)
+                match(indexes, "javaModuleDeclarations", pattern,
+                        SearchInConstantPoolsController::matchWithChar,
+                        SearchInConstantPoolsController::matchWithString, matchingEntries);
+            if (references)
+                match(indexes, "javaModuleReferences", pattern,
+                        SearchInConstantPoolsController::matchWithChar,
+                        SearchInConstantPoolsController::matchWithString, matchingEntries);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void match(Indexes indexes, String indexName, String pattern,
+                         BiFunction<Character, Map<String, Collection>, Map<String, Collection>> matchWithCharFunction,
+                         BiFunction<String, Map<String, Collection>, Map<String, Collection>> matchWithStringFunction,
+                         Set<Container.Entry> matchingEntries) {
+        int patternLength = pattern.length();
+
+        if (patternLength > 0) {
+            String key = String.valueOf(indexes.hashCode()) + "***" + indexName + "***" + pattern;
+            Map<String, Collection> matchedEntries = cache.get(key);
+
+            if (matchedEntries == null) {
+                Map<String, Collection> index = indexes.getIndex(indexName);
+
+                if (index != null) {
+                    if (patternLength == 1) {
+                        matchedEntries = matchWithCharFunction.apply(pattern.charAt(0), index);
+                    } else {
+                        String lastKey = key.substring(0, key.length() - 1);
+                        Map<String, Collection> lastMatchedTypes = cache.get(lastKey);
+                        if (lastMatchedTypes != null) {
+                            matchedEntries = matchWithStringFunction.apply(pattern, lastMatchedTypes);
+                        } else {
+                            matchedEntries = matchWithStringFunction.apply(pattern, index);
+                        }
+                    }
+                }
+
+                // Cache matchingEntries
+                cache.put(key, matchedEntries);
+            }
+
+            if (matchedEntries != null) {
+                for (Collection<Container.Entry> entries : matchedEntries.values()) {
+                    matchingEntries.addAll(entries);
+                }
+            }
+        }
+    }
+
+    protected static Map<String, Collection> matchTypeEntriesWithChar(char c, Map<String, Collection> index) {
+        if ((c == '*') || (c == '?')) {
+            return index;
+        } else {
+            Map<String, Collection> map = new HashMap<>();
+
+            for (String typeName : index.keySet()) {
+                // Search last package separator
+                int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1;
+                int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1;
+                int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex);
+
+                if ((lastIndex < typeName.length()) && (typeName.charAt(lastIndex) == c)) {
+                    map.put(typeName, index.get(typeName));
+                }
+            }
+
+            return map;
+        }
+    }
+
+    protected static Map<String, Collection> matchTypeEntriesWithString(String pattern, Map<String, Collection> index) {
+        Pattern p = createPattern(pattern);
+        Map<String, Collection> map = new HashMap<>();
+
+        for (String typeName : index.keySet()) {
+            // Search last package separator
+            int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1;
+            int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1;
+            int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex);
+
+            if (p.matcher(typeName.substring(lastIndex)).matches()) {
+                map.put(typeName, index.get(typeName));
+            }
+        }
+
+        return map;
+    }
+
+    protected static Map<String, Collection> matchWithChar(char c, Map<String, Collection> index) {
+        if ((c == '*') || (c == '?')) {
+            return index;
+        } else {
+            Map<String, Collection> map = new HashMap<>();
+
+            for (String key : index.keySet()) {
+                if (!key.isEmpty() && (key.charAt(0) == c)) {
+                    map.put(key, index.get(key));
+                }
+            }
+
+            return map;
+        }
+    }
+
+    protected static Map<String, Collection> matchWithString(String pattern, Map<String, Collection> index) {
+        Pattern p = createPattern(pattern);
+        Map<String, Collection> map = new HashMap<>();
+
+        for (String key : index.keySet()) {
+            if (p.matcher(key).matches()) {
+                map.put(key, index.get(key));
+            }
+        }
+
+        return map;
+    }
+
+    /**
+     * Create a simple regular expression
+     *
+     * Rules:
+     *  '*'        matchTypeEntries 0 ou N characters
+     *  '?'        matchTypeEntries 1 character
+     */
+    protected static Pattern createPattern(String pattern) {
+        int patternLength = pattern.length();
+        StringBuilder sbPattern = new StringBuilder(patternLength * 2);
+
+        for (int i = 0; i < patternLength; i++) {
+            char c = pattern.charAt(i);
+
+            if (c == '*') {
+                sbPattern.append(".*");
+            } else if (c == '?') {
+                sbPattern.append('.');
+            } else if (c == '.') {
+                sbPattern.append("\\.");
+            } else {
+                sbPattern.append(c);
+            }
+        }
+
+        sbPattern.append(".*");
+
+        return Pattern.compile(sbPattern.toString());
+    }
+
+    protected void onTypeSelected(URI uri, String pattern, int flags) {
+        // Open the single entry uri
+        Container.Entry entry = null;
+
+        for (DelegatingFilterContainer container : delegatingFilterContainers) {
+            entry = container.getEntry(uri);
+            if (entry != null)
+                break;
+        }
+
+        if (entry != null) {
+            StringBuilder sbPattern = new StringBuilder(200 + pattern.length());
+
+            sbPattern.append("highlightPattern=");
+            sbPattern.append(pattern);
+            sbPattern.append("&highlightFlags=");
+
+            if ((flags & SearchInConstantPoolsView.SEARCH_DECLARATION) != 0)
+                sbPattern.append('d');
+            if ((flags & SearchInConstantPoolsView.SEARCH_REFERENCE) != 0)
+                sbPattern.append('r');
+            if ((flags & SearchInConstantPoolsView.SEARCH_TYPE) != 0)
+                sbPattern.append('t');
+            if ((flags & SearchInConstantPoolsView.SEARCH_CONSTRUCTOR) != 0)
+                sbPattern.append('c');
+            if ((flags & SearchInConstantPoolsView.SEARCH_METHOD) != 0)
+                sbPattern.append('m');
+            if ((flags & SearchInConstantPoolsView.SEARCH_FIELD) != 0)
+                sbPattern.append('f');
+            if ((flags & SearchInConstantPoolsView.SEARCH_STRING) != 0)
+                sbPattern.append('s');
+            if ((flags & SearchInConstantPoolsView.SEARCH_MODULE) != 0)
+                sbPattern.append('M');
+
+            // TODO In a future release, add 'highlightScope' to display search results in correct type and inner-type
+            // def type = TypeFactoryService.instance.get(entry)?.make(api, entry, null)
+            // if (type) {
+            //     sbPattern.append('&highlightScope=')
+            //     sbPattern.append(type.name)
+            //
+            //     def query = sbPattern.toString()
+            //     def outerPath = UriUtil.getOuterPath(collectionOfFutureIndexes, entry, type)
+            //
+            //     openClosure(new URI(entry.uri.scheme, entry.uri.host, outerPath, query, null))
+            // } else {
+                String query = sbPattern.toString();
+                URI u = entry.getUri();
+
+                try {
+                    openCallback.accept(new URI(u.getScheme(), u.getHost(), u.getPath(), query, null));
+                } catch (URISyntaxException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            // }
+        }
+    }
+
+    // --- IndexesChangeListener --- //
+    public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        if (searchInConstantPoolsView.isVisible()) {
+            // Update the list of containers
+            this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+            // And refresh
+            updateTree(searchInConstantPoolsView.getPattern(), searchInConstantPoolsView.getFlags());
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/controller/SelectLocationController.java b/app/src/main/java/org/jd/gui/controller/SelectLocationController.java
new file mode 100644
index 0000000..5f10557
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/controller/SelectLocationController.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.controller;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Type;
+import org.jd.gui.model.container.DelegatingFilterContainer;
+import org.jd.gui.service.type.TypeFactoryService;
+import org.jd.gui.spi.TypeFactory;
+import org.jd.gui.view.SelectLocationView;
+
+import javax.swing.*;
+import java.awt.*;
+import java.net.URI;
+import java.util.*;
+import java.util.function.Consumer;
+
+public class SelectLocationController {
+    protected static final ContainerEntryComparator CONTAINER_ENTRY_COMPARATOR = new ContainerEntryComparator();
+
+    protected API api;
+    protected SelectLocationView selectLocationView;
+
+    public SelectLocationController(API api, JFrame mainFrame) {
+        this.api = api;
+        // Create UI
+        selectLocationView = new SelectLocationView(api, mainFrame);
+    }
+
+    @SuppressWarnings("unchecked")
+    public void show(Point location, Collection<Container.Entry> entries, Consumer<Container.Entry> selectedLocationCallback, Runnable closeCallback) {
+        // Show UI
+        HashMap<Container, ArrayList<Container.Entry>> map = new HashMap<>();
+
+        for (Container.Entry entry : entries) {
+            Container container = entry.getContainer();
+
+            // Search root container
+            while (true) {
+                Container parentContainer = container.getRoot().getParent().getContainer();
+                if (parentContainer.getRoot() == null) {
+                    break;
+                } else {
+                    container = parentContainer;
+                }
+            }
+
+            ArrayList<Container.Entry> list = map.get(container);
+
+            if (list == null) {
+                map.put(container, list=new ArrayList<>());
+            }
+
+            list.add(entry);
+        }
+
+        HashSet<DelegatingFilterContainer> delegatingFilterContainers = new HashSet<>();
+
+        for (Map.Entry<Container, ArrayList<Container.Entry>> mapEntry : map.entrySet()) {
+            Container container = mapEntry.getKey();
+            // Create a filtered container
+            // TODO In a future release, display matching types and inner-types, not only matching files
+            delegatingFilterContainers.add(new DelegatingFilterContainer(container, getOuterEntries(mapEntry.getValue())));
+        }
+
+        Consumer<URI> selectedEntryCallback = uri -> onLocationSelected(delegatingFilterContainers, uri, selectedLocationCallback);
+
+        selectLocationView.show(location, delegatingFilterContainers, entries.size(), selectedEntryCallback, closeCallback);
+    }
+
+    protected Collection<Container.Entry> getOuterEntries(Collection<Container.Entry> entries) {
+        HashMap<Container.Entry, Container.Entry> innerTypeEntryToOuterTypeEntry = new HashMap<>();
+        HashSet<Container.Entry> outerEntriesSet = new HashSet<>();
+
+        for (Container.Entry entry : entries) {
+            Container.Entry outerTypeEntry = null;
+            TypeFactory factory = TypeFactoryService.getInstance().get(entry);
+
+            if (factory != null) {
+                Type type = factory.make(api, entry, null);
+
+                if ((type != null) && (type.getOuterName() != null)) {
+                    outerTypeEntry = innerTypeEntryToOuterTypeEntry.get(entry);
+
+                    if (outerTypeEntry == null) {
+                        HashMap<String, Container.Entry> typeNameToEntry = new HashMap<>();
+                        HashMap<String, String> innerTypeNameToOuterTypeName = new HashMap<>();
+
+                        // Populate "typeNameToEntry" and "innerTypeNameToOuterTypeName"
+                        for (Container.Entry e : entry.getParent().getChildren()) {
+                            factory = TypeFactoryService.getInstance().get(e);
+
+                            if (factory != null) {
+                                type = factory.make(api, e, null);
+
+                                if (type != null) {
+                                    typeNameToEntry.put(type.getName(), e);
+                                    if (type.getOuterName() != null) {
+                                        innerTypeNameToOuterTypeName.put(type.getName(), type.getOuterName());
+                                    }
+                                }
+                            }
+                        }
+
+                        // Search outer type entries and populate "innerTypeEntryToOuterTypeEntry"
+                        for (Map.Entry<String, String> e : innerTypeNameToOuterTypeName.entrySet()) {
+                            Container.Entry innerTypeEntry = typeNameToEntry.get(e.getKey());
+
+                            if (innerTypeEntry != null) {
+                                String outerTypeName = e.getValue();
+
+                                for (;;) {
+                                    String typeName = innerTypeNameToOuterTypeName.get(outerTypeName);
+                                    if (typeName != null) {
+                                        outerTypeName = typeName;
+                                    } else {
+                                        break;
+                                    }
+                                }
+
+                                outerTypeEntry = typeNameToEntry.get(outerTypeName);
+
+                                if (outerTypeEntry != null) {
+                                    innerTypeEntryToOuterTypeEntry.put(innerTypeEntry, outerTypeEntry);
+                                }
+                            }
+                        }
+
+                        // Get outer type entry
+                        outerTypeEntry = innerTypeEntryToOuterTypeEntry.get(entry);
+                    }
+                }
+            }
+
+            if (outerTypeEntry != null) {
+                outerEntriesSet.add(outerTypeEntry);
+            } else {
+                outerEntriesSet.add(entry);
+            }
+        }
+
+        // Return outer type entries sorted by path
+        ArrayList<Container.Entry> result = new ArrayList<>(outerEntriesSet);
+
+        result.sort(CONTAINER_ENTRY_COMPARATOR);
+
+        return result;
+    }
+
+    protected void onLocationSelected(Set<DelegatingFilterContainer> delegatingFilterContainers, URI uri, Consumer<Container.Entry> selectedLocationCallback) {
+        // Open the single entry uri
+        Container.Entry entry = null;
+
+        for (DelegatingFilterContainer container : delegatingFilterContainers) {
+            entry = container.getEntry(uri);
+            if (entry != null) {
+                break;
+            }
+        }
+
+        if (entry != null) {
+            selectedLocationCallback.accept(entry);
+        }
+    }
+
+    protected static class ContainerEntryComparator implements Comparator<Container.Entry> {
+        @Override
+        public int compare(Container.Entry e1, Container.Entry e2) {
+            return e1.getPath().compareTo(e2.getPath());
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/model/configuration/Configuration.java b/app/src/main/java/org/jd/gui/model/configuration/Configuration.java
new file mode 100644
index 0000000..e661fb6
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/model/configuration/Configuration.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.model.configuration;
+
+import org.jd.gui.Constants;
+
+import java.awt.*;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Configuration {
+	protected Point mainWindowLocation;
+    protected Dimension mainWindowSize;
+    protected boolean mainWindowMaximize;
+    protected String lookAndFeel;
+
+    protected List<File> recentFiles = new ArrayList<>();
+
+    protected File recentLoadDirectory;
+    protected File recentSaveDirectory;
+
+    protected Map<String, String> preferences = new HashMap<>();
+
+    public Point getMainWindowLocation() {
+        return mainWindowLocation;
+    }
+
+    public Dimension getMainWindowSize() {
+        return mainWindowSize;
+    }
+
+    public boolean isMainWindowMaximize() {
+        return mainWindowMaximize;
+    }
+
+    public String getLookAndFeel() {
+        return lookAndFeel;
+    }
+
+    public List<File> getRecentFiles() {
+        return recentFiles;
+    }
+
+    public File getRecentLoadDirectory() {
+        return recentLoadDirectory;
+    }
+
+    public File getRecentSaveDirectory() {
+        return recentSaveDirectory;
+    }
+
+    public Map<String, String> getPreferences() {
+        return preferences;
+    }
+
+    public void setMainWindowLocation(Point mainWindowLocation) {
+        this.mainWindowLocation = mainWindowLocation;
+    }
+
+    public void setMainWindowSize(Dimension mainWindowSize) {
+        this.mainWindowSize = mainWindowSize;
+    }
+
+    public void setMainWindowMaximize(boolean mainWindowMaximize) {
+        this.mainWindowMaximize = mainWindowMaximize;
+    }
+
+    public void setLookAndFeel(String lookAndFeel) {
+        this.lookAndFeel = lookAndFeel;
+    }
+
+    public void setRecentFiles(List<File> recentFiles) {
+        this.recentFiles = recentFiles;
+    }
+
+    public void setRecentLoadDirectory(File recentLoadDirectory) {
+        this.recentLoadDirectory = recentLoadDirectory;
+    }
+
+    public void setRecentSaveDirectory(File recentSaveDirectory) {
+        this.recentSaveDirectory = recentSaveDirectory;
+    }
+
+    public void setPreferences(Map<String, String> preferences) {
+        this.preferences = preferences;
+    }
+
+    public void addRecentFile(File file) {
+        recentFiles.remove(file);
+        recentFiles.add(0, file);
+        if (recentFiles.size() > Constants.MAX_RECENT_FILES) {
+            recentFiles.remove(Constants.MAX_RECENT_FILES);
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/model/container/DelegatingFilterContainer.java b/app/src/main/java/org/jd/gui/model/container/DelegatingFilterContainer.java
new file mode 100644
index 0000000..aab7d65
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/model/container/DelegatingFilterContainer.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.model.container;
+
+import org.jd.gui.api.model.Container;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.util.*;
+
+public class DelegatingFilterContainer implements Container {
+    protected static final URI DEFAULT_ROOT_URI = URI.create("file:.");
+
+    protected Container container;
+    protected DelegatedEntry root;
+
+    protected Set<URI> validEntries = new HashSet<>();
+    protected Map<URI, DelegatedEntry> uriToDelegatedEntry = new HashMap<>();
+    protected Map<URI, DelegatedContainer> uriToDelegatedContainer = new HashMap<>();
+
+    public DelegatingFilterContainer(Container container, Collection<Entry> entries) {
+        this.container = container;
+        this.root = getDelegatedEntry(container.getRoot());
+
+        for (Entry entry : entries) {
+            while ((entry != null) && !validEntries.contains(entry.getUri())) {
+                validEntries.add(entry.getUri());
+                entry = entry.getParent();
+            }
+        }
+    }
+
+    @Override public String getType() { return container.getType(); }
+    @Override public Container.Entry getRoot() { return root; }
+
+    public Container.Entry getEntry(URI uri) { return uriToDelegatedEntry.get(uri); }
+    public Set<URI> getUris() { return validEntries; }
+
+    protected DelegatedEntry getDelegatedEntry(Container.Entry entry) {
+        URI uri = entry.getUri();
+        DelegatedEntry delegatedEntry = uriToDelegatedEntry.get(uri);
+        if (delegatedEntry == null) {
+            uriToDelegatedEntry.put(uri, delegatedEntry =new DelegatedEntry(entry));
+        }
+        return delegatedEntry;
+    }
+
+    protected DelegatedContainer getDelegatedContainer(Container container) {
+        Entry root = container.getRoot();
+        URI uri = (root == null) ? DEFAULT_ROOT_URI : root.getUri();
+        DelegatedContainer delegatedContainer = uriToDelegatedContainer.get(uri);
+        if (delegatedContainer == null) {
+            uriToDelegatedContainer.put(uri, delegatedContainer =new DelegatedContainer(container));
+        }
+        return delegatedContainer;
+    }
+
+    protected class DelegatedEntry implements Entry, Comparable<DelegatedEntry> {
+        protected Entry entry;
+        protected Collection<Entry> children;
+
+        public DelegatedEntry(Entry entry) {
+            this.entry = entry;
+        }
+
+        @Override public Container getContainer() { return getDelegatedContainer(entry.getContainer()); }
+        @Override public Entry getParent() { return getDelegatedEntry(entry.getParent()); }
+        @Override public URI getUri() { return entry.getUri(); }
+        @Override public String getPath() { return entry.getPath(); }
+        @Override public boolean isDirectory() { return entry.isDirectory(); }
+        @Override public long length() { return entry.length(); }
+        @Override public InputStream getInputStream() { return entry.getInputStream(); }
+
+        @Override
+        public Collection<Entry> getChildren() {
+            if (children == null) {
+                children = new ArrayList<>();
+                for (Entry child : entry.getChildren()) {
+                    if (validEntries.contains(child.getUri())) {
+                        children.add(getDelegatedEntry(child));
+                    }
+                }
+            }
+            return children;
+        }
+
+        @Override
+        public int compareTo(DelegatedEntry other) {
+            if (entry.isDirectory()) {
+                if (!other.isDirectory()) {
+                    return -1;
+                }
+            } else {
+                if (other.isDirectory()) {
+                    return 1;
+                }
+            }
+            return entry.getPath().compareTo(other.getPath());
+        }
+    }
+
+    protected class DelegatedContainer implements Container {
+        protected Container container;
+
+        public DelegatedContainer(Container container) {
+            this.container = container;
+        }
+
+        @Override public String getType() { return container.getType(); }
+        @Override public Entry getRoot() { return getDelegatedEntry(container.getRoot()); }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/jd/gui/model/history/History.java b/app/src/main/java/org/jd/gui/model/history/History.java
new file mode 100644
index 0000000..05c1b43
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/model/history/History.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.model.history;
+
+import java.net.URI;
+import java.util.ArrayList;
+
+public class History {
+    protected URI            current = null;
+    protected ArrayList<URI> backward = new ArrayList<>();
+    protected ArrayList<URI> forward = new ArrayList<>();
+
+    public void add(URI uri) {
+        if (current == null) {
+            // Init history
+            forward.clear();
+            current = uri;
+            return;
+        }
+
+        if (current.equals(uri)) {
+            // Already stored -> Nothing to do
+            return;
+        }
+
+        if (uri.getPath().toString().equals(current.getPath().toString())) {
+            if ((uri.getFragment() == null) && (uri.getQuery() == null)) {
+                // Ignore
+            } else if ((current.getFragment() == null) && (current.getQuery() == null)) {
+                // Replace current URI
+                current = uri;
+            } else {
+                // Store URI
+                forward.clear();
+                backward.add(current);
+                current = uri;
+            }
+            return;
+        }
+
+        if (uri.toString().startsWith(current.toString())) {
+            // Replace current URI
+            current = uri;
+            return;
+        }
+
+        if (current.toString().startsWith(uri.toString())) {
+            // Parent URI -> Nothing to do
+            return;
+        }
+
+        // Store URI
+        forward.clear();
+        backward.add(current);
+        current = uri;
+    }
+
+    public URI backward() {
+        if (! backward.isEmpty()) {
+            forward.add(current);
+            int size = backward.size();
+            current = backward.remove(size-1);
+        }
+        return current;
+    }
+
+    public URI forward() {
+        if (! forward.isEmpty()) {
+            backward.add(current);
+            int size = forward.size();
+            current = forward.remove(size-1);
+        }
+        return current;
+    }
+
+    public boolean canBackward() { return !backward.isEmpty(); }
+    public boolean canForward() { return !forward.isEmpty(); }
+}
diff --git a/app/src/main/java/org/jd/gui/service/actions/ContextualActionsFactoryService.java b/app/src/main/java/org/jd/gui/service/actions/ContextualActionsFactoryService.java
new file mode 100644
index 0000000..c47b667
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/actions/ContextualActionsFactoryService.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.actions;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.ContextualActionsFactory;
+
+import javax.swing.*;
+import java.util.*;
+
+public class ContextualActionsFactoryService {
+    protected static final ContextualActionsFactoryService CONTEXTUAL_ACTIONS_FACTORY_SERVICE = new ContextualActionsFactoryService();
+
+    public static ContextualActionsFactoryService getInstance() { return CONTEXTUAL_ACTIONS_FACTORY_SERVICE; }
+
+    protected static final ActionNameComparator COMPARATOR = new ActionNameComparator();
+
+    protected final Collection<ContextualActionsFactory> providers = ExtensionService.getInstance().load(ContextualActionsFactory.class);
+
+    public Collection<Action> get(API api, Container.Entry entry, String fragment) {
+        HashMap<String, ArrayList<Action>> mapActions = new HashMap<>();
+
+        for (ContextualActionsFactory provider : providers) {
+            Collection<Action> actions = provider.make(api, entry, fragment);
+
+            for (Action action : actions) {
+                String groupName = (String)action.getValue(ContextualActionsFactory.GROUP_NAME);
+                ArrayList<Action> list = mapActions.get(groupName);
+
+                if (list == null) {
+                    mapActions.put(groupName, list=new ArrayList<>());
+                }
+
+                list.add(action);
+            }
+        }
+
+        if (!mapActions.isEmpty()) {
+            ArrayList<Action> result = new ArrayList<>();
+
+            // Sort by group names
+            ArrayList<String> groupNames = new ArrayList<>(mapActions.keySet());
+            Collections.sort(groupNames);
+
+            for (String groupName : groupNames) {
+                if (! result.isEmpty()) {
+                    // Add 'null' to mark a separator
+                    result.add(null);
+                }
+                // Sort by names
+                ArrayList<Action> actions = mapActions.get(groupName);
+                Collections.sort(actions, COMPARATOR);
+                result.addAll(actions);
+            }
+
+            return result;
+        } else {
+            return Collections.emptyList();
+        }
+    }
+
+    protected static class ActionNameComparator implements Comparator<Action> {
+        @Override
+        public int compare(Action a1, Action a2) {
+            String n1 = (String)a1.getValue(Action.NAME);
+            if (n1 == null) {
+                n1 = "";
+            }
+
+            String n2 = (String)a2.getValue(Action.NAME);
+            if (n2 == null) {
+                n2 = "";
+            }
+
+            return n1.compareTo(n2);
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/configuration/ConfigurationPersister.java b/app/src/main/java/org/jd/gui/service/configuration/ConfigurationPersister.java
new file mode 100644
index 0000000..40c1dcd
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/configuration/ConfigurationPersister.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.configuration;
+
+import org.jd.gui.model.configuration.Configuration;
+
+public interface ConfigurationPersister {
+    Configuration load();
+
+    void save(Configuration configuration);
+}
diff --git a/app/src/main/java/org/jd/gui/service/configuration/ConfigurationPersisterService.java b/app/src/main/java/org/jd/gui/service/configuration/ConfigurationPersisterService.java
new file mode 100644
index 0000000..fb4ba71
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/configuration/ConfigurationPersisterService.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.configuration;
+
+public class ConfigurationPersisterService {
+    protected static final ConfigurationPersisterService CONFIGURATION_PERSISTER_SERVICE = new ConfigurationPersisterService();
+
+    protected ConfigurationPersister provider = new ConfigurationXmlPersisterProvider();
+
+    public static ConfigurationPersisterService getInstance() { return CONFIGURATION_PERSISTER_SERVICE; }
+
+    protected ConfigurationPersisterService() {}
+
+    public ConfigurationPersister get() {
+        return provider;
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/configuration/ConfigurationXmlPersisterProvider.java b/app/src/main/java/org/jd/gui/service/configuration/ConfigurationXmlPersisterProvider.java
new file mode 100644
index 0000000..3e9bee2
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/configuration/ConfigurationXmlPersisterProvider.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.configuration;
+
+import org.jd.gui.Constants;
+import org.jd.gui.model.configuration.Configuration;
+import org.jd.gui.service.platform.PlatformService;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.swing.*;
+import javax.xml.stream.*;
+import java.awt.*;
+import java.io.*;
+import java.net.URL;
+import java.util.*;
+import java.util.List;
+import java.util.jar.Manifest;
+
+public class ConfigurationXmlPersisterProvider implements ConfigurationPersister {
+    protected static final String ERROR_BACKGROUND_COLOR = "JdGuiPreferences.errorBackgroundColor";
+    protected static final String JD_CORE_VERSION = "JdGuiPreferences.jdCoreVersion";
+
+    protected static final File FILE = getConfigFile();
+
+    protected static File getConfigFile() {
+        String configFilePath = System.getProperty(Constants.CONFIG_FILENAME);
+
+        if (configFilePath != null) {
+            File configFile = new File(configFilePath);
+            if (configFile.exists()) {
+                return configFile;
+            }
+        }
+
+        if (PlatformService.getInstance().isLinux()) {
+            // See: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
+            String xdgConfigHome = System.getenv("XDG_CONFIG_HOME");
+            if (xdgConfigHome != null) {
+                File xdgConfigHomeFile = new File(xdgConfigHome);
+                if (xdgConfigHomeFile.exists()) {
+                    return new File(xdgConfigHomeFile, Constants.CONFIG_FILENAME);
+                }
+            }
+
+            File userConfigFile = new File(System.getProperty("user.home"), ".config");
+            if (userConfigFile.exists()) {
+                return new File(userConfigFile, Constants.CONFIG_FILENAME);
+            }
+        } else if (PlatformService.getInstance().isWindows()) {
+            // See: http://blogs.msdn.com/b/patricka/archive/2010/03/18/where-should-i-store-my-data-and-configuration-files-if-i-target-multiple-os-versions.aspx
+            String roamingConfigHome = System.getenv("APPDATA");
+            if (roamingConfigHome != null) {
+                File roamingConfigHomeFile = new File(roamingConfigHome);
+                if (roamingConfigHomeFile.exists()) {
+                    return new File(roamingConfigHomeFile, Constants.CONFIG_FILENAME);
+                }
+            }
+        }
+
+        return new File(Constants.CONFIG_FILENAME);
+    }
+
+    @Override
+    public Configuration load() {
+        // Default values
+        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+
+        int w = (screenSize.width>Constants.DEFAULT_WIDTH) ? Constants.DEFAULT_WIDTH : screenSize.width;
+        int h = (screenSize.height>Constants.DEFAULT_HEIGHT) ? Constants.DEFAULT_HEIGHT : screenSize.height;
+        int x = (screenSize.width-w)/2;
+        int y = (screenSize.height-h)/2;
+
+        Configuration config = new Configuration();
+        config.setMainWindowLocation(new Point(x, y));
+        config.setMainWindowSize(new Dimension(w, h));
+        config.setMainWindowMaximize(false);
+
+        String defaultLaf = System.getProperty("swing.defaultlaf");
+
+        config.setLookAndFeel((defaultLaf != null) ? defaultLaf : UIManager.getSystemLookAndFeelClassName());
+
+        File recentSaveDirectory = new File(System.getProperty("user.dir"));
+
+        config.setRecentLoadDirectory(recentSaveDirectory);
+        config.setRecentSaveDirectory(recentSaveDirectory);
+
+        if (FILE.exists()) {
+            try (FileInputStream fis = new FileInputStream(FILE)) {
+                XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(fis);
+
+                // Load values
+                String name = "";
+                Stack<String> names = new Stack<>();
+                List<File> recentFiles = new ArrayList<>();
+                boolean maximize = false;
+                Map<String, String> preferences = config.getPreferences();
+
+                while (reader.hasNext()) {
+                    switch (reader.next()) {
+                        case XMLStreamConstants.START_ELEMENT:
+                            names.push(name);
+                            name += '/' + reader.getLocalName();
+                            switch (name) {
+                                case "/configuration/gui/mainWindow/location":
+                                    x = Integer.parseInt(reader.getAttributeValue(null, "x"));
+                                    y = Integer.parseInt(reader.getAttributeValue(null, "y"));
+                                    break;
+                                case "/configuration/gui/mainWindow/size":
+                                    w = Integer.parseInt(reader.getAttributeValue(null, "w"));
+                                    h = Integer.parseInt(reader.getAttributeValue(null, "h"));
+                                    break;
+                            }
+                            break;
+                        case XMLStreamConstants.END_ELEMENT:
+                            name = names.pop();
+                            break;
+                        case XMLStreamConstants.CHARACTERS:
+                            switch (name) {
+                                case "/configuration/recentFilePaths/filePath":
+                                    File file = new File(reader.getText().trim());
+                                    if (file.exists()) {
+                                        recentFiles.add(file);
+                                    }
+                                    break;
+                                case "/configuration/recentDirectories/loadPath":
+                                    file = new File(reader.getText().trim());
+                                    if (file.exists()) {
+                                        config.setRecentLoadDirectory(file);
+                                    }
+                                    break;
+                                case "/configuration/recentDirectories/savePath":
+                                    file = new File(reader.getText().trim());
+                                    if (file.exists()) {
+                                        config.setRecentSaveDirectory(file);
+                                    }
+                                    break;
+                                case "/configuration/gui/lookAndFeel":
+                                    config.setLookAndFeel(reader.getText().trim());
+                                    break;
+                                case "/configuration/gui/mainWindow/maximize":
+                                    maximize = Boolean.parseBoolean(reader.getText().trim());
+                                    break;
+                                default:
+                                    if (name.startsWith("/configuration/preferences/")) {
+                                        String key = name.substring("/configuration/preferences/".length());
+                                        preferences.put(key, reader.getText().trim());
+                                    }
+                                    break;
+                            }
+                            break;
+                    }
+                }
+
+                if (recentFiles.size() > Constants.MAX_RECENT_FILES) {
+                    // Truncate
+                    recentFiles = recentFiles.subList(0, Constants.MAX_RECENT_FILES);
+                }
+                config.setRecentFiles(recentFiles);
+
+                if ((x >= 0) && (y >= 0) && (x + w < screenSize.width) && (y + h < screenSize.height)) {
+                    // Update preferences
+                    config.setMainWindowLocation(new Point(x, y));
+                    config.setMainWindowSize(new Dimension(w, h));
+                    config.setMainWindowMaximize(maximize);
+                }
+
+                reader.close();
+            } catch (Exception e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+
+        if (! config.getPreferences().containsKey(ERROR_BACKGROUND_COLOR)) {
+            config.getPreferences().put(ERROR_BACKGROUND_COLOR, "0xFF6666");
+        }
+
+        config.getPreferences().put(JD_CORE_VERSION, getJdCoreVersion());
+
+        return config;
+    }
+
+    protected String getJdCoreVersion() {
+        try {
+            Enumeration<URL> enumeration = ConfigurationXmlPersisterProvider.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
+
+            while (enumeration.hasMoreElements()) {
+                try (InputStream is = enumeration.nextElement().openStream()) {
+                    String attribute = new Manifest(is).getMainAttributes().getValue("JD-Core-Version");
+                    if (attribute != null) {
+                        return attribute;
+                    }
+                }
+            }
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        return "SNAPSHOT";
+    }
+
+    @Override
+    public void save(Configuration configuration) {
+        Point l = configuration.getMainWindowLocation();
+        Dimension s = configuration.getMainWindowSize();
+
+        try (FileOutputStream fos = new FileOutputStream(FILE)) {
+            XMLStreamWriter writer = XMLOutputFactory.newInstance().createXMLStreamWriter(fos);
+
+            // Save values
+            writer.writeStartDocument();
+            writer.writeCharacters("\n");
+            writer.writeStartElement("configuration");
+            writer.writeCharacters("\n\t");
+
+            writer.writeStartElement("gui");
+            writer.writeCharacters("\n\t\t");
+                writer.writeStartElement("mainWindow");
+                writer.writeCharacters("\n\t\t\t");
+                    writer.writeStartElement("location");
+                        writer.writeAttribute("x", String.valueOf(l.x));
+                        writer.writeAttribute("y", String.valueOf(l.y));
+                    writer.writeEndElement();
+                    writer.writeCharacters("\n\t\t\t");
+                    writer.writeStartElement("size");
+                        writer.writeAttribute("w", String.valueOf(s.width));
+                        writer.writeAttribute("h", String.valueOf(s.height));
+                    writer.writeEndElement();
+                    writer.writeCharacters("\n\t\t\t");
+                    writer.writeStartElement("maximize");
+                        writer.writeCharacters(String.valueOf(configuration.isMainWindowMaximize()));
+                    writer.writeEndElement();
+                    writer.writeCharacters("\n\t\t");
+                writer.writeEndElement();
+                writer.writeCharacters("\n\t\t");
+                writer.writeStartElement("lookAndFeel");
+                    writer.writeCharacters(configuration.getLookAndFeel());
+                writer.writeEndElement();
+                writer.writeCharacters("\n\t");
+            writer.writeEndElement();
+            writer.writeCharacters("\n\t");
+
+            writer.writeStartElement("recentFilePaths");
+
+            for (File recentFile : configuration.getRecentFiles()) {
+                writer.writeCharacters("\n\t\t");
+                writer.writeStartElement("filePath");
+                    writer.writeCharacters(recentFile.getAbsolutePath());
+                writer.writeEndElement();
+            }
+
+            writer.writeCharacters("\n\t");
+            writer.writeEndElement();
+            writer.writeCharacters("\n\t");
+
+            writer.writeStartElement("recentDirectories");
+            writer.writeCharacters("\n\t\t");
+                writer.writeStartElement("loadPath");
+                    writer.writeCharacters(configuration.getRecentLoadDirectory().getAbsolutePath());
+                writer.writeEndElement();
+                writer.writeCharacters("\n\t\t");
+                writer.writeStartElement("savePath");
+                    writer.writeCharacters(configuration.getRecentSaveDirectory().getAbsolutePath());
+                writer.writeEndElement();
+                writer.writeCharacters("\n\t");
+            writer.writeEndElement();
+            writer.writeCharacters("\n\t");
+
+            writer.writeStartElement("preferences");
+
+            for (Map.Entry<String, String> preference : configuration.getPreferences().entrySet()) {
+                writer.writeCharacters("\n\t\t");
+                writer.writeStartElement(preference.getKey());
+                    writer.writeCharacters(preference.getValue());
+                writer.writeEndElement();
+            }
+
+            writer.writeCharacters("\n\t");
+            writer.writeEndElement();
+            writer.writeCharacters("\n");
+
+            writer.writeEndElement();
+            writer.writeEndDocument();
+            writer.close();
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/container/ContainerFactoryService.java b/app/src/main/java/org/jd/gui/service/container/ContainerFactoryService.java
new file mode 100644
index 0000000..31cae51
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/container/ContainerFactoryService.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.ContainerFactory;
+
+import java.nio.file.Path;
+import java.util.Collection;
+
+public class ContainerFactoryService {
+    protected static final ContainerFactoryService CONTAINER_FACTORY_SERVICE = new ContainerFactoryService();
+
+    public static ContainerFactoryService getInstance() { return CONTAINER_FACTORY_SERVICE; }
+
+    protected final Collection<ContainerFactory> providers = ExtensionService.getInstance().load(ContainerFactory.class);
+
+    public ContainerFactory get(API api, Path rootPath) {
+        for (ContainerFactory containerFactory : providers) {
+            if (containerFactory.accept(api, rootPath)) {
+                return containerFactory;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/extension/ExtensionService.java b/app/src/main/java/org/jd/gui/service/extension/ExtensionService.java
new file mode 100644
index 0000000..a289c47
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/extension/ExtensionService.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.extension;
+
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.*;
+
+public class ExtensionService {
+    protected static final ExtensionService EXTENSION_SERVICE = new ExtensionService();
+    protected static final UrlComparator URL_COMPARATOR = new UrlComparator();
+
+    protected ClassLoader extensionClassLoader;
+
+    public static ExtensionService getInstance() {
+        return EXTENSION_SERVICE;
+    }
+
+    protected ExtensionService() {
+        try {
+            URI jarUri = ExtensionService.class.getProtectionDomain().getCodeSource().getLocation().toURI();
+            File baseDirectory = new File(jarUri).getParentFile();
+            File extDirectory = new File(baseDirectory, "ext");
+
+            if (extDirectory.exists() && extDirectory.isDirectory()) {
+                ArrayList<URL> urls = new ArrayList<>();
+
+                searchJarAndMetaInf(urls, extDirectory);
+
+                if (!urls.isEmpty()) {
+                    URL[] array = urls.toArray(new URL[urls.size()]);
+                    Arrays.sort(array, URL_COMPARATOR);
+                    extensionClassLoader = new URLClassLoader(array, ExtensionService.class.getClassLoader());
+                }
+            }
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        extensionClassLoader = ExtensionService.class.getClassLoader();
+    }
+
+    protected void searchJarAndMetaInf(List<URL> urls, File directory) throws Exception {
+        File metaInf = new File(directory, "META-INF");
+
+        if (metaInf.exists() && metaInf.isDirectory()) {
+            urls.add(directory.toURI().toURL());
+        } else {
+            for (File child : directory.listFiles()) {
+                if (child.isDirectory()) {
+                    searchJarAndMetaInf(urls, child);
+                } else if (child.getName().toLowerCase().endsWith(".jar")) {
+                    urls.add(new URL("jar", "", child.toURI().toURL().toString() + "!/"));
+                }
+            }
+        }
+    }
+
+    public <T> Collection<T> load(Class<T> service) {
+        ArrayList<T> list = new ArrayList<>();
+        Iterator<T> iterator = ServiceLoader.load(service, extensionClassLoader).iterator();
+
+        while (iterator.hasNext()) {
+            list.add(iterator.next());
+        }
+
+        return list;
+    }
+
+    protected static class UrlComparator implements Comparator<URL> {
+        @Override
+        public int compare(URL url1, URL url2) {
+            return url1.getPath().compareTo(url2.getPath());
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/fileloader/FileLoaderService.java b/app/src/main/java/org/jd/gui/service/fileloader/FileLoaderService.java
new file mode 100644
index 0000000..dc2e401
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/fileloader/FileLoaderService.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.FileLoader;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.HashMap;
+
+public class FileLoaderService {
+    protected static final FileLoaderService FILE_LOADER_SERVICE = new FileLoaderService();
+
+    public static FileLoaderService getInstance() { return FILE_LOADER_SERVICE; }
+
+    protected final Collection<FileLoader> providers = ExtensionService.getInstance().load(FileLoader.class);
+
+    protected HashMap<String, FileLoader> mapProviders = new HashMap<>();
+
+    protected FileLoaderService() {
+        for (FileLoader provider : providers) {
+            for (String extension : provider.getExtensions()) {
+                mapProviders.put(extension, provider);
+            }
+        }
+    }
+
+    public FileLoader get(API api, File file) {
+        String name = file.getName();
+        int lastDot = name.lastIndexOf('.');
+        String extension = name.substring(lastDot+1);
+        FileLoader provider = mapProviders.get(extension);
+        return provider;
+    }
+
+    public HashMap<String, FileLoader> getMapProviders() {
+        return mapProviders;
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/indexer/IndexerService.java b/app/src/main/java/org/jd/gui/service/indexer/IndexerService.java
new file mode 100644
index 0000000..30374a0
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/indexer/IndexerService.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.model.Container;
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.Indexer;
+
+import java.util.Collection;
+import java.util.HashMap;
+
+public class IndexerService {
+    protected static final IndexerService INDEXER_SERVICE = new IndexerService();
+
+    public static IndexerService getInstance() { return INDEXER_SERVICE; }
+
+    protected HashMap<String, Indexers> mapProviders = new HashMap<>();
+
+    protected IndexerService() {
+        Collection<Indexer> providers = ExtensionService.getInstance().load(Indexer.class);
+
+        for (Indexer provider : providers) {
+            for (String selector : provider.getSelectors()) {
+                Indexers indexers = mapProviders.get(selector);
+
+                if (indexers == null) {
+                    mapProviders.put(selector, indexers=new Indexers());
+                }
+
+                indexers.add(provider);
+            }
+        }
+    }
+
+    public Indexer get(Container.Entry entry) {
+        Indexer indexer = get(entry.getContainer().getType(), entry);
+        return (indexer != null) ? indexer : get("*", entry);
+    }
+
+    protected Indexer get(String containerType, Container.Entry entry) {
+        String path = entry.getPath();
+        String type = entry.isDirectory() ? "dir" : "file";
+        String prefix = containerType + ':' + type;
+        Indexer indexer = null;
+        Indexers indexers = mapProviders.get(prefix + ':' + path);
+
+        if (indexers != null) {
+            indexer = indexers.match(path);
+        }
+
+        if (indexer == null) {
+            int lastSlashIndex = path.lastIndexOf('/');
+            String name = path.substring(lastSlashIndex+1);
+
+            indexers = mapProviders.get(prefix + ":*/" + name);
+            if (indexers != null) {
+                indexer = indexers.match(path);
+            }
+
+            if (indexer == null) {
+                int index = name.lastIndexOf('.');
+
+                if (index != -1) {
+                    String extension = name.substring(index + 1);
+
+                    indexers = mapProviders.get(prefix + ":*." + extension);
+                    if (indexers != null) {
+                        indexer = indexers.match(path);
+                    }
+                }
+
+                if (indexer == null) {
+                    indexers = mapProviders.get(prefix + ":*");
+                    if (indexers != null) {
+                        indexer = indexers.match(path);
+                    }
+                }
+            }
+        }
+
+        return indexer;
+    }
+
+    protected static class Indexers {
+        protected HashMap<String, Indexer> indexers = new HashMap<>();
+        protected Indexer defaultIndexer;
+
+        public void add(Indexer indexer) {
+            if (indexer.getPathPattern() != null) {
+                indexers.put(indexer.getPathPattern().pattern(), indexer);
+            } else {
+                defaultIndexer = indexer;
+            }
+        }
+
+        public Indexer match(String path) {
+            for (Indexer indexer : indexers.values()) {
+                if (indexer.getPathPattern().matcher(path).matches()) {
+                    return indexer;
+                }
+            }
+            return defaultIndexer;
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/mainpanel/ContainerPanelFactoryProvider.java b/app/src/main/java/org/jd/gui/service/mainpanel/ContainerPanelFactoryProvider.java
new file mode 100644
index 0000000..c47548b
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/mainpanel/ContainerPanelFactoryProvider.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.mainpanel;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContentIndexable;
+import org.jd.gui.api.feature.SourcesSavable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.spi.Indexer;
+import org.jd.gui.spi.PanelFactory;
+import org.jd.gui.spi.SourceSaver;
+import org.jd.gui.spi.TreeNodeFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.view.component.panel.TreeTabbedPanel;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+
+public class ContainerPanelFactoryProvider implements PanelFactory {
+    protected static final String[] TYPES = { "default" };
+
+	@Override public String[] getTypes() { return TYPES; }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends JComponent & UriGettable> T make(API api, Container container) {
+        return (T)new ContainerPanel(api, container);
+	}
+
+    protected class ContainerPanel extends TreeTabbedPanel implements ContentIndexable, SourcesSavable {
+        protected Container.Entry entry;
+
+        public ContainerPanel(API api, Container container) {
+            super(api, container.getRoot().getParent().getUri());
+
+            this.entry = container.getRoot().getParent();
+
+            DefaultMutableTreeNode root = new DefaultMutableTreeNode();
+
+            for (Container.Entry entry : container.getRoot().getChildren()) {
+                TreeNodeFactory factory = api.getTreeNodeFactory(entry);
+                if (factory != null) {
+                    root.add(factory.make(api, entry));
+                }
+            }
+
+            tree.setModel(new DefaultTreeModel(root));
+        }
+
+        // --- ContentIndexable --- //
+        @Override
+        public Indexes index(API api) {
+            HashMap<String, Map<String, Collection>> map = new HashMap<>();
+            DelegatedMapMapWithDefault mapWithDefault = new DelegatedMapMapWithDefault(map);
+
+            // Index populating value automatically
+            Indexes indexesWithDefault = name -> mapWithDefault.get(name);
+
+            // Index entry
+            Indexer indexer = api.getIndexer(entry);
+
+            if (indexer != null) {
+                indexer.index(api, entry, indexesWithDefault);
+            }
+
+            // To prevent memory leaks, return an index without the 'populate' behaviour
+            return name -> map.get(name);
+        }
+
+        // --- SourcesSavable --- //
+        @Override
+        public String getSourceFileName() {
+            SourceSaver saver = api.getSourceSaver(entry);
+
+            if (saver != null) {
+                String path = saver.getSourcePath(entry);
+                int index = path.lastIndexOf('/');
+                return path.substring(index+1);
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public int getFileCount() {
+            SourceSaver saver = api.getSourceSaver(entry);
+            return (saver != null) ? saver.getFileCount(api, entry) : 0;
+        }
+
+        @Override
+        public void save(API api, Controller controller, Listener listener, Path path) {
+            try {
+                Path parentPath = path.getParent();
+
+                if ((parentPath != null) && !Files.exists(parentPath)) {
+                    Files.createDirectories(parentPath);
+                }
+
+                URI uri = path.toUri();
+                URI archiveUri = new URI("jar:" + uri.getScheme(), uri.getHost(), uri.getPath() + "!/", null);
+
+                try (FileSystem archiveFs = FileSystems.newFileSystem(archiveUri, Collections.singletonMap("create", "true"))) {
+                    Path archiveRootPath = archiveFs.getPath("/");
+                    SourceSaver saver = api.getSourceSaver(entry);
+
+                    if (saver != null) {
+                        saver.saveContent(
+                            api,
+                            () -> controller.isCancelled(),
+                            (p) -> listener. pathSaved(p),
+                            archiveRootPath, archiveRootPath, entry);
+                    }
+                }
+            } catch (URISyntaxException|IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+    }
+
+    protected static class DelegatedMap<K, V> implements Map<K, V> {
+        protected Map<K, V> map;
+
+        public DelegatedMap(Map<K, V> map) { this.map = map; }
+
+        @Override public int size() { return map.size(); }
+        @Override public boolean isEmpty() { return map.isEmpty(); }
+        @Override public boolean containsKey(Object o) { return map.containsKey(o); }
+        @Override public boolean containsValue(Object o) { return map.containsValue(o); }
+        @Override public V get(Object o) { return map.get(o); }
+        @Override public V put(K k, V v) { return map.put(k, v); }
+        @Override public V remove(Object o) { return map.remove(o); }
+        @Override public void putAll(Map<? extends K, ? extends V> map) { this.map.putAll(map); }
+        @Override public void clear() { map.clear(); }
+        @Override public Set<K> keySet() { return map.keySet(); }
+        @Override public Collection<V> values() { return map.values(); }
+        @Override public Set<Entry<K, V>> entrySet() { return map.entrySet(); }
+        @Override public boolean equals(Object o) { return map.equals(o); }
+        @Override public int hashCode() { return map.hashCode(); }
+    }
+
+    protected static class DelegatedMapWithDefault extends DelegatedMap<String, Collection> {
+        public DelegatedMapWithDefault(Map<String, Collection> map) { super(map); }
+
+        @Override public Collection get(Object o) {
+            Collection value = map.get(o);
+            if (value == null) {
+                String key = o.toString();
+                map.put(key, value=new ArrayList());
+            }
+            return value;
+        }
+    }
+
+    protected static class DelegatedMapMapWithDefault extends DelegatedMap<String, Map<String, Collection>> {
+	    protected HashMap<String, Map<String, Collection>> wrappers = new HashMap<>();
+
+        public DelegatedMapMapWithDefault(Map<String, Map<String, Collection>> map) { super(map); }
+
+        @Override public Map<String, Collection> get(Object o) {
+            Map<String, Collection> value = wrappers.get(o);
+
+            if (value == null) {
+                String key = o.toString();
+                HashMap<String, Collection> m = new HashMap<>();
+                map.put(key, m);
+                wrappers.put(key, value=new DelegatedMapWithDefault(m));
+            }
+
+            return value;
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/mainpanel/PanelFactoryService.java b/app/src/main/java/org/jd/gui/service/mainpanel/PanelFactoryService.java
new file mode 100644
index 0000000..0108ce1
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/mainpanel/PanelFactoryService.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.mainpanel;
+
+import org.jd.gui.api.model.Container;
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.PanelFactory;
+
+import java.util.Collection;
+import java.util.HashMap;
+
+public class PanelFactoryService {
+    protected static final PanelFactoryService PANEL_FACTORY_SERVICE = new PanelFactoryService();
+
+    public static PanelFactoryService getInstance() { return PANEL_FACTORY_SERVICE; }
+
+    protected HashMap<String, PanelFactory> mapProviders = new HashMap<>();
+
+    protected PanelFactoryService() {
+        Collection<PanelFactory> providers = ExtensionService.getInstance().load(PanelFactory.class);
+
+        for (PanelFactory provider : providers) {
+            for (String type : provider.getTypes()) {
+                mapProviders.put(type, provider);
+            }
+        }
+    }
+
+    public PanelFactory get(Container container) {
+        PanelFactory factory = mapProviders.get(container.getType());
+        return (factory != null) ? factory : mapProviders.get("default");
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/pastehandler/PasteHandlerService.java b/app/src/main/java/org/jd/gui/service/pastehandler/PasteHandlerService.java
new file mode 100644
index 0000000..c68ea29
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/pastehandler/PasteHandlerService.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.pastehandler;
+
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.PasteHandler;
+
+import java.util.Collection;
+
+public class PasteHandlerService {
+    protected static final PasteHandlerService PASTE_HANDLER_SERVICE = new PasteHandlerService();
+
+    public static PasteHandlerService getInstance() { return PASTE_HANDLER_SERVICE; }
+
+    protected final Collection<PasteHandler> providers = ExtensionService.getInstance().load(PasteHandler.class);
+
+    public PasteHandler get(Object obj) {
+        for (PasteHandler provider : providers) {
+            if (provider.accept(obj)) {
+                return provider;
+            }
+        }
+        return null;
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/platform/PlatformService.java b/app/src/main/java/org/jd/gui/service/platform/PlatformService.java
new file mode 100644
index 0000000..51ea1af
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/platform/PlatformService.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.platform;
+
+public class PlatformService {
+	protected static final PlatformService PLATFORM_SERVICE = new PlatformService();
+
+	public enum OS { Linux, MacOSX, Windows }
+
+	protected OS os;
+
+	protected PlatformService() {
+		String osName = System.getProperty("os.name").toLowerCase();
+
+		if (osName.contains("windows")) {
+			os = OS.Windows;
+		} else if (osName.contains("mac os")) {
+			os = OS.MacOSX;
+		} else {
+			os = OS.Linux;
+		}
+	}
+
+	public static PlatformService getInstance() { return PLATFORM_SERVICE; }
+
+	public OS getOs() { return os; }
+
+	public boolean isLinux() { return os == OS.Linux; }
+	public boolean isMac() { return os == OS.MacOSX; }
+	public boolean isWindows() { return os == OS.Windows; }
+}
diff --git a/app/src/main/java/org/jd/gui/service/preferencespanel/PreferencesPanelService.java b/app/src/main/java/org/jd/gui/service/preferencespanel/PreferencesPanelService.java
new file mode 100644
index 0000000..0ee9a6d
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/preferencespanel/PreferencesPanelService.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.preferencespanel;
+
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.PreferencesPanel;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+
+public class PreferencesPanelService {
+    protected static final PreferencesPanelService PREFERENCES_PANEL_SERVICE = new PreferencesPanelService();
+
+    public static PreferencesPanelService getInstance() { return PREFERENCES_PANEL_SERVICE; }
+
+    protected final Collection<PreferencesPanel> providers;
+
+    protected PreferencesPanelService() {
+        Collection<PreferencesPanel> list = ExtensionService.getInstance().load(PreferencesPanel.class);
+        Iterator<PreferencesPanel> iterator = list.iterator();
+
+        while (iterator.hasNext()) {
+            if (!iterator.next().isActivated()) {
+                iterator.remove();
+            }
+        }
+
+        HashMap<String, PreferencesPanel> map = new HashMap<>();
+
+        for (PreferencesPanel panel : list) {
+            map.put(panel.getPreferencesGroupTitle() + '$' + panel.getPreferencesPanelTitle(), panel);
+        }
+
+        providers = map.values();
+    }
+
+    public Collection<PreferencesPanel> getProviders() {
+        return providers;
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/preferencespanel/UISingleInstancePreferencesProvider.java b/app/src/main/java/org/jd/gui/service/preferencespanel/UISingleInstancePreferencesProvider.java
new file mode 100644
index 0000000..8d195d4
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/preferencespanel/UISingleInstancePreferencesProvider.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.preferencespanel;
+
+import org.jd.gui.service.platform.PlatformService;
+import org.jd.gui.spi.PreferencesPanel;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Map;
+
+/**
+ * Single instance is the default mode on Mac OSX, so this panel is not activated.
+ */
+public class UISingleInstancePreferencesProvider extends JPanel implements PreferencesPanel {
+    protected static final String SINGLE_INSTANCE = "UIMainWindowPreferencesProvider.singleInstance";
+
+    protected JCheckBox singleInstanceTabsCheckBox;
+
+    public UISingleInstancePreferencesProvider() {
+        super(new GridLayout(0,1));
+
+        singleInstanceTabsCheckBox = new JCheckBox("Single instance");
+
+        add(singleInstanceTabsCheckBox);
+    }
+
+    // --- PreferencesPanel --- //
+    @Override public String getPreferencesGroupTitle() { return "User Interface"; }
+    @Override public String getPreferencesPanelTitle() { return "Main window"; }
+    @Override public JComponent getPanel() { return this; }
+
+    @Override public void init(Color errorBackgroundColor) {}
+
+    @Override public boolean isActivated() { return !PlatformService.getInstance().isMac(); }
+
+    @Override
+    public void loadPreferences(Map<String, String> preferences) {
+        singleInstanceTabsCheckBox.setSelected("true".equals(preferences.get(SINGLE_INSTANCE)));
+    }
+
+    @Override
+    public void savePreferences(Map<String, String> preferences) {
+        preferences.put(SINGLE_INSTANCE, Boolean.toString(singleInstanceTabsCheckBox.isSelected()));
+    }
+
+    @Override public boolean arePreferencesValid() { return true; }
+
+    @Override public void addPreferencesChangeListener(PreferencesPanel.PreferencesPanelChangeListener listener) {}
+}
diff --git a/app/src/main/java/org/jd/gui/service/preferencespanel/UITabsPreferencesProvider.java b/app/src/main/java/org/jd/gui/service/preferencespanel/UITabsPreferencesProvider.java
new file mode 100644
index 0000000..3e5022e
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/preferencespanel/UITabsPreferencesProvider.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.preferencespanel;
+
+import org.jd.gui.service.platform.PlatformService;
+import org.jd.gui.spi.PreferencesPanel;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Map;
+
+/**
+ * JTabbedPane.WRAP_TAB_LAYOUT is not supported by Aqua L&F.
+ * This panel is not activated on Mac OSX.
+ */
+public class UITabsPreferencesProvider extends JPanel implements PreferencesPanel {
+    protected static final String TAB_LAYOUT = "UITabsPreferencesProvider.singleLineTabs";
+
+    protected JCheckBox singleLineTabsCheckBox;
+
+    public UITabsPreferencesProvider() {
+        super(new GridLayout(0,1));
+
+        singleLineTabsCheckBox = new JCheckBox("Tabs on a single line");
+
+        add(singleLineTabsCheckBox);
+    }
+
+    // --- PreferencesPanel --- //
+    @Override public String getPreferencesGroupTitle() { return "User Interface"; }
+    @Override public String getPreferencesPanelTitle() { return "Tabs"; }
+    @Override public JComponent getPanel() { return this; }
+
+    @Override public void init(Color errorBackgroundColor) {}
+
+    @Override public boolean isActivated() { return !PlatformService.getInstance().isMac(); }
+
+    @Override public void loadPreferences(Map<String, String> preferences) {
+        singleLineTabsCheckBox.setSelected("true".equals(preferences.get(TAB_LAYOUT)));
+    }
+
+    @Override public void savePreferences(Map<String, String> preferences) {
+        preferences.put(TAB_LAYOUT, Boolean.toString(singleLineTabsCheckBox.isSelected()));
+    }
+
+    @Override public boolean arePreferencesValid() { return true; }
+
+    @Override public void addPreferencesChangeListener(PreferencesPanel.PreferencesPanelChangeListener listener) {}
+}
diff --git a/app/src/main/java/org/jd/gui/service/sourceloader/SourceLoaderService.java b/app/src/main/java/org/jd/gui/service/sourceloader/SourceLoaderService.java
new file mode 100644
index 0000000..4141d49
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/sourceloader/SourceLoaderService.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.sourceloader;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.SourceLoader;
+
+import java.io.File;
+import java.util.Collection;
+
+public class SourceLoaderService {
+    protected static final SourceLoaderService SOURCE_LOADER_SERVICE = new SourceLoaderService();
+
+    public static SourceLoaderService getInstance() { return SOURCE_LOADER_SERVICE; }
+
+    protected Collection<SourceLoader> providers = ExtensionService.getInstance().load(SourceLoader.class);
+
+    public String getSource(API api, Container.Entry entry) {
+        for (SourceLoader provider : providers) {
+            String source = provider.getSource(api, entry);
+
+            if ((source != null) && !source.isEmpty()) {
+                return source;
+            }
+        }
+
+        return null;
+    }
+
+    public String loadSource(API api, Container.Entry entry) {
+        for (SourceLoader provider : providers) {
+            String source = provider.loadSource(api, entry);
+
+            if ((source != null) && !source.isEmpty()) {
+                return source;
+            }
+        }
+
+        return null;
+    }
+
+    public File getSourceFile(API api, Container.Entry entry) {
+        for (SourceLoader provider : providers) {
+            File file = provider.loadSourceFile(api, entry);
+
+            if (file != null) {
+                return file;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/sourcesaver/SourceSaverService.java b/app/src/main/java/org/jd/gui/service/sourcesaver/SourceSaverService.java
new file mode 100644
index 0000000..4e56a20
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/sourcesaver/SourceSaverService.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.sourcesaver;
+
+import org.jd.gui.api.model.Container;
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.SourceSaver;
+
+import java.util.Collection;
+import java.util.HashMap;
+
+public class SourceSaverService {
+    protected static final SourceSaverService SOURCE_SAVER_SERVICE = new SourceSaverService();
+
+    public static SourceSaverService getInstance() { return SOURCE_SAVER_SERVICE; }
+
+    protected HashMap<String, SourceSavers> mapProviders = new HashMap<>();
+
+    protected SourceSaverService() {
+        Collection<SourceSaver> providers = ExtensionService.getInstance().load(SourceSaver.class);
+
+        for (SourceSaver provider : providers) {
+            for (String selector : provider.getSelectors()) {
+                SourceSavers savers = mapProviders.get(selector);
+
+                if (savers == null) {
+                    mapProviders.put(selector, savers=new SourceSavers());
+                }
+
+                savers.add(provider);
+            }
+        }
+    }
+
+    public SourceSaver get(Container.Entry entry) {
+        SourceSaver saver = get(entry.getContainer().getType(), entry);
+        return (saver != null) ? saver : get("*", entry);
+    }
+
+    protected SourceSaver get(String containerType, Container.Entry entry) {
+        String path = entry.getPath();
+        String type = entry.isDirectory() ? "dir" : "file";
+        String prefix = containerType + ':' + type;
+        SourceSaver saver = null;
+        SourceSavers savers = mapProviders.get(prefix + ':' + path);
+
+        if (savers != null) {
+            saver = savers.match(path);
+        }
+
+        if (saver == null) {
+            int lastSlashIndex = path.lastIndexOf('/');
+            String name = path.substring(lastSlashIndex+1);
+
+            savers = mapProviders.get(prefix + ":*/" + path);
+            if (savers != null) {
+                saver = savers.match(path);
+            }
+
+            if (saver == null) {
+                int index = name.lastIndexOf('.');
+
+                if (index != -1) {
+                    String extension = name.substring(index + 1);
+
+                    savers = mapProviders.get(prefix + ":*." + extension);
+                    if (savers != null) {
+                        saver = savers.match(path);
+                    }
+                }
+
+                if (saver == null) {
+                    savers = mapProviders.get(prefix + ":*");
+                    if (savers != null) {
+                        saver = savers.match(path);
+                    }
+                }
+            }
+        }
+
+        return saver;
+    }
+
+    protected static class SourceSavers {
+        protected HashMap<String, SourceSaver> savers = new HashMap<>();
+        protected SourceSaver defaultSaver;
+
+        void add(SourceSaver saver) {
+            if (saver.getPathPattern() != null) {
+                savers.put(saver.getPathPattern().pattern(), saver);
+            } else {
+                defaultSaver = saver;
+            }
+        }
+
+        SourceSaver match(String path) {
+            for (SourceSaver saver : savers.values()) {
+                if (saver.getPathPattern().matcher(path).matches()) {
+                    return saver;
+                }
+            }
+            return defaultSaver;
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/treenode/TreeNodeFactoryService.java b/app/src/main/java/org/jd/gui/service/treenode/TreeNodeFactoryService.java
new file mode 100644
index 0000000..e7fe27c
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/treenode/TreeNodeFactoryService.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.model.Container;
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.TreeNodeFactory;
+
+import java.util.Collection;
+import java.util.HashMap;
+
+public class TreeNodeFactoryService {
+    protected static final TreeNodeFactoryService TREE_NODE_FACTORY_SERVICE = new TreeNodeFactoryService();
+
+    public static TreeNodeFactoryService getInstance() { return TREE_NODE_FACTORY_SERVICE; }
+
+    protected HashMap<String, TreeNodeFactories> mapProviders = new HashMap<>();
+
+    protected TreeNodeFactoryService() {
+        Collection<TreeNodeFactory> providers = ExtensionService.getInstance().load(TreeNodeFactory.class);
+
+        for (TreeNodeFactory provider : providers) {
+            for (String selector : provider.getSelectors()) {
+                TreeNodeFactories factories = mapProviders.get(selector);
+
+                if (factories == null) {
+                    mapProviders.put(selector, factories=new TreeNodeFactories());
+                }
+
+                factories.add(provider);
+            }
+        }
+    }
+
+    public TreeNodeFactory get(Container.Entry entry) {
+        TreeNodeFactory factory = get(entry.getContainer().getType(), entry);
+        return (factory != null) ? factory : get("*", entry);
+    }
+
+    protected TreeNodeFactory get(String containerType, Container.Entry entry) {
+        String path = entry.getPath();
+        String type = entry.isDirectory() ? "dir" : "file";
+        String prefix = containerType + ':' + type + ':';
+        TreeNodeFactory factory = null;
+        TreeNodeFactories factories = mapProviders.get(prefix + path);
+
+        if (factories != null) {
+            factory = factories.match(path);
+        }
+
+        if (factory == null) {
+            int lastSlashIndex = path.lastIndexOf('/');
+            String name = path.substring(lastSlashIndex+1);
+
+            factories = mapProviders.get(prefix + "*/" + name);
+            if (factories != null) {
+                factory = factories.match(path);
+            }
+
+            if (factory == null) {
+                int index = name.lastIndexOf('.');
+
+                if (index != -1) {
+                    String extension = name.substring(index + 1);
+
+                    factories = mapProviders.get(prefix + "*." + extension);
+                    if (factories != null) {
+                        factory = factories.match(path);
+                    }
+                }
+
+                if (factory == null) {
+                    factories = mapProviders.get(prefix + "*");
+                    if (factories != null) {
+                        factory = factories.match(path);
+                    }
+                }
+            }
+        }
+
+        return factory;
+    }
+
+    protected static class TreeNodeFactories {
+        protected HashMap<String, TreeNodeFactory> factories = new HashMap<>();
+        protected TreeNodeFactory defaultFactory;
+
+        public void add(TreeNodeFactory factory) {
+            if (factory.getPathPattern() != null) {
+                factories.put(factory.getPathPattern().pattern(), factory);
+            } else {
+                defaultFactory = factory;
+            }
+        }
+
+        public TreeNodeFactory match(String path) {
+            for (TreeNodeFactory factory : factories.values()) {
+                if (factory.getPathPattern().matcher(path).matches()) {
+                    return factory;
+                }
+            }
+            return defaultFactory;
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/type/TypeFactoryService.java b/app/src/main/java/org/jd/gui/service/type/TypeFactoryService.java
new file mode 100644
index 0000000..c0215ef
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/type/TypeFactoryService.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.type;
+
+import org.jd.gui.api.model.Container;
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.TypeFactory;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class TypeFactoryService {
+    protected static final TypeFactoryService TYPE_FACTORY_SERVICE = new TypeFactoryService();
+
+    protected Map<String, TypeFactories> mapProviders;
+
+    public static TypeFactoryService getInstance() {
+        return TYPE_FACTORY_SERVICE;
+    }
+
+    protected TypeFactoryService() {
+        Collection<TypeFactory> providers = ExtensionService.getInstance().load(TypeFactory.class);
+
+        mapProviders = new HashMap<>();
+
+        for (TypeFactory provider : providers) {
+            for (String selector : provider.getSelectors()) {
+                TypeFactories typeFactories = mapProviders.get(selector);
+
+                if (typeFactories == null) {
+                    mapProviders.put(selector, typeFactories=new TypeFactories());
+                }
+
+                typeFactories.add(provider);
+            }
+        }
+    }
+
+    public TypeFactory get(Container.Entry entry) {
+        TypeFactory typeFactory = get(entry.getContainer().getType(), entry);
+        return (typeFactory != null) ? typeFactory : get("*", entry);
+    }
+
+    public TypeFactory get(String containerType, Container.Entry entry) {
+        String path = entry.getPath();
+        String type = entry.isDirectory() ? "dir" : "file";
+        String prefix = containerType + ':' + type + ':';
+        TypeFactories typeFactories = mapProviders.get(prefix + path);
+        TypeFactory factory = null;
+
+        if (typeFactories != null) {
+            factory = typeFactories.match(path);
+        }
+
+        if (factory == null) {
+            int lastSlashIndex = path.lastIndexOf('/');
+            String name = path.substring(lastSlashIndex+1);
+
+            typeFactories = mapProviders.get(prefix + "*/" + name);
+
+            if (typeFactories != null) {
+                factory = typeFactories.match(path);
+            }
+
+            if (factory == null) {
+                int index = name.lastIndexOf('.');
+                if (index != -1) {
+                    String extension = name.substring(index + 1);
+
+                    typeFactories = mapProviders.get(prefix + "*." + extension);
+
+                    if (typeFactories != null) {
+                        factory = typeFactories.match(path);
+                    }
+                }
+                if (factory == null) {
+                    typeFactories = mapProviders.get(prefix + '*');
+
+                    if (typeFactories != null) {
+                        factory = typeFactories.match(path);
+                    }
+                }
+            }
+        }
+
+        return factory;
+    }
+
+    protected static class TypeFactories {
+        protected HashMap<String, TypeFactory> factories = new HashMap<>();
+        protected TypeFactory defaultFactory;
+
+        public void add(TypeFactory factory) {
+            Pattern pathPattern = factory.getPathPattern();
+
+            if (pathPattern != null) {
+                factories.put(pathPattern.pattern(), factory);
+            } else {
+                defaultFactory = factory;
+            }
+        }
+
+        public TypeFactory match(String path) {
+            for (TypeFactory factory : factories.values()) {
+                Matcher matcher = factory.getPathPattern().matcher(path);
+
+                if (matcher.matches()) {
+                    return factory;
+                }
+            }
+            return defaultFactory;
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/service/uriloader/UriLoaderService.java b/app/src/main/java/org/jd/gui/service/uriloader/UriLoaderService.java
new file mode 100644
index 0000000..115536d
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/service/uriloader/UriLoaderService.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.uriloader;
+
+import org.jd.gui.api.API;
+import org.jd.gui.service.extension.ExtensionService;
+import org.jd.gui.spi.UriLoader;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.HashMap;
+
+public class UriLoaderService {
+    protected static final UriLoaderService URI_LOADER_SERVICE = new UriLoaderService();
+
+    public static UriLoaderService getInstance() { return URI_LOADER_SERVICE; }
+
+    protected HashMap<String, UriLoader> mapProviders = new HashMap<>();
+
+    protected UriLoaderService() {
+        Collection<UriLoader> providers = ExtensionService.getInstance().load(UriLoader.class);
+
+        for (UriLoader provider : providers) {
+            for (String scheme : provider.getSchemes()) {
+                mapProviders.put(scheme, provider);
+            }
+        }
+    }
+
+    public UriLoader get(API api, URI uri) {
+        UriLoader provider = mapProviders.get(uri.getScheme());
+
+        if (provider.accept(api, uri)) {
+            return provider;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/util/exception/ExceptionUtil.java b/app/src/main/java/org/jd/gui/util/exception/ExceptionUtil.java
new file mode 100644
index 0000000..d931686
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/util/exception/ExceptionUtil.java
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.exception;
+
+public class ExceptionUtil {
+    public static boolean printStackTrace(Throwable throwable) {
+        throwable.printStackTrace();
+        return true;
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/util/function/TriConsumer.java b/app/src/main/java/org/jd/gui/util/function/TriConsumer.java
new file mode 100644
index 0000000..1212757
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/util/function/TriConsumer.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.function;
+
+import java.util.Objects;
+
+@FunctionalInterface
+public interface TriConsumer<T, U, V> {
+    void accept(T t, U u, V v);
+
+    default TriConsumer<T, U, V> andThen(TriConsumer<? super T, ? super U, ? super V> after) {
+        Objects.requireNonNull(after);
+
+        return (a, b, c) -> {
+            accept(a, b, c);
+            after.accept(a, b, c);
+        };
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/util/net/InterProcessCommunicationUtil.java b/app/src/main/java/org/jd/gui/util/net/InterProcessCommunicationUtil.java
new file mode 100644
index 0000000..1ef55b9
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/util/net/InterProcessCommunicationUtil.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.net;
+
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.function.Consumer;
+
+public class InterProcessCommunicationUtil {
+    protected static final int PORT = 2015_6;
+
+    public static void listen(final Consumer<String[]> consumer) throws Exception {
+        final ServerSocket listener = new ServerSocket(PORT);
+
+        Runnable runnable = new Runnable() {
+            @Override
+            public void run() {
+                while (true) {
+                    try (Socket socket = listener.accept();
+                         ObjectInputStream ois = new ObjectInputStream(socket.getInputStream())) {
+                        // Receive args from another JD-GUI instance
+                        String[] args = (String[])ois.readObject();
+                        consumer.accept(args);
+                    } catch (IOException|ClassNotFoundException e) {
+                        assert ExceptionUtil.printStackTrace(e);
+                    }
+                }
+            }
+        };
+
+        new Thread(runnable).start();
+    }
+
+    public static void send(String[] args) {
+        try (Socket socket = new Socket(InetAddress.getLocalHost(), PORT);
+             ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream())) {
+            // Send args to the main JD-GUI instance
+            oos.writeObject(args);
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/util/net/UriUtil.java b/app/src/main/java/org/jd/gui/util/net/UriUtil.java
new file mode 100644
index 0000000..7fd57ec
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/util/net/UriUtil.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.net;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.api.model.Type;
+import org.jd.gui.service.type.TypeFactoryService;
+import org.jd.gui.spi.TypeFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.concurrent.Future;
+
+public class UriUtil {
+    /*
+     * Convert inner entry URI to outer entry uri with a fragment. Example:
+     *  file://codebase/a/b/c/D$E.class => file://codebase/a/b/c/D.class#typeDeclaration=D$E
+     */
+    public static URI createURI(API api, Collection<Future<Indexes>> collectionOfFutureIndexes, Container.Entry entry, String query, String fragment) {
+        URI uri = entry.getUri();
+
+        try {
+            String path = uri.getPath();
+            TypeFactory typeFactory = TypeFactoryService.getInstance().get(entry);
+
+            if (typeFactory != null) {
+                Type type = typeFactory.make(api, entry, fragment);
+
+                if (type != null) {
+                    path = getOuterPath(collectionOfFutureIndexes, entry, type);
+                }
+            }
+
+            return new URI(uri.getScheme(), uri.getHost(), path, query, fragment);
+        } catch (URISyntaxException e) {
+            assert ExceptionUtil.printStackTrace(e);
+            return uri;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected static String getOuterPath(Collection<Future<Indexes>> collectionOfFutureIndexes, Container.Entry entry, Type type) {
+        String outerName = type.getOuterName();
+
+        if (outerName != null) {
+            try {
+                for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
+                    if (futureIndexes.isDone()) {
+                        Collection<Container.Entry> outerEntries = futureIndexes.get().getIndex("typeDeclarations").get(outerName);
+
+                        if (outerEntries != null) {
+                            for (Container.Entry outerEntry : outerEntries) {
+                                if (outerEntry.getContainer() == entry.getContainer()) {
+                                    return outerEntry.getUri().getPath();
+                                }
+                            }
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+
+        return entry.getUri().getPath();
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/util/swing/SwingUtil.java b/app/src/main/java/org/jd/gui/util/swing/SwingUtil.java
new file mode 100644
index 0000000..24615b5
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/util/swing/SwingUtil.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.swing;
+
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+/**
+ * See: https://www.ailis.de/~k/archives/67-Workaround-for-borderless-Java-Swing-menus-on-Linux.html
+ */
+public class SwingUtil {
+    /*
+     * This is free and unencumbered software released into the public domain.
+     *
+     * Anyone is free to copy, modify, publish, use, compile, sell, or
+     * distribute this software, either in source code form or as a compiled
+     * binary, for any purpose, commercial or non-commercial, and by any
+     * means.
+     *
+     * In jurisdictions that recognize copyright laws, the author or authors
+     * of this software dedicate any and all copyright interest in the
+     * software to the public domain. We make this dedication for the benefit
+     * of the public at large and to the detriment of our heirs and
+     * successors. We intend this dedication to be an overt act of
+     * relinquishment in perpetuity of all present and future rights to this
+     * software under copyright law.
+     *
+     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+     * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+     * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+     * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+     * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+     * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+     * OTHER DEALINGS IN THE SOFTWARE.
+     *
+     * For more information, please refer to <http://unlicense.org/>
+     */
+
+    /**
+     * Swing menus are looking pretty bad on Linux when the GTK LaF is used (See
+     * bug #6925412). It will most likely never be fixed anytime soon so this
+     * method provides a workaround for it. It uses reflection to change the GTK
+     * style objects of Swing so popup menu borders have a minimum thickness of
+     * 1 and menu separators have a minimum vertical thickness of 1.
+     */
+    public static void installGtkPopupBugWorkaround() {
+        // Get current look-and-feel implementation class
+        LookAndFeel laf = UIManager.getLookAndFeel();
+        Class<?> lafClass = laf.getClass();
+
+        // Do nothing when not using the problematic LaF
+        if (!lafClass.getName().equals("com.sun.java.swing.plaf.gtk.GTKLookAndFeel")) return;
+
+        // We do reflection from here on. Failure is silently ignored. The
+        // workaround is simply not installed when something goes wrong here
+        try {
+            // Access the GTK style factory
+            Field field = lafClass.getDeclaredField("styleFactory");
+            boolean accessible = field.isAccessible();
+            field.setAccessible(true);
+            Object styleFactory = field.get(laf);
+            field.setAccessible(accessible);
+
+            // Fix the horizontal and vertical thickness of popup menu style
+            Object style = getGtkStyle(styleFactory, new JPopupMenu(), "POPUP_MENU");
+            fixGtkThickness(style, "yThickness");
+            fixGtkThickness(style, "xThickness");
+
+            // Fix the vertical thickness of the popup menu separator style
+            style = getGtkStyle(styleFactory, new JSeparator(), "POPUP_MENU_SEPARATOR");
+            fixGtkThickness(style, "yThickness");
+        } catch (Exception e) {
+            // Silently ignored. Workaround can't be applied.
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    /**
+     * Called internally by installGtkPopupBugWorkaround to fix the thickness
+     * of a GTK style field by setting it to a minimum value of 1.
+     *
+     * @param style
+     *            The GTK style object.
+     * @param fieldName
+     *            The field name.
+     * @throws Exception
+     *             When reflection fails.
+     */
+    private static void fixGtkThickness(Object style, String fieldName) throws Exception {
+        Field field = style.getClass().getDeclaredField(fieldName);
+        boolean accessible = field.isAccessible();
+        field.setAccessible(true);
+        field.setInt(style, Math.max(1, field.getInt(style)));
+        field.setAccessible(accessible);
+    }
+
+    /**
+     * Called internally by installGtkPopupBugWorkaround. Returns a specific
+     * GTK style object.
+     *
+     * @param styleFactory
+     *            The GTK style factory.
+     * @param component
+     *            The target component of the style.
+     * @param regionName
+     *            The name of the target region of the style.
+     * @return The GTK style.
+     * @throws Exception
+     *             When reflection fails.
+     */
+    private static Object getGtkStyle(Object styleFactory, JComponent component, String regionName) throws Exception {
+        // Create the region object
+        Class<?> regionClass = Class.forName("javax.swing.plaf.synth.Region");
+        Field field = regionClass.getField(regionName);
+        Object region = field.get(regionClass);
+
+        // Get and return the style
+        Class<?> styleFactoryClass = styleFactory.getClass();
+        Method method = styleFactoryClass.getMethod("getStyle", JComponent.class, regionClass);
+        boolean accessible = method.isAccessible();
+        method.setAccessible(true);
+        Object style = method.invoke(styleFactory, component, region);
+        method.setAccessible(accessible);
+        return style;
+    }
+
+    public static void invokeLater(Runnable runnable) {
+        if (SwingUtilities.isEventDispatchThread()) {
+            runnable.run();
+        } else {
+            SwingUtilities.invokeLater(runnable);
+        }
+    }
+
+    public static Image getImage(String iconPath) {
+        return Toolkit.getDefaultToolkit().getImage(SwingUtil.class.getResource(iconPath));
+    }
+
+    public static ImageIcon newImageIcon(String iconPath) {
+        return new ImageIcon(getImage(iconPath));
+    }
+
+    public static Action newAction(String name, boolean enable, ActionListener listener) {
+        Action action = new AbstractAction(name) {
+            @Override
+            public void actionPerformed(ActionEvent actionEvent) {
+                listener.actionPerformed(actionEvent);
+            }
+        };
+        action.setEnabled(enable);
+        return action;
+    }
+
+    public static Action newAction(String name, ImageIcon icon, boolean enable, ActionListener listener) {
+        Action action = newAction(name, enable, listener);
+        action.putValue(Action.SMALL_ICON, icon);
+        return action;
+    }
+
+    public static Action newAction(ImageIcon icon, boolean enable, ActionListener listener) {
+        Action action = newAction(null, icon, enable, listener);
+        action.putValue(Action.SMALL_ICON, icon);
+        return action;
+    }
+
+    public static Action newAction(String name, ImageIcon icon, boolean enable, String shortDescription, ActionListener listener) {
+        Action action = newAction(name, icon, enable, listener);
+        action.putValue(Action.SHORT_DESCRIPTION, shortDescription);
+        return action;
+    }
+
+    public static Action newAction(String name, boolean enable, String shortDescription, ActionListener listener) {
+        Action action = newAction(name, enable, listener);
+        action.putValue(Action.SHORT_DESCRIPTION, shortDescription);
+        return action;
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/AboutView.java b/app/src/main/java/org/jd/gui/view/AboutView.java
new file mode 100644
index 0000000..f7cfa84
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/AboutView.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view;
+
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.swing.SwingUtil;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+public class AboutView {
+    protected JDialog aboutDialog;
+    protected JButton aboutOkButton;
+
+    public AboutView(JFrame mainFrame) {
+        // Build GUI
+        SwingUtil.invokeLater(() -> {
+            aboutDialog = new JDialog(mainFrame, "About Java Decompiler", false);
+            aboutDialog.setResizable(false);
+
+            JPanel panel = new JPanel();
+            panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+            panel.setLayout(new BorderLayout());
+            aboutDialog.add(panel);
+
+            Box vbox = Box.createVerticalBox();
+            panel.add(vbox, BorderLayout.NORTH);
+            JPanel subpanel = new JPanel();
+            vbox.add(subpanel);
+            subpanel.setBorder(BorderFactory.createLineBorder(Color.BLACK));
+            subpanel.setBackground(Color.WHITE);
+            subpanel.setLayout(new BorderLayout());
+            JLabel logo = new JLabel(new ImageIcon(SwingUtil.getImage("/org/jd/gui/images/jd_icon_64.png")));
+            logo.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+            subpanel.add(logo, BorderLayout.WEST);
+            Box subvbox = Box.createVerticalBox();
+            subvbox.setBorder(BorderFactory.createEmptyBorder(15,0,15,15));
+            subpanel.add(subvbox, BorderLayout.EAST);
+            Box hbox = Box.createHorizontalBox();
+            subvbox.add(hbox);
+            JLabel mainLabel = new JLabel("Java Decompiler");
+            mainLabel.setFont(UIManager.getFont("Label.font").deriveFont(Font.BOLD, 14));
+            hbox.add(mainLabel);
+            hbox.add(Box.createHorizontalGlue());
+            hbox = Box.createHorizontalBox();
+            subvbox.add(hbox);
+            JPanel subsubpanel = new JPanel();
+            hbox.add(subsubpanel);
+            subsubpanel.setLayout(new GridLayout(2,2));
+            subsubpanel.setOpaque(false);
+            subsubpanel.setBorder(BorderFactory.createEmptyBorder(5,10,5,5));
+
+            String jdGuiVersion = "SNAPSHOT";
+            String jdCoreVersion = "SNAPSHOT";
+
+            try {
+                Enumeration<URL> enumeration = AboutView.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
+
+                while (enumeration.hasMoreElements()) {
+                    try (InputStream is = enumeration.nextElement().openStream()) {
+                        Attributes attributes = new Manifest(is).getMainAttributes();
+                        String attribute = attributes.getValue("JD-GUI-Version");
+
+                        if (attribute != null) {
+                            jdGuiVersion = attribute;
+                        }
+
+                        attribute = attributes.getValue("JD-Core-Version");
+
+                        if (attribute != null) {
+                            jdCoreVersion = attribute;
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+
+            subsubpanel.add(new JLabel("JD-GUI"));
+            subsubpanel.add(new JLabel("version " + jdGuiVersion));
+            subsubpanel.add(new JLabel("JD-Core"));
+            subsubpanel.add(new JLabel("version " + jdCoreVersion));
+
+            hbox.add(Box.createHorizontalGlue());
+
+            hbox = Box.createHorizontalBox();
+            hbox.add(new JLabel("Copyright © 2008, 2019 Emmanuel Dupuy"));
+            hbox.add(Box.createHorizontalGlue());
+            subvbox.add(hbox);
+
+            vbox.add(Box.createVerticalStrut(10));
+
+            hbox = Box.createHorizontalBox();
+            panel.add(hbox, BorderLayout.SOUTH);
+            hbox.add(Box.createHorizontalGlue());
+            aboutOkButton = new JButton("    Ok    ");
+            Action aboutOkActionListener = new AbstractAction() {
+                @Override public void actionPerformed(ActionEvent actionEvent) { aboutDialog.setVisible(false); }
+            };
+            aboutOkButton.addActionListener(aboutOkActionListener);
+            hbox.add(aboutOkButton);
+            hbox.add(Box.createHorizontalGlue());
+
+            // Last setup
+            JRootPane rootPane = aboutDialog.getRootPane();
+            rootPane.setDefaultButton(aboutOkButton);
+            rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "AboutView.ok");
+            rootPane.getActionMap().put("AboutView.ok", aboutOkActionListener);
+
+            // Prepare to display
+            aboutDialog.pack();
+        });
+    }
+
+    public void show() {
+        SwingUtil.invokeLater(() -> {
+            // Show
+            aboutDialog.setLocationRelativeTo(aboutDialog.getParent());
+            aboutDialog.setVisible(true);
+            aboutOkButton.requestFocus();
+        });
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/GoToView.java b/app/src/main/java/org/jd/gui/view/GoToView.java
new file mode 100644
index 0000000..c6c48b5
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/GoToView.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view;
+
+import org.jd.gui.api.feature.LineNumberNavigable;
+import org.jd.gui.model.configuration.Configuration;
+import org.jd.gui.util.swing.SwingUtil;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.util.function.IntConsumer;
+
+public class GoToView {
+    protected JDialog goToDialog;
+    protected JLabel goToEnterLineNumberLabel;
+    protected JTextField goToEnterLineNumberTextField;
+    protected JLabel goToEnterLineNumberErrorLabel;
+
+    protected LineNumberNavigable navigator;
+    protected IntConsumer okCallback;
+
+    public GoToView(Configuration configuration, JFrame mainFrame) {
+        // Build GUI
+        SwingUtil.invokeLater(() -> {
+            goToDialog = new JDialog(mainFrame, "Go to Line", false);
+            goToDialog.setResizable(false);
+
+            Box vbox = Box.createVerticalBox();
+            vbox.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+            goToDialog.add(vbox);
+
+            // First label "Enter line number (1..xxx):"
+            Box hbox = Box.createHorizontalBox();
+            hbox.add(goToEnterLineNumberLabel = new JLabel());
+            hbox.add(Box.createHorizontalGlue());
+            vbox.add(hbox);
+
+            vbox.add(Box.createVerticalStrut(10));
+
+            // Text field
+            vbox.add(goToEnterLineNumberTextField = new JTextField(30));
+
+            vbox.add(Box.createVerticalStrut(10));
+
+            // Error label
+            hbox = Box.createHorizontalBox();
+            hbox.add(goToEnterLineNumberErrorLabel = new JLabel(" "));
+            goToEnterLineNumberTextField.addKeyListener(new KeyAdapter() {
+                @Override public void keyTyped(KeyEvent e) {
+                    if (! Character.isDigit(e.getKeyChar())) {
+                        e.consume();
+                    }
+                }
+            });
+            hbox.add(Box.createHorizontalGlue());
+            vbox.add(hbox);
+
+            vbox.add(Box.createVerticalStrut(15));
+
+            // Buttons "Ok" and "Cancel"
+            hbox = Box.createHorizontalBox();
+            hbox.add(Box.createHorizontalGlue());
+            JButton goToOkButton = new JButton("   Ok   ");
+            hbox.add(goToOkButton);
+            goToOkButton.setEnabled(false);
+            goToOkButton.addActionListener(e -> {
+                okCallback.accept(Integer.valueOf(goToEnterLineNumberTextField.getText()));
+                goToDialog.setVisible(false);
+            });
+            hbox.add(Box.createHorizontalStrut(5));
+            JButton goToCancelButton = new JButton("Cancel");
+            hbox.add(goToCancelButton);
+            Action goToCancelActionListener = new AbstractAction() {
+                public void actionPerformed(ActionEvent actionEvent) { goToDialog.setVisible(false); }
+            };
+            goToCancelButton.addActionListener(goToCancelActionListener);
+            vbox.add(hbox);
+
+            vbox.add(Box.createVerticalStrut(13));
+
+            // Last setup
+            JRootPane rootPane = goToDialog.getRootPane();
+            rootPane.setDefaultButton(goToOkButton);
+            rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "OpenTypeView.cancel");
+            rootPane.getActionMap().put("OpenTypeView.cancel", goToCancelActionListener);
+
+            // Add main listener
+            goToEnterLineNumberTextField.getDocument().addDocumentListener(new DocumentListener() {
+                protected Color backgroundColor = UIManager.getColor("TextField.background");
+                protected Color errorBackgroundColor = Color.decode(configuration.getPreferences().get("JdGuiPreferences.errorBackgroundColor"));
+
+                @Override public void insertUpdate(DocumentEvent e) { onTextChange(); }
+                @Override public void removeUpdate(DocumentEvent e) { onTextChange(); }
+                @Override public void changedUpdate(DocumentEvent e) { onTextChange(); }
+
+                protected void onTextChange() {
+                    String text = goToEnterLineNumberTextField.getText();
+
+                    if (text.length() == 0) {
+                        goToOkButton.setEnabled(false);
+                        clearErrorMessage();
+                    } else {
+                        try {
+                            int lineNumber = Integer.valueOf(text);
+
+                            if (lineNumber > navigator.getMaximumLineNumber()) {
+                                goToOkButton.setEnabled(false);
+                                showErrorMessage("Line number out of range");
+                            } else if (navigator.checkLineNumber(lineNumber)) {
+                                goToOkButton.setEnabled(true);
+                                clearErrorMessage();
+                            } else {
+                                goToOkButton.setEnabled(false);
+                                showErrorMessage("Line number not found");
+                            }
+                        } catch (NumberFormatException e) {
+                            goToOkButton.setEnabled(false);
+                            showErrorMessage("Not a number");
+                        }
+                    }
+                }
+
+                protected void showErrorMessage(String message) {
+                    goToEnterLineNumberErrorLabel.setText(message);
+                    goToEnterLineNumberTextField.setBackground(errorBackgroundColor);
+                }
+
+                protected void clearErrorMessage() {
+                    goToEnterLineNumberErrorLabel.setText(" ");
+                    goToEnterLineNumberTextField.setBackground(backgroundColor);
+                }
+            });
+
+            // Prepare to display
+            goToDialog.pack();
+            goToDialog.setLocationRelativeTo(mainFrame);
+        });
+    }
+
+    public void show(LineNumberNavigable navigator, IntConsumer okCallback) {
+        this.navigator = navigator;
+        this.okCallback = okCallback;
+
+        SwingUtil.invokeLater(() -> {
+            // Init
+            goToEnterLineNumberLabel.setText("Enter line number (1.." + navigator.getMaximumLineNumber() + "):");
+            goToEnterLineNumberTextField.setText("");
+            // Show
+            goToDialog.setVisible(true);
+            goToEnterLineNumberTextField.requestFocus();
+        });
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/MainView.java b/app/src/main/java/org/jd/gui/view/MainView.java
new file mode 100644
index 0000000..b439819
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/MainView.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view;
+
+import org.jd.gui.Constants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.*;
+import org.jd.gui.model.configuration.Configuration;
+import org.jd.gui.model.history.History;
+import org.jd.gui.service.platform.PlatformService;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.view.component.IconButton;
+import org.jd.gui.view.component.panel.MainTabbedPanel;
+
+import javax.swing.*;
+import javax.swing.border.Border;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import java.awt.*;
+import java.awt.event.ActionListener;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.io.File;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import static org.jd.gui.util.swing.SwingUtil.*;
+
+@SuppressWarnings("unchecked")
+public class MainView<T extends JComponent & UriGettable> implements UriOpenable, PreferencesChangeListener {
+    protected History history;
+    protected Consumer<File> openFilesCallback;
+    protected JFrame mainFrame;
+    protected JMenu recentFiles = new JMenu("Recent Files");
+    protected Action closeAction;
+    protected Action openTypeAction;
+    protected Action backwardAction;
+    protected Action forwardAction;
+    protected MainTabbedPanel mainTabbedPanel;
+    protected Box findPanel;
+    protected JComboBox findComboBox;
+    protected JCheckBox findCaseSensitive;
+    protected Color findBackgroundColor;
+    protected Color findErrorBackgroundColor;
+
+    public MainView(
+            Configuration configuration, API api, History history,
+            ActionListener openActionListener,
+            ActionListener closeActionListener,
+            ActionListener saveActionListener,
+            ActionListener saveAllSourcesActionListener,
+            ActionListener exitActionListener,
+            ActionListener copyActionListener,
+            ActionListener pasteActionListener,
+            ActionListener selectAllActionListener,
+            ActionListener findActionListener,
+            ActionListener findPreviousActionListener,
+            ActionListener findNextActionListener,
+            ActionListener findCaseSensitiveActionListener,
+            Runnable findCriteriaChangedCallback,
+            ActionListener openTypeActionListener,
+            ActionListener openTypeHierarchyActionListener,
+            ActionListener goToActionListener,
+            ActionListener backwardActionListener,
+            ActionListener forwardActionListener,
+            ActionListener searchActionListener,
+            ActionListener jdWebSiteActionListener,
+            ActionListener jdGuiIssuesActionListener,
+            ActionListener jdCoreIssuesActionListener,
+            ActionListener preferencesActionListener,
+            ActionListener aboutActionListener,
+            Runnable panelClosedCallback,
+            Consumer<T> currentPageChangedCallback,
+            Consumer<File> openFilesCallback) {
+        this.history = history;
+        this.openFilesCallback = openFilesCallback;
+        // Build GUI
+        invokeLater(() -> {
+            mainFrame = new JFrame("Java Decompiler");
+            mainFrame.setIconImages(Arrays.asList(getImage("/org/jd/gui/images/jd_icon_32.png"), getImage("/org/jd/gui/images/jd_icon_64.png"), getImage("/org/jd/gui/images/jd_icon_128.png")));
+            mainFrame.setMinimumSize(new Dimension(Constants.MINIMAL_WIDTH, Constants.MINIMAL_HEIGHT));
+            mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+
+            // Find panel //
+            Action findNextAction = newAction("Next", newImageIcon("/org/jd/gui/images/next_nav.png"), true, findNextActionListener);
+            findPanel = Box.createHorizontalBox();
+            findPanel.setVisible(false);
+            findPanel.add(new JLabel("Find: "));
+            findComboBox = new JComboBox();
+            findComboBox.setEditable(true);
+            JComponent editorComponent = (JComponent)findComboBox.getEditor().getEditorComponent();
+            editorComponent.addKeyListener(new KeyAdapter() {
+                protected String lastStr = "";
+
+                @Override
+                public void keyReleased(KeyEvent e) {
+                    switch (e.getKeyCode()) {
+                        case KeyEvent.VK_ESCAPE:
+                            findPanel.setVisible(false);
+                            break;
+                        case KeyEvent.VK_ENTER:
+                            String str = getFindText();
+                            if (str.length() > 1) {
+                                int index = ((DefaultComboBoxModel)findComboBox.getModel()).getIndexOf(str);
+                                if(index != -1 ) {
+                                    findComboBox.removeItemAt(index);
+                                }
+                                findComboBox.insertItemAt(str, 0);
+                                findComboBox.setSelectedIndex(0);
+                                findNextAction.actionPerformed(null);
+                            }
+                            break;
+                        default:
+                            str = getFindText();
+                            if (! lastStr.equals(str)) {
+                                findCriteriaChangedCallback.run();
+                                lastStr = str;
+                            }
+                    }
+                }
+            });
+            editorComponent.setOpaque(true);
+            findComboBox.setBackground(this.findBackgroundColor = editorComponent.getBackground());
+            this.findErrorBackgroundColor = Color.decode(configuration.getPreferences().get("JdGuiPreferences.errorBackgroundColor"));
+
+            findPanel.add(findComboBox);
+            findPanel.add(Box.createHorizontalStrut(5));
+            JToolBar toolBar = new JToolBar();
+            toolBar.setFloatable(false);
+            toolBar.setRollover(true);
+
+            IconButton findNextButton = new IconButton("Next", newAction(newImageIcon("/org/jd/gui/images/next_nav.png"), true, findNextActionListener));
+            toolBar.add(findNextButton);
+
+            toolBar.add(Box.createHorizontalStrut(5));
+
+            IconButton findPreviousButton = new IconButton("Previous", newAction(newImageIcon("/org/jd/gui/images/prev_nav.png"), true, findPreviousActionListener));
+            toolBar.add(findPreviousButton);
+
+            findPanel.add(toolBar);
+            findCaseSensitive = new JCheckBox();
+            findCaseSensitive.setAction(newAction("Case sensitive", true, findCaseSensitiveActionListener));
+            findPanel.add(findCaseSensitive);
+            findPanel.add(Box.createHorizontalGlue());
+
+            IconButton findCloseButton = new IconButton(newAction(null, null, true, e -> findPanel.setVisible(false)));
+            findCloseButton.setContentAreaFilled(false);
+            findCloseButton.setIcon(newImageIcon("/org/jd/gui/images/close.gif"));
+            findCloseButton.setRolloverIcon(newImageIcon("/org/jd/gui/images/close_active.gif"));
+            findPanel.add(findCloseButton);
+
+            if (PlatformService.getInstance().isMac()) {
+                findPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
+                Border border = BorderFactory.createEmptyBorder();
+                findNextButton.setBorder(border);
+                findPreviousButton.setBorder(border);
+                findCloseButton.setBorder(border);
+            } else {
+                findPanel.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 2));
+            }
+
+            // Actions //
+            boolean browser = Desktop.isDesktopSupported() ? Desktop.getDesktop().isSupported(Desktop.Action.BROWSE) : false;
+            Action openAction = newAction("Open File...", newImageIcon("/org/jd/gui/images/open.png"), true, "Open a file", openActionListener);
+            closeAction = newAction("Close", false, closeActionListener);
+            Action saveAction = newAction("Save", newImageIcon("/org/jd/gui/images/save.png"), false, saveActionListener);
+            Action saveAllSourcesAction = newAction("Save All Sources", newImageIcon("/org/jd/gui/images/save_all.png"), false, saveAllSourcesActionListener);
+            Action exitAction = newAction("Exit", true, "Quit this program", exitActionListener);
+            Action copyAction = newAction("Copy", newImageIcon("/org/jd/gui/images/copy.png"), false, copyActionListener);
+            Action pasteAction = newAction("Paste Log", newImageIcon("/org/jd/gui/images/paste.png"), true, pasteActionListener);
+            Action selectAllAction = newAction("Select all", false, selectAllActionListener);
+            Action findAction = newAction("Find...", false, findActionListener);
+            openTypeAction = newAction("Open Type...", newImageIcon("/org/jd/gui/images/open_type.png"), false, openTypeActionListener);
+            Action openTypeHierarchyAction = newAction("Open Type Hierarchy...", false, openTypeHierarchyActionListener);
+            Action goToAction = newAction("Go to Line...", false, goToActionListener);
+            backwardAction = newAction("Back", newImageIcon("/org/jd/gui/images/backward_nav.png"), false, backwardActionListener);
+            forwardAction = newAction("Forward", newImageIcon("/org/jd/gui/images/forward_nav.png"), false, forwardActionListener);
+            Action searchAction = newAction("Search...", newImageIcon("/org/jd/gui/images/search_src.png"), false, searchActionListener);
+            Action jdWebSiteAction = newAction("JD Web site", browser, "Open JD Web site", jdWebSiteActionListener);
+            Action jdGuiIssuesActionAction = newAction("JD-GUI issues", browser, "Open JD-GUI issues page", jdGuiIssuesActionListener);
+            Action jdCoreIssuesActionAction = newAction("JD-Core issues", browser, "Open JD-Core issues page", jdCoreIssuesActionListener);
+            Action preferencesAction = newAction("Preferences...", newImageIcon("/org/jd/gui/images/preferences.png"), true, "Open the preferences panel", preferencesActionListener);
+            Action aboutAction = newAction("About...", true, "About JD-GUI", aboutActionListener);
+
+            // Menu //
+            int menuShortcutKeyMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
+            JMenuBar menuBar = new JMenuBar();
+            JMenu menu = new JMenu("File");
+            menuBar.add(menu);
+            menu.add(openAction).setAccelerator(KeyStroke.getKeyStroke('O', menuShortcutKeyMask));
+            menu.addSeparator();
+            menu.add(closeAction).setAccelerator(KeyStroke.getKeyStroke('W', menuShortcutKeyMask));
+            menu.addSeparator();
+            menu.add(saveAction).setAccelerator(KeyStroke.getKeyStroke('S', menuShortcutKeyMask));
+            menu.add(saveAllSourcesAction).setAccelerator(KeyStroke.getKeyStroke('S', menuShortcutKeyMask|InputEvent.ALT_MASK));
+            menu.addSeparator();
+            menu.add(recentFiles);
+            if (!PlatformService.getInstance().isMac()) {
+                menu.addSeparator();
+                menu.add(exitAction).setAccelerator(KeyStroke.getKeyStroke('X', InputEvent.ALT_MASK));
+            }
+            menu = new JMenu("Edit");
+            menuBar.add(menu);
+            menu.add(copyAction).setAccelerator(KeyStroke.getKeyStroke('C', menuShortcutKeyMask));
+            menu.add(pasteAction).setAccelerator(KeyStroke.getKeyStroke('V', menuShortcutKeyMask));
+            menu.addSeparator();
+            menu.add(selectAllAction).setAccelerator(KeyStroke.getKeyStroke('A', menuShortcutKeyMask));
+            menu.addSeparator();
+            menu.add(findAction).setAccelerator(KeyStroke.getKeyStroke('F', menuShortcutKeyMask));
+            menu = new JMenu("Navigation");
+            menuBar.add(menu);
+            menu.add(openTypeAction).setAccelerator(KeyStroke.getKeyStroke('T', menuShortcutKeyMask));
+            menu.add(openTypeHierarchyAction).setAccelerator(KeyStroke.getKeyStroke('H', menuShortcutKeyMask));
+            menu.addSeparator();
+            menu.add(goToAction).setAccelerator(KeyStroke.getKeyStroke('L', menuShortcutKeyMask));
+            menu.addSeparator();
+            menu.add(backwardAction).setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.ALT_MASK));
+            menu.add(forwardAction).setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.ALT_MASK));
+            menu = new JMenu("Search");
+            menuBar.add(menu);
+            menu.add(searchAction).setAccelerator(KeyStroke.getKeyStroke('S', menuShortcutKeyMask|InputEvent.SHIFT_MASK));
+            menu = new JMenu("Help");
+            menuBar.add(menu);
+            if (browser) {
+                menu.add(jdWebSiteAction);
+                menu.add(jdGuiIssuesActionAction);
+                menu.add(jdCoreIssuesActionAction);
+                menu.addSeparator();
+            }
+            menu.add(preferencesAction).setAccelerator(KeyStroke.getKeyStroke('P', menuShortcutKeyMask|InputEvent.SHIFT_MASK));
+            if (!PlatformService.getInstance().isMac()) {
+                menu.addSeparator();
+                menu.add(aboutAction).setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0));
+            }
+            mainFrame.setJMenuBar(menuBar);
+
+            // Icon bar //
+            JPanel panel = new JPanel();
+            panel.setLayout(new BorderLayout());
+            toolBar = new JToolBar();
+            toolBar.setFloatable(false);
+            toolBar.setRollover(true);
+            toolBar.add(new IconButton(openAction));
+            toolBar.addSeparator();
+            toolBar.add(new IconButton(openTypeAction));
+            toolBar.add(new IconButton(searchAction));
+            toolBar.addSeparator();
+            toolBar.add(new IconButton(backwardAction));
+            toolBar.add(new IconButton(forwardAction));
+            panel.add(toolBar, BorderLayout.PAGE_START);
+
+            mainTabbedPanel = new MainTabbedPanel(api);
+            mainTabbedPanel.getPageChangedListeners().add(new PageChangeListener() {
+                protected JComponent currentPage = null;
+
+                @Override public <U extends JComponent & UriGettable> void pageChanged(U page) {
+                    if (currentPage != page) {
+                        // Update current page
+                        currentPage = page;
+                        currentPageChangedCallback.accept((T)page);
+
+                        invokeLater(() -> {
+                            if (page == null) {
+                                // Update title
+                                mainFrame.setTitle("Java Decompiler");
+                                // Update menu
+                                saveAction.setEnabled(false);
+                                copyAction.setEnabled(false);
+                                selectAllAction.setEnabled(false);
+                                openTypeHierarchyAction.setEnabled(false);
+                                goToAction.setEnabled(false);
+                                // Update find panel
+                                findPanel.setVisible(false);
+                            } else {
+                                // Update title
+                                String path = page.getUri().getPath();
+                                int index = path.lastIndexOf('/');
+                                String name = (index == -1) ? path : path.substring(index + 1);
+                                mainFrame.setTitle((name != null) ? name + " - Java Decompiler" : "Java Decompiler");
+                                // Update history
+                                history.add(page.getUri());
+                                // Update history actions
+                                updateHistoryActions();
+                                // Update menu
+                                saveAction.setEnabled(page instanceof ContentSavable);
+                                copyAction.setEnabled(page instanceof ContentCopyable);
+                                selectAllAction.setEnabled(page instanceof ContentSelectable);
+                                findAction.setEnabled(page instanceof ContentSearchable);
+                                openTypeHierarchyAction.setEnabled(page instanceof FocusedTypeGettable);
+                                goToAction.setEnabled(page instanceof LineNumberNavigable);
+                                // Update find panel
+                                if (findPanel.isVisible()) {
+                                    findPanel.setVisible(page instanceof ContentSearchable);
+                                }
+                            }
+                        });
+                    }
+                }
+            });
+            mainTabbedPanel.getTabbedPane().addChangeListener(new ChangeListener() {
+                protected int lastTabCount = 0;
+
+                @Override
+                public void stateChanged(ChangeEvent e) {
+                    int tabCount = mainTabbedPanel.getTabbedPane().getTabCount();
+                    boolean enabled = (tabCount > 0);
+
+                    closeAction.setEnabled(enabled);
+                    openTypeAction.setEnabled(enabled);
+                    searchAction.setEnabled(enabled);
+                    saveAllSourcesAction.setEnabled((mainTabbedPanel.getTabbedPane().getSelectedComponent() instanceof SourcesSavable));
+
+                    if (tabCount < lastTabCount) {
+                        panelClosedCallback.run();
+                    }
+
+                    lastTabCount = tabCount;
+                }
+            });
+            mainTabbedPanel.preferencesChanged(configuration.getPreferences());
+            panel.add(mainTabbedPanel, BorderLayout.CENTER);
+
+            panel.add(findPanel, BorderLayout.PAGE_END);
+            mainFrame.add(panel);
+        });
+    }
+
+    public void show(Point location, Dimension size, boolean maximize) {
+        invokeLater(() -> {
+            // Set position, resize and show
+            mainFrame.setLocation(location);
+            mainFrame.setSize(size);
+            mainFrame.setExtendedState(maximize ? JFrame.MAXIMIZED_BOTH : 0);
+            mainFrame.setVisible(true);
+        });
+    }
+
+    public JFrame getMainFrame() {
+        return mainFrame;
+    }
+
+    public void showFindPanel() {
+        invokeLater(() -> {
+            findPanel.setVisible(true);
+            findComboBox.requestFocus();
+        });
+    }
+
+    public void setFindBackgroundColor(boolean wasFound) {
+        invokeLater(() -> {
+            findComboBox.getEditor().getEditorComponent().setBackground(wasFound ? findBackgroundColor : findErrorBackgroundColor);
+        });
+    }
+
+    public <T extends JComponent & UriGettable> void addMainPanel(String title, Icon icon, String tip, T component) {
+        invokeLater(() -> {
+            mainTabbedPanel.addPage(title, icon, tip, component);
+        });
+    }
+
+    public <T extends JComponent & UriGettable> List<T> getMainPanels() {
+        return mainTabbedPanel.getPages();
+    }
+
+    public <T extends JComponent & UriGettable> T getSelectedMainPanel() {
+        return (T)mainTabbedPanel.getTabbedPane().getSelectedComponent();
+    }
+
+    public void closeCurrentTab() {
+        invokeLater(() -> {
+            Component component = mainTabbedPanel.getTabbedPane().getSelectedComponent();
+            if (component instanceof PageClosable) {
+                if (!((PageClosable)component).closePage()) {
+                    mainTabbedPanel.removeComponent(component);
+                }
+            } else {
+                mainTabbedPanel.removeComponent(component);
+            }
+        });
+    }
+
+    public void updateRecentFilesMenu(List<File> files) {
+        invokeLater(() -> {
+            recentFiles.removeAll();
+
+            for (File file : files) {
+                JMenuItem menuItem = new JMenuItem(reduceRecentFilePath(file.getAbsolutePath()));
+                menuItem.addActionListener(e -> openFilesCallback.accept(file));
+                recentFiles.add(menuItem);
+            }
+        });
+    }
+
+    public String getFindText() {
+        Document doc = ((JTextField)findComboBox.getEditor().getEditorComponent()).getDocument();
+
+        try {
+            return doc.getText(0, doc.getLength());
+        } catch (BadLocationException e) {
+            assert ExceptionUtil.printStackTrace(e);
+            return "";
+        }
+    }
+
+    public boolean getFindCaseSensitive() { return findCaseSensitive.isSelected(); }
+
+    public void updateHistoryActions() {
+        invokeLater(() -> {
+            backwardAction.setEnabled(history.canBackward());
+            forwardAction.setEnabled(history.canForward());
+        });
+    }
+
+    // --- Utils --- //
+    static String reduceRecentFilePath(String path) {
+        int lastSeparatorPosition = path.lastIndexOf(File.separatorChar);
+
+        if ((lastSeparatorPosition == -1) || (lastSeparatorPosition < Constants.RECENT_FILE_MAX_LENGTH)) {
+            return path;
+        }
+
+        int length = Constants.RECENT_FILE_MAX_LENGTH/2 - 2;
+        String left = path.substring(0, length);
+        String right = path.substring(path.length() - length);
+
+        return left + "..." + right;
+    }
+
+    // --- URIOpener --- //
+    @Override
+    public boolean openUri(URI uri) {
+        boolean success = mainTabbedPanel.openUri(uri);
+
+        if (success) {
+            closeAction.setEnabled(true);
+            openTypeAction.setEnabled(true);
+        }
+
+        return success;
+    }
+
+    // --- PreferencesChangeListener --- //
+    @Override
+    public void preferencesChanged(Map<String, String> preferences) {
+        mainTabbedPanel.preferencesChanged(preferences);
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/OpenTypeHierarchyView.java b/app/src/main/java/org/jd/gui/view/OpenTypeHierarchyView.java
new file mode 100644
index 0000000..a223ad9
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/OpenTypeHierarchyView.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.api.model.TreeNodeData;
+import org.jd.gui.api.model.Type;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.function.TriConsumer;
+import org.jd.gui.util.swing.SwingUtil;
+import org.jd.gui.view.component.Tree;
+import org.jd.gui.view.renderer.TreeNodeRenderer;
+
+import javax.swing.*;
+import javax.swing.event.TreeExpansionEvent;
+import javax.swing.event.TreeExpansionListener;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreePath;
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.Future;
+
+public class OpenTypeHierarchyView {
+    protected static final ImageIcon ROOT_CLASS_ICON = new ImageIcon(OpenTypeHierarchyView.class.getClassLoader().getResource("org/jd/gui/images/generate_class.png"));
+    protected static final ImageIcon ROOT_INTERFACE_ICON = new ImageIcon(OpenTypeHierarchyView.class.getClassLoader().getResource("org/jd/gui/images/generate_int.png"));
+
+    protected static final TreeNodeComparator TREE_NODE_COMPARATOR = new TreeNodeComparator();
+
+    protected API api;
+    protected Collection<Future<Indexes>> collectionOfFutureIndexes;
+
+    protected JDialog openTypeHierarchyDialog;
+    protected Tree openTypeHierarchyTree;
+
+    protected TriConsumer<Point, Collection<Container.Entry>, String> selectedTypeCallback;
+
+    public OpenTypeHierarchyView(API api, JFrame mainFrame, TriConsumer<Point, Collection<Container.Entry>, String> selectedTypeCallback) {
+        this.api = api;
+        this.selectedTypeCallback = selectedTypeCallback;
+        // Build GUI
+        SwingUtil.invokeLater(() -> {
+            openTypeHierarchyDialog = new JDialog(mainFrame, "Hierarchy Type", false);
+
+            JPanel panel = new JPanel();
+            panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+            panel.setLayout(new BorderLayout());
+            openTypeHierarchyDialog.add(panel);
+
+            openTypeHierarchyTree = new Tree();
+            openTypeHierarchyTree.setModel(new DefaultTreeModel(new DefaultMutableTreeNode()));
+            openTypeHierarchyTree.setCellRenderer(new TreeNodeRenderer());
+            openTypeHierarchyTree.addMouseListener(new MouseAdapter() {
+                @Override public void mouseClicked(MouseEvent e) {
+                    if (e.getClickCount() == 2) {
+                        onTypeSelected();
+                    }
+                }
+            });
+            openTypeHierarchyTree.addTreeExpansionListener(new TreeExpansionListener() {
+                @Override public void treeExpanded(TreeExpansionEvent e) {
+                    TreeNode node = (TreeNode)e.getPath().getLastPathComponent();
+                    // Expand node and find the first leaf
+                    while (node.getChildCount() > 0) {
+                        if (((DefaultMutableTreeNode)node.getChildAt(0)).getUserObject() == null) {
+                            // Remove dummy node and create children
+                            populateTreeNode(node, null);
+                        }
+                        if (node.getChildCount() != 1) {
+                            break;
+                        }
+                        node = ((TreeNode)node.getChildAt(0));
+                    }
+                    DefaultTreeModel model = (DefaultTreeModel)openTypeHierarchyTree.getModel();
+                    model.reload((TreeNode)e.getPath().getLastPathComponent());
+                    openTypeHierarchyTree.setSelectionPath(new TreePath(node.getPath()));
+                }
+                @Override public void treeCollapsed(TreeExpansionEvent e) {}
+            });
+            openTypeHierarchyTree.addKeyListener(new KeyAdapter() {
+                @Override public void keyPressed(KeyEvent e) {
+                    if (e.getKeyCode() == KeyEvent.VK_F4) {
+                        TreeNode node = (TreeNode)openTypeHierarchyTree.getLastSelectedPathComponent();
+                        if (node != null) {
+                            updateTree(node.entry, node.typeName);
+                        }
+                    }
+                }
+            });
+
+            JScrollPane openTypeHierarchyScrollPane = new JScrollPane(openTypeHierarchyTree);
+            openTypeHierarchyScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+            openTypeHierarchyScrollPane.setPreferredSize(new Dimension(400, 150));
+            panel.add(openTypeHierarchyScrollPane, BorderLayout.CENTER);
+
+            // Buttons "Open" and "Cancel"
+            Box vbox = Box.createVerticalBox();
+            panel.add(vbox, BorderLayout.SOUTH);
+            vbox.add(Box.createVerticalStrut(25));
+            Box hbox = Box.createHorizontalBox();
+            vbox.add(hbox);
+            hbox.add(Box.createHorizontalGlue());
+            JButton openTypeHierarchyOpenButton = new JButton("Open");
+            hbox.add(openTypeHierarchyOpenButton);
+            openTypeHierarchyOpenButton.setEnabled(false);
+            openTypeHierarchyOpenButton.addActionListener(e -> onTypeSelected());
+            hbox.add(Box.createHorizontalStrut(5));
+            JButton openTypeHierarchyCancelButton = new JButton("Cancel");
+            hbox.add(openTypeHierarchyCancelButton);
+            Action openTypeHierarchyCancelActionListener = new AbstractAction() {
+                @Override public void actionPerformed(ActionEvent actionEvent) { openTypeHierarchyDialog.setVisible(false); }
+            };
+            openTypeHierarchyCancelButton.addActionListener(openTypeHierarchyCancelActionListener);
+
+            openTypeHierarchyTree.addTreeSelectionListener(e -> {
+                Object o = openTypeHierarchyTree.getLastSelectedPathComponent();
+                if (o != null) {
+                    o = ((TreeNode)o).entry;
+                }
+                openTypeHierarchyOpenButton.setEnabled(o != null);
+            });
+
+            // Last setup
+            JRootPane rootPane = openTypeHierarchyDialog.getRootPane();
+            rootPane.setDefaultButton(openTypeHierarchyOpenButton);
+            rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "OpenTypeHierarchyView.cancel");
+            rootPane.getActionMap().put("OpenTypeHierarchyView.cancel", openTypeHierarchyCancelActionListener);
+
+            openTypeHierarchyDialog.setMinimumSize(openTypeHierarchyDialog.getSize());
+
+            // Prepare to display
+            openTypeHierarchyDialog.pack();
+            openTypeHierarchyDialog.setLocationRelativeTo(mainFrame);
+        });
+    }
+
+    public void show(Collection<Future<Indexes>> collectionOfFutureIndexes, Container.Entry entry, String typeName) {
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        SwingUtil.invokeLater(() -> {
+            updateTree(entry, typeName);
+            openTypeHierarchyDialog.setVisible(true);
+            openTypeHierarchyTree.requestFocus();
+        });
+    }
+
+    public boolean isVisible() { return openTypeHierarchyDialog.isVisible(); }
+
+    public void showWaitCursor() {
+        SwingUtil.invokeLater(() -> openTypeHierarchyDialog.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)));
+    }
+
+    public void hideWaitCursor() {
+        SwingUtil.invokeLater(() -> openTypeHierarchyDialog.setCursor(Cursor.getDefaultCursor()));
+    }
+
+    public void updateTree(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        TreeNode selectedTreeNode = (TreeNode)openTypeHierarchyTree.getLastSelectedPathComponent();
+
+        if (selectedTreeNode != null) {
+            updateTree(selectedTreeNode.entry, selectedTreeNode.typeName);
+        }
+    }
+
+    protected void updateTree(Container.Entry entry, String typeName) {
+        SwingUtil.invokeLater(() -> {
+            // Clear tree
+            DefaultTreeModel model = (DefaultTreeModel)openTypeHierarchyTree.getModel();
+            DefaultMutableTreeNode root = (DefaultMutableTreeNode)model.getRoot();
+            root.removeAllChildren();
+
+            TreeNode selectedTreeNode = createTreeNode(entry, typeName);
+            TreeNode parentTreeNode = createParentTreeNode(selectedTreeNode);
+
+            root.add(parentTreeNode);
+            model.reload();
+
+            if (selectedTreeNode != null) {
+                TreePath path = new TreePath(selectedTreeNode.getPath());
+                // Expand
+                openTypeHierarchyTree.expandPath(path);
+                // Scroll to show tree node
+                openTypeHierarchyTree.makeVisible(path);
+                Rectangle bounds = openTypeHierarchyTree.getPathBounds(path);
+
+                if(bounds != null) {
+                    bounds.x = 0;
+
+                    Rectangle lastRowBounds = openTypeHierarchyTree.getRowBounds(openTypeHierarchyTree.getRowCount()-1);
+
+                    if (lastRowBounds != null) {
+                        bounds.y = Math.max(bounds.y-30, 0);
+                        bounds.height = Math.min(bounds.height+bounds.y+60, lastRowBounds.height+lastRowBounds.y) - bounds.y;
+                    }
+
+                    openTypeHierarchyTree.scrollRectToVisible(bounds);
+                    openTypeHierarchyTree.scrollPathToVisible(path);
+                    openTypeHierarchyTree.fireVisibleDataPropertyChange();
+                }
+                // Select tree node
+                openTypeHierarchyTree.setSelectionPath(path);
+            }
+        });
+    }
+
+    protected TreeNode createTreeNode(Container.Entry entry, String typeName) {
+        Type type = api.getTypeFactory(entry).make(api, entry, typeName);
+
+        typeName = type.getName();
+
+        List<Container.Entry> entries = getEntries(typeName);
+        TreeNode treeNode = new TreeNode(entry, typeName, entries, new TreeNodeBean(type));
+        List<String> childTypeNames = getSubTypeNames(typeName);
+
+        if (childTypeNames != null) {
+            // Add dummy node
+            treeNode.add(new DefaultMutableTreeNode());
+        }
+
+        return treeNode;
+    }
+
+    /**
+     * Create parent and sibling tree nodes
+     */
+    protected TreeNode createParentTreeNode(TreeNode treeNode) {
+        Type type = api.getTypeFactory(treeNode.entry).make(api, treeNode.entry, treeNode.typeName);
+        String superTypeName = type.getSuperName();
+
+        if (superTypeName != null) {
+            List<Container.Entry> superEntries = getEntries(superTypeName);
+
+            // Search entry in the sane container of 'entry'
+            Container.Entry superEntry = null;
+
+            if ((superEntries != null) && !superEntries.isEmpty()) {
+                for (Container.Entry se : superEntries) {
+                    if (se.getContainer() == treeNode.entry.getContainer()) {
+                        superEntry = se;
+                        break;
+                    }
+                }
+
+                if (superEntry == null) {
+                    // Not found -> Choose 1st one
+                    superEntry = superEntries.get(0);
+                }
+            } else {
+                superEntry = null;
+            }
+
+            if (superEntry != null) {
+                // Create parent tree node
+                TreeNode superTreeNode = createTreeNode(superEntry, superTypeName);
+                // Populate parent tree node
+                populateTreeNode(superTreeNode, treeNode);
+                // Recursive call
+                return createParentTreeNode(superTreeNode);
+            } else {
+                // Entry not found --> Most probable hypothesis : Java type entry
+                int lastPackageSeparatorIndex = superTypeName.lastIndexOf('/');
+                String package_ = superTypeName.substring(0, lastPackageSeparatorIndex).replace('/', '.');
+                String name = superTypeName.substring(lastPackageSeparatorIndex + 1).replace('$', '.');
+                String label = (package_ != null) ? name + " - " + package_ : name;
+                Icon icon = ((type.getFlags() & Type.FLAG_INTERFACE) == 0) ? ROOT_CLASS_ICON : ROOT_INTERFACE_ICON;
+                TreeNode rootTreeNode = new TreeNode(null, superTypeName, null, new TreeNodeBean(label, icon));
+
+                if (package_.startsWith("java.")) {
+                    // If root type is a JDK type, do not create a tree node for each child types
+                    rootTreeNode.add(treeNode);
+                } else {
+                    populateTreeNode(rootTreeNode, treeNode);
+                }
+
+                return rootTreeNode;
+            }
+        } else {
+            // super type undefined
+            return treeNode;
+        }
+    }
+
+    /**
+     * @param superTreeNode  node to populate
+     * @param activeTreeNode active child node
+     */
+    protected void populateTreeNode(TreeNode superTreeNode, TreeNode activeTreeNode) {
+        superTreeNode.removeAllChildren();
+
+        // Search preferred container: if 'superTreeNode' is a root with an unknown super entry, uses the container of active child node
+        Container.Entry notNullEntry = superTreeNode.entry;
+
+        if (notNullEntry == null) {
+            notNullEntry = activeTreeNode.entry;
+        }
+
+        Container preferredContainer = notNullEntry.getContainer();
+        String activeTypName = null;
+
+        if (activeTreeNode != null) {
+            activeTypName = activeTreeNode.typeName;
+        }
+
+        List<String> subTypeNames = getSubTypeNames(superTreeNode.typeName);
+        ArrayList<TreeNode> treeNodes = new ArrayList<>();
+
+        for (String subTypeName : subTypeNames) {
+            if (subTypeName.equals(activeTypName)) {
+                treeNodes.add(activeTreeNode);
+            } else {
+                // Search entry in the sane container of 'superTreeNode.entry'
+                List<Container.Entry> entries = getEntries(subTypeName);
+                Container.Entry entry = null;
+
+                for (Container.Entry e : entries) {
+                    if (e.getContainer() == preferredContainer) {
+                        entry = e;
+                    }
+                }
+
+                if (entry == null) {
+                    // Not found -> Choose 1st one
+                    entry = entries.get(0);
+                }
+                if (entry != null) {
+                    // Create type
+                    Type t = api.getTypeFactory(entry).make(api, entry, subTypeName);
+                    if (t != null) {
+                        // Create tree node
+                        treeNodes.add(createTreeNode(entry, t.getName()));
+                    }
+                }
+            }
+        }
+
+        treeNodes.sort(TREE_NODE_COMPARATOR);
+
+        for (TreeNode treeNode : treeNodes) {
+            superTreeNode.add(treeNode);
+        }
+    }
+
+    public void focus() {
+        SwingUtil.invokeLater(() -> openTypeHierarchyTree.requestFocus());
+    }
+
+    protected void onTypeSelected() {
+        TreeNode selectedTreeNode = (TreeNode)openTypeHierarchyTree.getLastSelectedPathComponent();
+
+        if (selectedTreeNode != null) {
+            TreePath path = new TreePath(selectedTreeNode.getPath());
+            Rectangle bounds = openTypeHierarchyTree.getPathBounds(path);
+            Point listLocation = openTypeHierarchyTree.getLocationOnScreen();
+            Point leftBottom = new Point(listLocation.x+bounds.x, listLocation.y+bounds.y+bounds.height);
+            selectedTypeCallback.accept(leftBottom, selectedTreeNode.entries, selectedTreeNode.typeName);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected List<String> getSubTypeNames(String typeName) {
+        ArrayList<String> result = new ArrayList<>();
+
+        try {
+            for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
+                if (futureIndexes.isDone()) {
+                    Map<String, Collection> subTypeNames = futureIndexes.get().getIndex("subTypeNames");
+                    if (subTypeNames != null) {
+                        Collection<String> collection = subTypeNames.get(typeName);
+                        if (collection != null) {
+                            for (String tn : collection) {
+                                if (tn != null) {
+                                    result.add(tn);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    protected List<Container.Entry> getEntries(String typeName) {
+        ArrayList<Container.Entry> result = new ArrayList<>();
+
+        try {
+            for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
+                if (futureIndexes.isDone()) {
+                    Map<String, Collection> typeDeclarations = futureIndexes.get().getIndex("typeDeclarations");
+                    if (typeDeclarations != null) {
+                        Collection<Container.Entry> collection = typeDeclarations.get(typeName);
+                        if (collection != null) {
+                            for (Container.Entry e : collection) {
+                                if (e != null) {
+                                    result.add(e);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        return result;
+    }
+
+    protected static class TreeNode extends DefaultMutableTreeNode {
+        Container.Entry entry;
+        String typeName;
+        List<Container.Entry> entries;
+
+        TreeNode(Container.Entry entry, String typeName, List<Container.Entry> entries, Object userObject) {
+            super(userObject);
+            this.entry = entry;
+            this.typeName = typeName;
+            this.entries = entries;
+        }
+    }
+
+    // Graphic data for renderer
+    protected static class TreeNodeBean implements TreeNodeData {
+        String label;
+        String tip;
+        Icon icon;
+        Icon openIcon;
+
+        TreeNodeBean(Type type) {
+            this.label = (type.getDisplayPackageName() != null) ? type.getDisplayTypeName() + " - " + type.getDisplayPackageName() : type.getDisplayTypeName();
+            this.icon = type.getIcon();
+        }
+
+        TreeNodeBean(String label, Icon icon) {
+            this.label = label;
+            this.icon = icon;
+        }
+
+        @Override public String getLabel() { return label; }
+        @Override public String getTip() { return tip; }
+        @Override public Icon getIcon() { return icon; }
+        @Override public Icon getOpenIcon() { return openIcon; }
+    }
+
+    protected static class TreeNodeComparator implements Comparator<TreeNode> {
+        @Override
+        public int compare(TreeNode tn1, TreeNode tn2) {
+            return ((TreeNodeBean)tn1.getUserObject()).label.compareTo(((TreeNodeBean)tn2.getUserObject()).label);
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/OpenTypeView.java b/app/src/main/java/org/jd/gui/view/OpenTypeView.java
new file mode 100644
index 0000000..725a0c9
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/OpenTypeView.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Type;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.function.TriConsumer;
+import org.jd.gui.util.swing.SwingUtil;
+import org.jd.gui.view.bean.OpenTypeListCellBean;
+import org.jd.gui.view.renderer.OpenTypeListCellRenderer;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.BadLocationException;
+import java.awt.*;
+import java.awt.event.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.function.Consumer;
+
+public class OpenTypeView {
+    protected static final int MAX_LINE_COUNT = 80;
+    protected static final TypeNameComparator TYPE_NAME_COMPARATOR = new TypeNameComparator();
+
+    protected API api;
+
+    protected JDialog openTypeDialog;
+    protected JTextField openTypeEnterTextField;
+    protected JLabel openTypeMatchLabel;
+    protected JList openTypeList;
+
+    @SuppressWarnings("unchecked")
+    public OpenTypeView(API api, JFrame mainFrame, Consumer<String> changedPatternCallback, TriConsumer<Point, Collection<Container.Entry>, String> selectedTypeCallback) {
+        this.api = api;
+        // Build GUI
+        SwingUtil.invokeLater(() -> {
+            openTypeDialog = new JDialog(mainFrame, "Open Type", false);
+
+            JPanel panel = new JPanel();
+            panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+            panel.setLayout(new BorderLayout());
+            openTypeDialog.add(panel);
+
+            // Box for "Select a type to open"
+            Box vbox = Box.createVerticalBox();
+            panel.add(vbox, BorderLayout.NORTH);
+
+            Box hbox = Box.createHorizontalBox();
+            hbox.add(new JLabel("Select a type to open (* = any string, ? = any character, TZ = TimeZone):"));
+            hbox.add(Box.createHorizontalGlue());
+            vbox.add(hbox);
+
+            vbox.add(Box.createVerticalStrut(10));
+
+            // Text field
+            vbox.add(openTypeEnterTextField = new JTextField(30));
+            openTypeEnterTextField.addKeyListener(new KeyAdapter() {
+                @Override public void keyTyped(KeyEvent e) {
+                    switch (e.getKeyChar()) {
+                        case '=': case '(': case ')': case '{': case '}': case '[': case ']':
+                            e.consume();
+                            break;
+                        default:
+                            if (Character.isDigit(e.getKeyChar()) && (openTypeEnterTextField.getText().length() == 0)) {
+                                // First character can not be a digit
+                                e.consume();
+                            }
+                            break;
+                    }
+                }
+                @Override public void keyPressed(KeyEvent e) {
+                    if ((e.getKeyCode() == KeyEvent.VK_DOWN) && (openTypeList.getModel().getSize() > 0)) {
+                        openTypeList.setSelectedIndex(0);
+                        openTypeList.requestFocus();
+                        e.consume();
+                    }
+                }
+            });
+            openTypeEnterTextField.addFocusListener(new FocusListener() {
+                @Override public void focusGained(FocusEvent e) { openTypeList.clearSelection(); }
+                @Override public void focusLost(FocusEvent e) {}
+            });
+            openTypeEnterTextField.getDocument().addDocumentListener(new DocumentListener() {
+                @Override public void insertUpdate(DocumentEvent e) { call(e); }
+                @Override public void removeUpdate(DocumentEvent e) { call(e); }
+                @Override public void changedUpdate(DocumentEvent e) { call(e); }
+                protected void call(DocumentEvent e) {
+                    try {
+                        changedPatternCallback.accept(e.getDocument().getText(0, e.getDocument().getLength()));
+                    } catch (BadLocationException ex) {
+                        assert ExceptionUtil.printStackTrace(ex);
+                    }
+                }
+            });
+
+            vbox.add(Box.createVerticalStrut(10));
+
+            hbox = Box.createHorizontalBox();
+            hbox.add(openTypeMatchLabel = new JLabel("Matching types:"));
+            hbox.add(Box.createHorizontalGlue());
+            vbox.add(hbox);
+
+            vbox.add(Box.createVerticalStrut(10));
+
+            // List of types
+            JScrollPane scrollPane = new JScrollPane(openTypeList = new JList());
+            openTypeList.addKeyListener(new KeyAdapter() {
+                @Override public void keyPressed(KeyEvent e) {
+                    if ((e.getKeyCode() == KeyEvent.VK_UP) && (openTypeList.getSelectedIndex()  == 0)) {
+                        openTypeEnterTextField.requestFocus();
+                        e.consume();
+                    }
+                }
+            });
+            openTypeList.setModel(new DefaultListModel<OpenTypeListCellBean>());
+            openTypeList.setCellRenderer(new OpenTypeListCellRenderer());
+            openTypeList.addMouseListener(new MouseAdapter() {
+                @Override public void mouseClicked(MouseEvent e) {
+                    if (e.getClickCount() == 2) {
+                        onTypeSelected(selectedTypeCallback);
+                    }
+                }
+            });
+            scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+            scrollPane.setPreferredSize(new Dimension(400, 150));
+            panel.add(scrollPane, BorderLayout.CENTER);
+
+            // Buttons "Open" and "Cancel"
+            vbox = Box.createVerticalBox();
+            panel.add(vbox, BorderLayout.SOUTH);
+            vbox.add(Box.createVerticalStrut(25));
+            vbox.add(hbox = Box.createHorizontalBox());
+            hbox.add(Box.createHorizontalGlue());
+            JButton openTypeOpenButton = new JButton("Open");
+            hbox.add(openTypeOpenButton);
+            openTypeOpenButton.setEnabled(false);
+            openTypeOpenButton.addActionListener(e -> onTypeSelected(selectedTypeCallback));
+            hbox.add(Box.createHorizontalStrut(5));
+            JButton openTypeCancelButton = new JButton("Cancel");
+            hbox.add(openTypeCancelButton);
+            Action openTypeCancelActionListener = new AbstractAction() {
+                @Override public void actionPerformed(ActionEvent actionEvent) { openTypeDialog.setVisible(false); }
+            };
+            openTypeCancelButton.addActionListener(openTypeCancelActionListener);
+
+            // Last setup
+            JRootPane rootPane = openTypeDialog.getRootPane();
+            rootPane.setDefaultButton(openTypeOpenButton);
+            rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "OpenTypeView.cancel");
+            rootPane.getActionMap().put("OpenTypeView.cancel", openTypeCancelActionListener);
+
+            openTypeList.addListSelectionListener(e -> openTypeOpenButton.setEnabled(openTypeList.getSelectedValue() != null));
+
+            openTypeDialog.setMinimumSize(openTypeDialog.getSize());
+
+            // Prepare to display
+            openTypeDialog.pack();
+            openTypeDialog.setLocationRelativeTo(mainFrame);
+        });
+    }
+
+    public void show() {
+        SwingUtil.invokeLater(() -> {
+            // Init
+            openTypeEnterTextField.selectAll();
+            // Show
+            openTypeDialog.setVisible(true);
+            openTypeEnterTextField.requestFocus();
+        });
+    }
+
+    public boolean isVisible() { return openTypeDialog.isVisible(); }
+
+    public String getPattern() { return openTypeEnterTextField.getText(); }
+
+    public void showWaitCursor() {
+        SwingUtil.invokeLater(() -> openTypeDialog.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)));
+    }
+
+    public void hideWaitCursor() {
+        SwingUtil.invokeLater(() -> openTypeDialog.setCursor(Cursor.getDefaultCursor()));
+    }
+
+    @SuppressWarnings("unchecked")
+    public void updateList(Map<String, Collection<Container.Entry>> map) {
+        SwingUtil.invokeLater(() -> {
+            DefaultListModel model = (DefaultListModel)openTypeList.getModel();
+            ArrayList<String> typeNames = new ArrayList<>(map.keySet());
+            int index = 0;
+
+            typeNames.sort(TYPE_NAME_COMPARATOR);
+
+            model.removeAllElements();
+
+            for (String typeName : typeNames) {
+                if (index < MAX_LINE_COUNT) {
+                    Collection<Container.Entry> entries = map.get(typeName);
+                    Container.Entry firstEntry = entries.iterator().next();
+                    Type type = api.getTypeFactory(firstEntry).make(api, firstEntry, typeName);
+
+                    if (type != null) {
+                        model.addElement(new OpenTypeListCellBean(type.getDisplayTypeName(), type.getDisplayPackageName(), type.getIcon(), entries, typeName));
+                    } else {
+                        model.addElement(new OpenTypeListCellBean(typeName, entries, typeName));
+                    }
+                } else if (index == MAX_LINE_COUNT) {
+                    model.addElement(null);
+                }
+            }
+
+            int count = typeNames.size();
+
+            switch (count) {
+                case 0:
+                    openTypeMatchLabel.setText("Matching types:");
+                    break;
+                case 1:
+                    openTypeMatchLabel.setText("1 matching type:");
+                    break;
+                default:
+                    openTypeMatchLabel.setText(count + " matching types:");
+            }
+        });
+    }
+
+    public void focus() {
+        SwingUtil.invokeLater(() -> {
+            openTypeList.requestFocus();
+        });
+    }
+
+    protected void onTypeSelected(TriConsumer<Point, Collection<Container.Entry>, String> selectedTypeCallback) {
+        SwingUtil.invokeLater(() -> {
+            int index = openTypeList.getSelectedIndex();
+
+            if (index != -1) {
+                OpenTypeListCellBean selectedCellBean = (OpenTypeListCellBean)openTypeList.getModel().getElementAt(index);
+                Point listLocation = openTypeList.getLocationOnScreen();
+                Rectangle cellBound = openTypeList.getCellBounds(index, index);
+                Point leftBottom = new Point(listLocation.x + cellBound.x, listLocation.y + cellBound.y + cellBound.height);
+                selectedTypeCallback.accept(leftBottom, selectedCellBean.entries, selectedCellBean.typeName);
+            }
+        });
+    }
+
+    protected static class TypeNameComparator implements Comparator<String> {
+        @Override
+        public int compare(String tn1, String tn2) {
+            int lasPackageSeparatorIndex = tn1.lastIndexOf('/');
+            String shortName1 = tn1.substring(lasPackageSeparatorIndex+1);
+
+            lasPackageSeparatorIndex = tn2.lastIndexOf('/');
+            String shortName2 = tn2.substring(lasPackageSeparatorIndex+1);
+
+            return shortName1.compareTo(shortName2);
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/PreferencesView.java b/app/src/main/java/org/jd/gui/view/PreferencesView.java
new file mode 100644
index 0000000..587cde6
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/PreferencesView.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view;
+
+import org.jd.gui.model.configuration.Configuration;
+import org.jd.gui.spi.PreferencesPanel;
+import org.jd.gui.util.swing.SwingUtil;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.util.*;
+
+public class PreferencesView implements PreferencesPanel.PreferencesPanelChangeListener {
+    protected Map<String, String> preferences;
+    protected Collection<PreferencesPanel> panels;
+    protected HashMap<PreferencesPanel, Boolean> valids = new HashMap<>();
+
+    protected JDialog preferencesDialog;
+    protected JButton preferencesOkButton = new JButton();
+
+    protected Runnable okCallback;
+
+    public PreferencesView(Configuration configuration, JFrame mainFrame, Collection<PreferencesPanel> panels) {
+        this.preferences = configuration.getPreferences();
+        this.panels = panels;
+        // Build GUI
+        SwingUtil.invokeLater(() -> {
+            preferencesDialog = new JDialog(mainFrame, "Preferences", false);
+
+            JPanel panel = new JPanel();
+            panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+            panel.setLayout(new BorderLayout());
+            preferencesDialog.add(panel);
+
+            // Box for preferences panels
+            Box preferencesPanels = Box.createVerticalBox();
+            preferencesPanels.setBackground(panel.getBackground());
+            preferencesPanels.setOpaque(true);
+            Color errorBackgroundColor = Color.decode(configuration.getPreferences().get("JdGuiPreferences.errorBackgroundColor"));
+
+            // Group "PreferencesPanel" by group name
+            HashMap<String, ArrayList<PreferencesPanel>> groups = new HashMap<>();
+            ArrayList<String> sortedGroupNames = new ArrayList<>();
+
+            for (PreferencesPanel pp : panels) {
+                ArrayList<PreferencesPanel> pps = groups.get(pp.getPreferencesGroupTitle());
+
+                pp.init(errorBackgroundColor);
+                pp.addPreferencesChangeListener(this);
+
+                if (pps == null) {
+                    String groupNames = pp.getPreferencesGroupTitle();
+                    groups.put(groupNames, pps=new ArrayList<>());
+                    sortedGroupNames.add(groupNames);
+                }
+
+                pps.add(pp);
+            }
+
+            Collections.sort(sortedGroupNames);
+
+            // Add preferences panels
+            for (String groupName : sortedGroupNames) {
+                Box vbox = Box.createVerticalBox();
+                vbox.setBorder(BorderFactory.createTitledBorder(groupName));
+
+                ArrayList<PreferencesPanel> sortedPreferencesPanels = groups.get(groupName);
+                Collections.sort(sortedPreferencesPanels, new PreferencesPanelComparator());
+
+                for (PreferencesPanel pp : sortedPreferencesPanels) {
+                    // Add title
+                    Box hbox = Box.createHorizontalBox();
+                    JLabel title = new JLabel(pp.getPreferencesPanelTitle());
+                    title.setFont(title.getFont().deriveFont(Font.BOLD));
+                    hbox.add(title);
+                    hbox.add(Box.createHorizontalGlue());
+                    hbox.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
+                    vbox.add(hbox);
+                    // Add panel
+                    JComponent component = pp.getPanel();
+                    component.setMaximumSize(new Dimension(component.getMaximumSize().width, component.getPreferredSize().height));
+                    vbox.add(component);
+                }
+
+                preferencesPanels.add(vbox);
+            }
+
+            JScrollPane preferencesScrollPane = new JScrollPane(preferencesPanels);
+            preferencesScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+            preferencesScrollPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
+            panel.add(preferencesScrollPane, BorderLayout.CENTER);
+
+            Box vbox = Box.createVerticalBox();
+            panel.add(vbox, BorderLayout.SOUTH);
+
+            vbox.add(Box.createVerticalStrut(15));
+
+            // Buttons "Ok" and "Cancel"
+            Box hbox = Box.createHorizontalBox();
+            hbox.add(Box.createHorizontalGlue());
+            preferencesOkButton.setText("   Ok   ");
+            preferencesOkButton.addActionListener(e -> {
+                for (PreferencesPanel pp : panels) {
+                    pp.savePreferences(preferences);
+                }
+                preferencesDialog.setVisible(false);
+                okCallback.run();
+            });
+            hbox.add(preferencesOkButton);
+            hbox.add(Box.createHorizontalStrut(5));
+            JButton preferencesCancelButton = new JButton("Cancel");
+            Action preferencesCancelActionListener = new AbstractAction() {
+                public void actionPerformed(ActionEvent actionEvent) { preferencesDialog.setVisible(false); }
+            };
+            preferencesCancelButton.addActionListener(preferencesCancelActionListener);
+            hbox.add(preferencesCancelButton);
+            vbox.add(hbox);
+
+            // Last setup
+            JRootPane rootPane = preferencesDialog.getRootPane();
+            rootPane.setDefaultButton(preferencesOkButton);
+            rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "PreferencesDescription.cancel");
+            rootPane.getActionMap().put("PreferencesDescription.cancel", preferencesCancelActionListener);
+
+            // Size of the screen
+            Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+            // Height of the task bar
+            Insets scnMax = Toolkit.getDefaultToolkit().getScreenInsets(preferencesDialog.getGraphicsConfiguration());
+            // screen height in pixels without taskbar
+            int taskBarHeight = scnMax.bottom + scnMax.top;
+            int maxHeight = screenSize.height - taskBarHeight;
+            int preferredHeight = preferencesPanels.getPreferredSize().height + 2;
+
+            if (preferredHeight > maxHeight) {
+                preferredHeight = maxHeight;
+            }
+
+            preferencesScrollPane.setPreferredSize(new Dimension(400, preferredHeight));
+            preferencesDialog.setMinimumSize(new Dimension(300, 200));
+
+            // Prepare to display
+            preferencesDialog.pack();
+            preferencesDialog.setLocationRelativeTo(mainFrame);
+        });
+    }
+
+    public void show(Runnable okCallback) {
+        this.okCallback = okCallback;
+
+        SwingUtilities.invokeLater(() -> {
+            // Init
+            for (PreferencesPanel pp : panels) {
+                pp.loadPreferences(preferences);
+            }
+            // Show
+            preferencesDialog.setVisible(true);
+        });
+    }
+
+    // --- PreferencesPanel.PreferencesChangeListener --- //
+    public void preferencesPanelChanged(PreferencesPanel source) {
+        SwingUtil.invokeLater(() -> {
+            boolean valid = source.arePreferencesValid();
+
+            valids.put(source, Boolean.valueOf(valid));
+
+            if (valid) {
+                for (PreferencesPanel pp : panels) {
+                    if (valids.get(pp) == Boolean.FALSE) {
+                        preferencesOkButton.setEnabled(false);
+                        return;
+                    }
+                }
+                preferencesOkButton.setEnabled(true);
+            } else {
+                preferencesOkButton.setEnabled(false);
+            }
+        });
+    }
+
+    protected static class PreferencesPanelComparator implements Comparator<PreferencesPanel> {
+        @Override
+        public int compare(PreferencesPanel pp1, PreferencesPanel pp2) {
+            return pp1.getPreferencesPanelTitle().compareTo(pp2.getPreferencesPanelTitle());
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/SaveAllSourcesView.java b/app/src/main/java/org/jd/gui/view/SaveAllSourcesView.java
new file mode 100644
index 0000000..b35f5c0
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/SaveAllSourcesView.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view;
+
+import org.jd.gui.util.swing.SwingUtil;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.File;
+
+public class SaveAllSourcesView {
+    protected JDialog saveAllSourcesDialog;
+    protected JLabel saveAllSourcesLabel;
+    protected JProgressBar saveAllSourcesProgressBar;
+
+    public SaveAllSourcesView(JFrame mainFrame, Runnable cancelCallback) {
+        // Build GUI
+        SwingUtil.invokeLater(() -> {
+            saveAllSourcesDialog = new JDialog(mainFrame, "Save All Sources", false);
+            saveAllSourcesDialog.setResizable(false);
+            saveAllSourcesDialog.addWindowListener(new WindowAdapter() {
+                @Override public void windowClosing(WindowEvent e) {
+                    cancelCallback.run();
+                }
+            });
+
+            Box vbox = Box.createVerticalBox();
+            vbox.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+            saveAllSourcesDialog.add(vbox);
+
+            // First label "Saving 'file' ..."
+            Box hbox = Box.createHorizontalBox();
+            hbox.add(saveAllSourcesLabel = new JLabel());
+            hbox.add(Box.createHorizontalGlue());
+            vbox.add(hbox);
+
+            vbox.add(Box.createVerticalStrut(10));
+
+            vbox.add(saveAllSourcesProgressBar = new JProgressBar());
+
+            vbox.add(Box.createVerticalStrut(15));
+
+            // Button "Cancel"
+            hbox = Box.createHorizontalBox();
+            hbox.add(Box.createHorizontalGlue());
+            JButton saveAllSourcesCancelButton = new JButton("Cancel");
+            Action saveAllSourcesCancelActionListener = new AbstractAction() {
+                public void actionPerformed(ActionEvent actionEvent) {
+                    cancelCallback.run();
+                    saveAllSourcesDialog.setVisible(false);
+                }
+            };
+            saveAllSourcesCancelButton.addActionListener(saveAllSourcesCancelActionListener);
+            hbox.add(saveAllSourcesCancelButton);
+            vbox.add(hbox);
+
+            // Last setup
+            JRootPane rootPane = saveAllSourcesDialog.getRootPane();
+            rootPane.setDefaultButton(saveAllSourcesCancelButton);
+            rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "SaveAllSourcesView.cancel");
+            rootPane.getActionMap().put("SaveAllSourcesView.cancel", saveAllSourcesCancelActionListener);
+
+            // Prepare to display
+            saveAllSourcesDialog.pack();
+        });
+    }
+
+    public void show(File file) {
+        SwingUtil.invokeLater(() -> {
+            // Init
+            saveAllSourcesLabel.setText("Saving '" + file.getAbsolutePath() + "'...");
+            saveAllSourcesProgressBar.setValue(0);
+            saveAllSourcesProgressBar.setMaximum(10);
+            saveAllSourcesProgressBar.setIndeterminate(true);
+            saveAllSourcesDialog.pack();
+            // Show
+            saveAllSourcesDialog.setLocationRelativeTo(saveAllSourcesDialog.getParent());
+            saveAllSourcesDialog.setVisible(true);
+        });
+    }
+
+    public boolean isVisible() { return saveAllSourcesDialog.isVisible(); }
+
+    public void setMaxValue(int maxValue) {
+        SwingUtil.invokeLater(() -> {
+            if (maxValue > 0) {
+                saveAllSourcesProgressBar.setMaximum(maxValue);
+                saveAllSourcesProgressBar.setIndeterminate(false);
+            } else {
+                saveAllSourcesProgressBar.setIndeterminate(true);
+            }
+        });
+    }
+
+    public void updateProgressBar(int value) {
+        SwingUtil.invokeLater(() -> {
+            saveAllSourcesProgressBar.setValue(value);
+        });
+    }
+
+    public void hide() {
+        SwingUtil.invokeLater(() -> {
+            saveAllSourcesDialog.setVisible(false);
+        });
+    }
+
+    public void showActionFailedDialog() {
+        SwingUtil.invokeLater(() -> {
+            JOptionPane.showMessageDialog(saveAllSourcesDialog, "'Save All Sources' action failed.", "Error", JOptionPane.ERROR_MESSAGE);
+        });
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/SearchInConstantPoolsView.java b/app/src/main/java/org/jd/gui/view/SearchInConstantPoolsView.java
new file mode 100644
index 0000000..ab9258a
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/SearchInConstantPoolsView.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.TreeNodeExpandable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.model.container.DelegatingFilterContainer;
+import org.jd.gui.spi.TreeNodeFactory;
+import org.jd.gui.util.function.TriConsumer;
+import org.jd.gui.util.swing.SwingUtil;
+import org.jd.gui.view.component.Tree;
+import org.jd.gui.view.renderer.TreeNodeRenderer;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.event.TreeExpansionEvent;
+import javax.swing.event.TreeExpansionListener;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreePath;
+import java.awt.*;
+import java.awt.event.*;
+import java.net.URI;
+import java.util.*;
+import java.util.function.BiConsumer;
+
+public class SearchInConstantPoolsView<T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> {
+    protected static final ContainerComparator CONTAINER_COMPARATOR = new ContainerComparator();
+
+    public static final int SEARCH_TYPE = 1;
+    public static final int SEARCH_CONSTRUCTOR = 2;
+    public static final int SEARCH_METHOD = 4;
+    public static final int SEARCH_FIELD = 8;
+    public static final int SEARCH_STRING = 16;
+    public static final int SEARCH_MODULE = 32;
+    public static final int SEARCH_DECLARATION = 64;
+    public static final int SEARCH_REFERENCE = 128;
+
+    protected API api;
+    protected Set<URI> accepted = new HashSet<>();
+    protected Set<URI> expanded = new HashSet<>();
+
+    protected JDialog searchInConstantPoolsDialog;
+    protected JTextField searchInConstantPoolsEnterTextField;
+    protected JLabel searchInConstantPoolsLabel;
+    protected JCheckBox searchInConstantPoolsCheckBoxType;
+    protected JCheckBox searchInConstantPoolsCheckBoxField;
+    protected JCheckBox searchInConstantPoolsCheckBoxConstructor;
+    protected JCheckBox searchInConstantPoolsCheckBoxMethod;
+    protected JCheckBox searchInConstantPoolsCheckBoxString;
+    protected JCheckBox searchInConstantPoolsCheckBoxModule;
+    protected JCheckBox searchInConstantPoolsCheckBoxDeclarations;
+    protected JCheckBox searchInConstantPoolsCheckBoxReferences;
+    protected Tree searchInConstantPoolsTree;
+
+    @SuppressWarnings("unchecked")
+    public SearchInConstantPoolsView(
+            API api, JFrame mainFrame,
+            BiConsumer<String, Integer> changedPatternCallback,
+            TriConsumer<URI, String, Integer> selectedTypeCallback) {
+        this.api = api;
+        // Build GUI
+        SwingUtil.invokeLater(() -> {
+            searchInConstantPoolsDialog = new JDialog(mainFrame, "Search", false);
+
+            JPanel panel = new JPanel();
+            panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+            panel.setLayout(new BorderLayout());
+            searchInConstantPoolsDialog.add(panel);
+
+            // Box for search criteria
+            Box vbox = Box.createVerticalBox();
+
+            Box hbox = Box.createHorizontalBox();
+            hbox.add(new JLabel("Search string (* = any string, ? = any character):"));
+            hbox.add(Box.createHorizontalGlue());
+            vbox.add(hbox);
+
+            vbox.add(Box.createVerticalStrut(10));
+
+            // Text field
+            vbox.add(searchInConstantPoolsEnterTextField = new JTextField(30));
+            searchInConstantPoolsEnterTextField.addKeyListener(new KeyAdapter() {
+                @Override public void keyTyped(KeyEvent e)  {
+                    switch (e.getKeyChar()) {
+                        case '=': case '(': case ')': case '{': case '}': case '[': case ']':
+                            e.consume();
+                            break;
+                        default:
+                            if (Character.isDigit(e.getKeyChar()) && (searchInConstantPoolsEnterTextField.getText().length() == 0)) {
+                                // First character can not be a digit
+                                e.consume();
+                            }
+                            break;
+                    }
+                }
+                @Override public void keyPressed(KeyEvent e) {
+                    if (e.getKeyCode() == KeyEvent.VK_DOWN) {
+                        DefaultMutableTreeNode root = (DefaultMutableTreeNode)searchInConstantPoolsTree.getModel().getRoot();
+                        if (root.getChildCount() > 0) {
+                            searchInConstantPoolsTree.requestFocus();
+                            if (searchInConstantPoolsTree.getSelectionCount() == 0) {
+                                searchInConstantPoolsTree.setSelectionPath(new TreePath(((DefaultMutableTreeNode)root.getChildAt(0)).getPath()));
+                            }
+                            e.consume();
+                        }
+                    }
+                }
+            });
+            searchInConstantPoolsEnterTextField.getDocument().addDocumentListener(new DocumentListener() {
+                @Override public void insertUpdate(DocumentEvent e) { call(); }
+                @Override public void removeUpdate(DocumentEvent e) { call(); }
+                @Override public void changedUpdate(DocumentEvent e) { call(); }
+                protected void call() { changedPatternCallback.accept(searchInConstantPoolsEnterTextField.getText(), getFlags()); }
+            });
+
+            vbox.add(Box.createVerticalStrut(10));
+
+            hbox = Box.createHorizontalBox();
+            vbox.add(hbox);
+
+            JPanel subpanel = new JPanel();
+            subpanel.setBorder(BorderFactory.createTitledBorder("Search For"));
+            subpanel.setLayout(new BorderLayout());
+            hbox.add(subpanel);
+
+            Box subhbox = Box.createHorizontalBox();
+            subpanel.add(subhbox, BorderLayout.WEST);
+
+            ItemListener checkBoxListener = (e) -> {
+                changedPatternCallback.accept(searchInConstantPoolsEnterTextField.getText(), getFlags());
+                searchInConstantPoolsEnterTextField.requestFocus();
+            };
+
+            JPanel subsubpanel = new JPanel();
+            subsubpanel.setLayout(new GridLayout(2, 1));
+            subsubpanel.add(searchInConstantPoolsCheckBoxType = new JCheckBox("Type", true));
+            searchInConstantPoolsCheckBoxType.addItemListener(checkBoxListener);
+            subsubpanel.add(searchInConstantPoolsCheckBoxField = new JCheckBox("Field"));
+            searchInConstantPoolsCheckBoxField.addItemListener(checkBoxListener);
+            subhbox.add(subsubpanel);
+
+            subsubpanel = new JPanel();
+            subsubpanel.setLayout(new GridLayout(2, 1));
+            subsubpanel.add(searchInConstantPoolsCheckBoxConstructor = new JCheckBox("Constructor"));
+            searchInConstantPoolsCheckBoxConstructor.addItemListener(checkBoxListener);
+            subsubpanel.add(searchInConstantPoolsCheckBoxMethod = new JCheckBox("Method"));
+            searchInConstantPoolsCheckBoxMethod.addItemListener(checkBoxListener);
+            subhbox.add(subsubpanel);
+
+            subsubpanel = new JPanel();
+            subsubpanel.setLayout(new GridLayout(2, 1));
+            subsubpanel.add(searchInConstantPoolsCheckBoxString = new JCheckBox("String Constant"));
+            searchInConstantPoolsCheckBoxString.addItemListener(checkBoxListener);
+            subsubpanel.add(searchInConstantPoolsCheckBoxModule = new JCheckBox("Java Module"));
+            searchInConstantPoolsCheckBoxModule.addItemListener(checkBoxListener);
+            subhbox.add(subsubpanel);
+
+            subpanel = new JPanel();
+            subpanel.setBorder(BorderFactory.createTitledBorder("Limit To"));
+            subpanel.setLayout(new BorderLayout());
+            hbox.add(subpanel);
+
+            subhbox = Box.createHorizontalBox();
+            subpanel.add(subhbox, BorderLayout.WEST);
+
+            subsubpanel = new JPanel();
+            subsubpanel.setLayout(new GridLayout(2, 1));
+            subsubpanel.add(searchInConstantPoolsCheckBoxDeclarations = new JCheckBox("Declarations", true));
+            searchInConstantPoolsCheckBoxDeclarations.addItemListener(checkBoxListener);
+            subsubpanel.add(searchInConstantPoolsCheckBoxReferences = new JCheckBox("References", true));
+            searchInConstantPoolsCheckBoxReferences.addItemListener(checkBoxListener);
+            subhbox.add(subsubpanel);
+
+            vbox.add(Box.createVerticalStrut(10));
+
+            hbox = Box.createHorizontalBox();
+            hbox.add(searchInConstantPoolsLabel = new JLabel("Matching types:"));
+            hbox.add(Box.createHorizontalGlue());
+            vbox.add(hbox);
+
+            vbox.add(Box.createVerticalStrut(10));
+            panel.add(vbox, BorderLayout.NORTH);
+
+            JScrollPane scrollPane = new JScrollPane(searchInConstantPoolsTree = new Tree());
+            searchInConstantPoolsTree.setShowsRootHandles(true);
+            searchInConstantPoolsTree.setModel(new DefaultTreeModel(new DefaultMutableTreeNode()));
+            searchInConstantPoolsTree.setCellRenderer(new TreeNodeRenderer());
+            searchInConstantPoolsTree.addKeyListener(new KeyAdapter() {
+                @Override public void keyPressed(KeyEvent e) {
+                    if (e.getKeyCode() == KeyEvent.VK_UP) {
+                        if (searchInConstantPoolsTree.getLeadSelectionRow() == 0) {
+                            searchInConstantPoolsEnterTextField.requestFocus();
+                            e.consume();
+                        }
+                    }
+                }
+            });
+            searchInConstantPoolsTree.addMouseListener(new MouseAdapter() {
+                @Override public void mouseClicked(MouseEvent e) {
+                    if (e.getClickCount() == 2) {
+                        T node = (T)searchInConstantPoolsTree.getLastSelectedPathComponent();
+                        if (node != null) {
+                            selectedTypeCallback.accept(node.getUri(), searchInConstantPoolsEnterTextField.getText(), getFlags());
+                        }
+                    }
+                }
+            });
+            searchInConstantPoolsTree.addTreeExpansionListener(new TreeExpansionListener() {
+                @Override public void treeExpanded(TreeExpansionEvent e) {
+                    DefaultTreeModel model = (DefaultTreeModel)searchInConstantPoolsTree.getModel();
+                    T node = (T)e.getPath().getLastPathComponent();
+                    // Expand node and find the first leaf
+                    while (true) {
+                        populate(model, node);
+                        if (node.getChildCount() == 0) {
+                            break;
+                        }
+                        node = (T)node.getChildAt(0);
+                    }
+                    searchInConstantPoolsTree.setSelectionPath(new TreePath(node.getPath()));
+                }
+                @Override public void treeCollapsed(TreeExpansionEvent e) {}
+            });
+            scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+            scrollPane.setPreferredSize(new Dimension(400, 150));
+            panel.add(scrollPane, BorderLayout.CENTER);
+
+            vbox = Box.createVerticalBox();
+
+            vbox.add(Box.createVerticalStrut(25));
+
+            hbox = Box.createHorizontalBox();
+            hbox.add(Box.createHorizontalGlue());
+            JButton searchInConstantPoolsOpenButton = new JButton("Open");
+            hbox.add(searchInConstantPoolsOpenButton);
+            searchInConstantPoolsOpenButton.setEnabled(false);
+            Action searchInConstantPoolsOpenActionListener = new AbstractAction() {
+                @Override public void actionPerformed(ActionEvent actionEvent) {
+                    T selectedTreeNode = (T)searchInConstantPoolsTree.getLastSelectedPathComponent();
+                    if (selectedTreeNode != null) {
+                        selectedTypeCallback.accept(selectedTreeNode.getUri(), searchInConstantPoolsEnterTextField.getText(), getFlags());
+                    }
+                }
+            };
+            searchInConstantPoolsOpenButton.addActionListener(searchInConstantPoolsOpenActionListener);
+            hbox.add(Box.createHorizontalStrut(5));
+            JButton searchInConstantPoolsCancelButton = new JButton("Cancel");
+            hbox.add(searchInConstantPoolsCancelButton);
+            Action searchInConstantPoolsCancelActionListener = new AbstractAction() {
+                @Override public void actionPerformed(ActionEvent actionEvent) { searchInConstantPoolsDialog.setVisible(false); }
+            };
+            searchInConstantPoolsCancelButton.addActionListener(searchInConstantPoolsCancelActionListener);
+
+            vbox.add(hbox);
+
+            panel.add(vbox, BorderLayout.SOUTH);
+
+            // Last setup
+            JRootPane rootPane = searchInConstantPoolsDialog.getRootPane();
+            rootPane.setDefaultButton(searchInConstantPoolsOpenButton);
+            rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "SearchInConstantPoolsView.cancel");
+            rootPane.getActionMap().put("SearchInConstantPoolsView.cancel", searchInConstantPoolsCancelActionListener);
+
+            searchInConstantPoolsDialog.setMinimumSize(searchInConstantPoolsDialog.getSize());
+
+            searchInConstantPoolsEnterTextField.addFocusListener(new FocusAdapter() {
+                @Override public void focusGained(FocusEvent e) {
+                    searchInConstantPoolsTree.clearSelection();
+                    searchInConstantPoolsOpenButton.setEnabled(false);
+                }
+            });
+
+            searchInConstantPoolsTree.addFocusListener(new FocusAdapter() {
+                @Override public void focusGained(FocusEvent e) {
+                    searchInConstantPoolsOpenButton.setEnabled(searchInConstantPoolsTree.getSelectionCount() > 0);
+                }
+            });
+
+            // Prepare to display
+            searchInConstantPoolsDialog.pack();
+            searchInConstantPoolsDialog.setLocationRelativeTo(searchInConstantPoolsDialog.getParent());
+        });
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void populate(DefaultTreeModel model, T node) {
+        // Populate node
+        populate(node);
+        // Populate children
+        int i = node.getChildCount();
+        while (i-- > 0) {
+            T child = (T)node.getChildAt(i);
+            if ((child instanceof TreeNodeExpandable) && !expanded.contains(child.getUri())) {
+                populate(child);
+            }
+        }
+        // Refresh
+        model.reload(node);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void populate(T node) {
+        if ((node instanceof TreeNodeExpandable) && !expanded.contains(node.getUri())) {
+            // Populate
+            ((TreeNodeExpandable)node).populateTreeNode(api);
+            expanded.add(node.getUri());
+            // Filter
+            int i = node.getChildCount();
+            while (i-- > 0) {
+                if (!accepted.contains(((T)node.getChildAt(i)).getUri())) {
+                    node.remove(i);
+                }
+            }
+        }
+    }
+
+    public void show() {
+        SwingUtil.invokeLater(() -> {
+            searchInConstantPoolsEnterTextField.selectAll();
+            // Show
+            searchInConstantPoolsDialog.setVisible(true);
+            searchInConstantPoolsEnterTextField.requestFocus();
+        });
+    }
+
+    public boolean isVisible() { return searchInConstantPoolsDialog.isVisible(); }
+
+    public String getPattern() { return searchInConstantPoolsEnterTextField.getText(); }
+
+    public int getFlags() {
+        int flags = 0;
+
+        if (searchInConstantPoolsCheckBoxType.isSelected())
+            flags += SEARCH_TYPE;
+        if (searchInConstantPoolsCheckBoxConstructor.isSelected())
+            flags += SEARCH_CONSTRUCTOR;
+        if (searchInConstantPoolsCheckBoxMethod.isSelected())
+            flags += SEARCH_METHOD;
+        if (searchInConstantPoolsCheckBoxField.isSelected())
+            flags += SEARCH_FIELD;
+        if (searchInConstantPoolsCheckBoxString.isSelected())
+            flags += SEARCH_STRING;
+        if (searchInConstantPoolsCheckBoxModule.isSelected())
+            flags += SEARCH_MODULE;
+        if (searchInConstantPoolsCheckBoxDeclarations.isSelected())
+            flags += SEARCH_DECLARATION;
+        if (searchInConstantPoolsCheckBoxReferences.isSelected())
+            flags += SEARCH_REFERENCE;
+
+        return flags;
+    }
+
+    public void showWaitCursor() {
+        SwingUtil.invokeLater(() -> searchInConstantPoolsDialog.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)));
+    }
+
+    public void hideWaitCursor() {
+        SwingUtil.invokeLater(() -> searchInConstantPoolsDialog.setCursor(Cursor.getDefaultCursor()));
+    }
+
+    @SuppressWarnings("unchecked")
+    public void updateTree(Collection<DelegatingFilterContainer> containers, int matchingTypeCount) {
+        SwingUtil.invokeLater(() -> {
+            DefaultTreeModel model = (DefaultTreeModel)searchInConstantPoolsTree.getModel();
+            T root = (T)model.getRoot();
+
+            // Reset tree nodes
+            root.removeAllChildren();
+
+            accepted.clear();
+            expanded.clear();
+
+            if (containers != null) {
+                ArrayList<DelegatingFilterContainer> list = new ArrayList<>(containers);
+
+                list.sort(CONTAINER_COMPARATOR);
+
+                for (DelegatingFilterContainer container : list) {
+                    // Init uri set
+                    accepted.addAll(container.getUris());
+                    // Populate tree
+                    Container.Entry parentEntry = container.getRoot().getParent();
+                    TreeNodeFactory treeNodeFactory = api.getTreeNodeFactory(parentEntry);
+
+                    if (treeNodeFactory != null) {
+                        root.add(treeNodeFactory.make(api, parentEntry));
+                    }
+                }
+
+                // Expand node and find the first leaf
+                T node = root;
+                while (true) {
+                    populate(model, node);
+                    if (node.getChildCount() == 0) {
+                        break;
+                    }
+                    node = (T)node.getChildAt(0);
+                }
+                searchInConstantPoolsTree.setSelectionPath(new TreePath(node.getPath()));
+            } else {
+                model.reload();
+            }
+
+            // Update matching item counter
+            switch (matchingTypeCount) {
+                case 0:
+                    searchInConstantPoolsLabel.setText("Matching entries:");
+                    break;
+                case 1:
+                    searchInConstantPoolsLabel.setText("1 matching entry:");
+                    break;
+                default:
+                    searchInConstantPoolsLabel.setText(matchingTypeCount + " matching entries:");
+            }
+        });
+    }
+
+    protected static class ContainerComparator implements Comparator<Container> {
+        @Override
+        public int compare(Container c1, Container c2) {
+            return c1.getRoot().getUri().compareTo(c2.getRoot().getUri());
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/SelectLocationView.java b/app/src/main/java/org/jd/gui/view/SelectLocationView.java
new file mode 100644
index 0000000..3dbc800
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/SelectLocationView.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.TreeNodeExpandable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.model.container.DelegatingFilterContainer;
+import org.jd.gui.spi.TreeNodeFactory;
+import org.jd.gui.util.swing.SwingUtil;
+import org.jd.gui.view.component.Tree;
+import org.jd.gui.view.renderer.TreeNodeRenderer;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreePath;
+import java.awt.*;
+import java.awt.event.*;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class SelectLocationView<T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> {
+    protected static final DelegatingFilterContainerComparator DELEGATING_FILTER_CONTAINER_COMPARATOR = new DelegatingFilterContainerComparator();
+
+    protected API api;
+
+    protected JDialog selectLocationDialog;
+    protected JLabel selectLocationLabel;
+    protected Tree selectLocationTree;
+
+    protected Consumer<URI> selectedEntryCallback;
+    protected Runnable closeCallback;
+
+    @SuppressWarnings("unchecked")
+    public SelectLocationView(API api, JFrame mainFrame) {
+        this.api = api;
+        // Build GUI
+        SwingUtil.invokeLater(() -> {
+            selectLocationDialog = new JDialog(mainFrame, "", false);
+            selectLocationDialog.setUndecorated(true);
+            selectLocationDialog.addWindowListener(new WindowAdapter() {
+                @Override public void windowDeactivated(WindowEvent e) { closeCallback.run(); }
+            });
+
+            Color bg = UIManager.getColor("ToolTip.background");
+
+            JPanel selectLocationPanel = new JPanel(new BorderLayout());
+            selectLocationPanel.setBorder(BorderFactory.createLineBorder(bg.darker()));
+            selectLocationPanel.setBackground(bg);
+            selectLocationDialog.add(selectLocationPanel);
+
+            selectLocationLabel = new JLabel();
+            selectLocationLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));
+            selectLocationPanel.add(selectLocationLabel, BorderLayout.NORTH);
+
+            selectLocationTree = new Tree();
+            selectLocationTree.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+            selectLocationTree.setOpaque(false);
+            selectLocationTree.setModel(new DefaultTreeModel(new DefaultMutableTreeNode()));
+            selectLocationTree.setCellRenderer(new TreeNodeRenderer());
+            selectLocationTree.addKeyListener(new KeyAdapter() {
+                @Override public void keyPressed(KeyEvent e) {
+                    if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+                        onSelectedEntry();
+                    }
+                }
+            });
+            selectLocationTree.addMouseListener(new MouseAdapter() {
+                @Override public void mouseClicked(MouseEvent e) {
+                    if (e.getClickCount() > 0) {
+                        onSelectedEntry();
+                    }
+                }
+            });
+            selectLocationTree.addFocusListener(new FocusAdapter() {
+                @Override public void focusLost(FocusEvent e) { selectLocationDialog.setVisible(false); }
+            });
+            selectLocationPanel.add(selectLocationTree, BorderLayout.CENTER);
+
+            // Last setup
+            JRootPane rootPane = selectLocationDialog.getRootPane();
+            rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "SelectLocationView.cancel");
+            rootPane.getActionMap().put("SelectLocationView.cancel", new AbstractAction() {
+                @Override public void actionPerformed(ActionEvent e) { selectLocationDialog.setVisible(false); }
+            });
+        });
+    }
+
+    @SuppressWarnings("unchecked")
+    public void show(Point location, Collection<DelegatingFilterContainer> containers, int locationCount, Consumer<URI> selectedEntryCallback, Runnable closeCallback) {
+        this.selectedEntryCallback = selectedEntryCallback;
+        this.closeCallback = closeCallback;
+
+        SwingUtil.invokeLater(() -> {
+            // Init
+            T root = (T)selectLocationTree.getModel().getRoot();
+
+            // Reset tree nodes
+            root.removeAllChildren();
+
+            ArrayList<DelegatingFilterContainer> sortedContainers = new ArrayList<>(containers);
+            sortedContainers.sort(DELEGATING_FILTER_CONTAINER_COMPARATOR);
+
+            for (DelegatingFilterContainer container : sortedContainers) {
+                Container.Entry parentEntry = container.getRoot().getParent();
+                TreeNodeFactory factory = api.getTreeNodeFactory(parentEntry);
+
+                if (factory != null) {
+                    T node = factory.make(api, parentEntry);
+
+                    if (node != null) {
+                        root.add(node);
+                        populate(container.getUris(), node);
+                    }
+                }
+            }
+
+            ((DefaultTreeModel)selectLocationTree.getModel()).reload();
+
+            // Expand all nodes
+            for (int row = 0; row < selectLocationTree.getRowCount(); row++) {
+                selectLocationTree.expandRow(row);
+            }
+
+            // Select first leaf
+            T node = root;
+            while (true) {
+                if (node.getChildCount() == 0) {
+                    break;
+                }
+                node = (T)node.getChildAt(0);
+            }
+            selectLocationTree.setSelectionPath(new TreePath(node.getPath()));
+
+            // Reset preferred size
+            selectLocationTree.setPreferredSize(null);
+
+            // Resize
+            Dimension ps = selectLocationTree.getPreferredSize();
+            if (ps.width < 200)
+                ps.width = 200;
+            if (ps.height < 50)
+                ps.height = 50;
+            selectLocationTree.setPreferredSize(ps);
+
+            selectLocationLabel.setText("" + locationCount + " locations:");
+
+            selectLocationDialog.pack();
+            selectLocationDialog.setLocation(location);
+            // Show
+            selectLocationDialog.setVisible(true);
+            selectLocationTree.requestFocus();
+        });
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void populate(Set<URI> uris, DefaultMutableTreeNode node) {
+        if (node instanceof TreeNodeExpandable) {
+            ((TreeNodeExpandable)node).populateTreeNode(api);
+
+            int i = node.getChildCount();
+
+            while (i-- > 0) {
+                T child = (T)node.getChildAt(i);
+
+                if (uris.contains(child.getUri())) {
+                    populate(uris, child);
+                } else {
+                    node.remove(i);
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void onSelectedEntry() {
+        T node = (T)selectLocationTree.getLastSelectedPathComponent();
+
+        if (node != null) {
+            selectLocationDialog.setVisible(false);
+            selectedEntryCallback.accept(node.getUri());
+        }
+    }
+
+    protected static class DelegatingFilterContainerComparator implements Comparator<DelegatingFilterContainer> {
+        @Override
+        public int compare(DelegatingFilterContainer fcw1, DelegatingFilterContainer fcw2) {
+            return fcw1.getRoot().getUri().compareTo(fcw2.getRoot().getUri());
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/bean/OpenTypeListCellBean.java b/app/src/main/java/org/jd/gui/view/bean/OpenTypeListCellBean.java
new file mode 100644
index 0000000..36fa222
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/bean/OpenTypeListCellBean.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.bean;
+
+import org.jd.gui.api.model.Container;
+
+import javax.swing.*;
+import java.util.Collection;
+
+public class OpenTypeListCellBean {
+    public String label;
+    public String packag;
+    public Icon icon;
+    public Collection<Container.Entry> entries;
+    public String typeName;
+
+    public OpenTypeListCellBean(String label, Collection<Container.Entry> entries, String typeName) {
+        this.label = label;
+        this.entries = entries;
+        this.typeName = typeName;
+    }
+
+    public OpenTypeListCellBean(String label, String packag, Icon icon, Collection<Container.Entry> entries, String typeName) {
+        this.label = label;
+        this.packag = packag;
+        this.icon = icon;
+        this.entries = entries;
+        this.typeName = typeName;
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/component/IconButton.java b/app/src/main/java/org/jd/gui/view/component/IconButton.java
new file mode 100644
index 0000000..9bed06f
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/component/IconButton.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class IconButton extends JButton {
+	protected static final Insets INSETS0 = new Insets(0, 0, 0, 0);
+
+	public IconButton(String text, Action action) {
+		setFocusPainted(false);
+		setBorderPainted(false);
+		setMargin(INSETS0);
+		setAction(action);
+		setText(text);
+	}
+
+	public IconButton(Action action) {
+		this(null, action);
+	}
+}
diff --git a/app/src/main/java/org/jd/gui/view/component/List.java b/app/src/main/java/org/jd/gui/view/component/List.java
new file mode 100644
index 0000000..afd72ed
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/component/List.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.jd.gui.api.model.TreeNodeData;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.awt.*;
+import java.awt.event.KeyEvent;
+
+public class List extends JList {
+
+    @SuppressWarnings("unchecked")
+    public List() {
+        super();
+
+        Toolkit toolkit = Toolkit.getDefaultToolkit();
+        KeyStroke ctrlA = KeyStroke.getKeyStroke(KeyEvent.VK_A, toolkit.getMenuShortcutKeyMask());
+        KeyStroke ctrlC = KeyStroke.getKeyStroke(KeyEvent.VK_C, toolkit.getMenuShortcutKeyMask());
+        KeyStroke ctrlV = KeyStroke.getKeyStroke(KeyEvent.VK_V, toolkit.getMenuShortcutKeyMask());
+
+        InputMap inputMap = getInputMap();
+        inputMap.put(ctrlA, "none");
+        inputMap.put(ctrlC, "none");
+        inputMap.put(ctrlV, "none");
+
+        setCellRenderer(new Renderer());
+    }
+
+    protected class Renderer implements ListCellRenderer {
+        protected Color textSelectionColor;
+        protected Color backgroundSelectionColor;
+        protected Color textNonSelectionColor;
+        protected Color backgroundNonSelectionColor;
+
+        protected JLabel label;
+
+        public Renderer() {
+            label = new JLabel();
+            label.setOpaque(true);
+
+            textSelectionColor = UIManager.getColor("List.dropCellForeground");
+            backgroundSelectionColor = UIManager.getColor("List.dropCellBackground");
+            textNonSelectionColor = UIManager.getColor("List.foreground");
+            backgroundNonSelectionColor = UIManager.getColor("List.background");
+            Insets margins = UIManager.getInsets("List.contentMargins");
+
+            if (textSelectionColor == null)
+                textSelectionColor = List.this.getSelectionForeground();
+            if (backgroundSelectionColor == null)
+                backgroundSelectionColor = List.this.getSelectionBackground();
+
+            if (margins != null) {
+                label.setBorder(BorderFactory.createEmptyBorder(margins.top, margins.left, margins.bottom, margins.right));
+            } else {
+                label.setBorder(BorderFactory.createEmptyBorder(0, 2, 1, 2));
+            }
+        }
+
+        @Override
+        public Component getListCellRendererComponent(JList list, Object value, int index, boolean selected, boolean hasFocus) {
+            Object data = ((DefaultMutableTreeNode)value).getUserObject();
+
+            if (data instanceof TreeNodeData) {
+                TreeNodeData tnd = (TreeNodeData)data;
+                label.setIcon(tnd.getIcon());
+                label.setText(tnd.getLabel());
+            } else {
+                label.setIcon(null);
+                label.setText("" + data);
+            }
+
+            if (selected) {
+                label.setForeground(textSelectionColor);
+                label.setBackground(backgroundSelectionColor);
+            } else {
+                label.setForeground(textNonSelectionColor);
+                label.setBackground(backgroundNonSelectionColor);
+            }
+
+            return label;
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/component/Tree.java b/app/src/main/java/org/jd/gui/view/component/Tree.java
new file mode 100644
index 0000000..bbfaa5c
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/component/Tree.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.KeyEvent;
+
+public class Tree extends JTree {
+    public Tree() {
+        Toolkit toolkit = Toolkit.getDefaultToolkit();
+        KeyStroke ctrlA = KeyStroke.getKeyStroke(KeyEvent.VK_A, toolkit.getMenuShortcutKeyMask());
+        KeyStroke ctrlC = KeyStroke.getKeyStroke(KeyEvent.VK_C, toolkit.getMenuShortcutKeyMask());
+        KeyStroke ctrlV = KeyStroke.getKeyStroke(KeyEvent.VK_V, toolkit.getMenuShortcutKeyMask());
+
+        InputMap inputMap = getInputMap();
+        inputMap.put(ctrlA, "none");
+        inputMap.put(ctrlC, "none");
+        inputMap.put(ctrlV, "none");
+
+        setRootVisible(false);
+    }
+
+    public void fireVisibleDataPropertyChange() {
+        if (getAccessibleContext() != null) {
+            getAccessibleContext().firePropertyChange("AccessibleVisibleData", false, true);
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/component/panel/MainTabbedPanel.java b/app/src/main/java/org/jd/gui/view/component/panel/MainTabbedPanel.java
new file mode 100644
index 0000000..e0e6a78
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/component/panel/MainTabbedPanel.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component.panel;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.*;
+import org.jd.gui.service.platform.PlatformService;
+
+import javax.swing.*;
+import java.awt.*;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@SuppressWarnings("unchecked")
+public class MainTabbedPanel<T extends JComponent & UriGettable> extends TabbedPanel<T> implements UriOpenable, PreferencesChangeListener, PageChangeListener {
+    protected ArrayList<PageChangeListener> pageChangedListeners = new ArrayList<>();
+    // Flag to prevent the event cascades
+    protected boolean pageChangedListenersEnabled = true;
+
+    public MainTabbedPanel(API api) {
+        super(api);
+    }
+
+    @Override
+    public void create() {
+        setLayout(cardLayout = new CardLayout());
+
+        Color bg = darker(getBackground());
+
+        if (PlatformService.getInstance().isWindows()) {
+            setBackground(bg);
+        }
+
+        // panel //
+        JPanel panel = new JPanel();
+        panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
+        panel.setBackground(bg);
+
+        Color fontColor = panel.getBackground().darker();
+
+        panel.add(Box.createHorizontalGlue());
+
+        JPanel box = new JPanel();
+        box.setLayout(new BoxLayout(box, BoxLayout.Y_AXIS));
+        box.setBackground(panel.getBackground());
+        box.add(Box.createVerticalGlue());
+
+        JLabel title = newLabel("No files are open", fontColor);
+        title.setFont(title.getFont().deriveFont(Font.BOLD, title.getFont().getSize()+8));
+
+        box.add(title);
+        box.add(newLabel("Open a file with menu \"File > Open File...\"", fontColor));
+        box.add(newLabel("Open recent files with menu \"File > Recent Files\"", fontColor));
+        box.add(newLabel("Drag and drop files from " + getFileManagerLabel(), fontColor));
+        box.add(Box.createVerticalGlue());
+
+        panel.add(box);
+        panel.add(Box.createHorizontalGlue());
+        add("panel", panel);
+
+        // tabs //
+        tabbedPane = createTabPanel();
+        tabbedPane.addChangeListener(e -> {
+            if (pageChangedListenersEnabled) {
+                JComponent subPage = (JComponent)tabbedPane.getSelectedComponent();
+
+                if (subPage == null) {
+                    // Fire page changed event
+                    for (PageChangeListener listener : pageChangedListeners) {
+                        listener.pageChanged(null);
+                    }
+                } else {
+                    T page = (T)subPage.getClientProperty("currentPage");
+
+                    if (page == null) {
+                        page = (T)tabbedPane.getSelectedComponent();
+                    }
+                    // Fire page changed event
+                    for (PageChangeListener listener : pageChangedListeners) {
+                        listener.pageChanged(page);
+                    }
+                    // Update current sub-page preferences
+                    if (subPage instanceof PreferencesChangeListener) {
+                        ((PreferencesChangeListener)subPage).preferencesChanged(preferences);
+                    }
+                }
+            }
+        });
+		add("tabs", tabbedPane);
+
+		setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, darker(darker(bg))));
+	}
+
+	protected String getFileManagerLabel() {
+        switch (PlatformService.getInstance().getOs()) {
+            case Linux:
+                return "your file manager";
+            case MacOSX:
+                return "the Finder";
+            default:
+                return "Explorer";
+        }
+    }
+
+	protected JLabel newLabel(String text, Color fontColor) {
+        JLabel label = new JLabel(text);
+        label.setForeground(fontColor);
+        return label;
+    }
+
+    @Override
+    public void addPage(String title, Icon icon, String tip, T page) {
+        super.addPage(title, icon, tip, page);
+        if (page instanceof PageChangeable) {
+            ((PageChangeable)page).addPageChangeListener(this);
+        }
+    }
+
+    public List<T> getPages() {
+        int i = tabbedPane.getTabCount();
+        ArrayList<T> pages = new ArrayList<>(i);
+        while (i-- > 0) {
+            pages.add((T)tabbedPane.getComponentAt(i));
+        }
+        return pages;
+    }
+
+    public ArrayList<PageChangeListener> getPageChangedListeners() {
+        return pageChangedListeners;
+    }
+
+    // --- URIOpener --- //
+    @Override
+    public boolean openUri(URI uri) {
+        try {
+            // Disable page changed event
+            pageChangedListenersEnabled = false;
+            // Search & display main tab
+            T page = showPage(uri);
+
+            if (page != null) {
+                if (page instanceof UriOpenable) {
+                    // Enable page changed event
+                    pageChangedListenersEnabled = true;
+                    // Search & display sub tab
+                    return ((UriOpenable)page).openUri(uri);
+                }
+                return true;
+            }
+        } finally {
+            // Enable page changed event
+            pageChangedListenersEnabled = true;
+        }
+
+        return false;
+    }
+
+    // --- PageChangedListener --- //
+    @Override
+    public <T extends JComponent & UriGettable> void pageChanged(T page) {
+        // Store active page for current sub tabbed pane
+        Component subPage = tabbedPane.getSelectedComponent();
+
+        if (subPage != null) {
+            ((JComponent)subPage).putClientProperty("currentPage", page);
+        }
+
+        if (page == null) {
+            page = (T)subPage;
+        }
+
+        // Forward event
+        for (PageChangeListener listener : pageChangedListeners) {
+            listener.pageChanged(page);
+        }
+    }
+
+    // --- PreferencesChangeListener --- //
+    @Override
+    public void preferencesChanged(Map<String, String> preferences) {
+        super.preferencesChanged(preferences);
+
+        // Update current sub-page preferences
+        Component subPage = tabbedPane.getSelectedComponent();
+        if (subPage instanceof PreferencesChangeListener) {
+            ((PreferencesChangeListener)subPage).preferencesChanged(preferences);
+        }
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/component/panel/TabbedPanel.java b/app/src/main/java/org/jd/gui/view/component/panel/TabbedPanel.java
new file mode 100644
index 0000000..1267548
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/component/panel/TabbedPanel.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component.panel;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.PreferencesChangeListener;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.service.platform.PlatformService;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.awt.event.*;
+import java.net.URI;
+import java.util.Collection;
+import java.util.Map;
+
+public class TabbedPanel<T extends JComponent & UriGettable> extends JPanel implements PreferencesChangeListener {
+	protected static final ImageIcon CLOSE_ICON = new ImageIcon(TabbedPanel.class.getClassLoader().getResource("org/jd/gui/images/close.gif"));
+    protected static final ImageIcon  CLOSE_ACTIVE_ICON = new ImageIcon(TabbedPanel.class.getClassLoader().getResource("org/jd/gui/images/close_active.gif"));
+
+    protected static final String TAB_LAYOUT = "UITabsPreferencesProvider.singleLineTabs";
+
+    protected API api;
+    protected CardLayout cardLayout;
+    protected JTabbedPane tabbedPane;
+    protected Map<String, String> preferences;
+
+    public TabbedPanel(API api) {
+        this.api = api;
+		create();
+	}
+
+    protected void create() {
+		setLayout(cardLayout = new CardLayout());
+		add("panel", new JPanel());
+		add("tabs", tabbedPane = createTabPanel());
+	}
+
+    protected JTabbedPane createTabPanel() {
+        JTabbedPane tabPanel = new JTabbedPane() {
+            @Override
+            public String getToolTipText(MouseEvent e) {
+                int index = indexAtLocation(e.getX(), e.getY());
+                if (index != -1) {
+                    return ((JComponent)getTabComponentAt(index)).getToolTipText();
+                }
+                return super.getToolTipText(e);
+            }
+        };
+        ToolTipManager.sharedInstance().registerComponent(tabPanel);
+        tabPanel.addMouseListener(new MouseAdapter() {
+            @Override public void mousePressed(MouseEvent e) { showPopupTabMenu(e); }
+            @Override public void mouseReleased(MouseEvent e) { showPopupTabMenu(e); }
+            protected void showPopupTabMenu(MouseEvent e) {
+                if (e.isPopupTrigger()) {
+                    int index = tabPanel.indexAtLocation(e.getX(), e.getY());
+                    if (index != -1) {
+                        new PopupTabMenu(tabPanel.getComponentAt(index)).show(e.getComponent(), e.getX(), e.getY());
+                    }
+                }
+            }
+        });
+        return tabPanel;
+	}
+
+    protected static Color darker(Color c) {
+		return new Color(
+			Math.max((int)(c.getRed()  *0.85), 0),
+			Math.max((int)(c.getGreen()*0.85), 0),
+			Math.max((int)(c.getBlue() *0.85), 0),
+			c.getAlpha());
+	}
+
+    public void addPage(String title, Icon icon, String tip, T page) {
+        // Add a new tab
+        JLabel tabCloseButton = new JLabel(CLOSE_ICON);
+        tabCloseButton.setToolTipText("Close this panel");
+        tabCloseButton.addMouseListener(new MouseListener() {
+            @Override public void mousePressed(MouseEvent e) {}
+            @Override public void mouseReleased(MouseEvent e) {}
+            @Override public void mouseEntered(MouseEvent e) { ((JLabel)e.getSource()).setIcon(CLOSE_ACTIVE_ICON); }
+            @Override public void mouseExited(MouseEvent e) { ((JLabel)e.getSource()).setIcon(CLOSE_ICON); }
+            @Override public void mouseClicked(MouseEvent e) { removeComponent(page); }
+        });
+
+		JPanel tab = new JPanel(new BorderLayout());
+        tab.setBorder(BorderFactory.createEmptyBorder(2, 0, 3, 0));
+		tab.setOpaque(false);
+        tab.setToolTipText(tip);
+        tab.add(new JLabel(title, icon, JLabel.LEADING), BorderLayout.CENTER);
+		tab.add(tabCloseButton, BorderLayout.EAST);
+        ToolTipManager.sharedInstance().unregisterComponent(tab);
+
+		int index = tabbedPane.getTabCount();
+		tabbedPane.addTab(title, page);
+        tabbedPane.setTabComponentAt(index, tab);
+        setSelectedIndex(index);
+
+        cardLayout.show(this, "tabs");
+	}
+
+    protected void setSelectedIndex(int index) {
+        if (index != -1) {
+            if (tabbedPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT) {
+                // Ensure that the new page is visible (bug with SCROLL_TAB_LAYOUT)
+                ChangeEvent event = new ChangeEvent(tabbedPane);
+                for (ChangeListener listener : tabbedPane.getChangeListeners()) {
+                    if (listener.getClass().getPackage().getName().startsWith("javax.")) {
+                        listener.stateChanged(event);
+                    }
+                }
+            }
+
+            tabbedPane.setSelectedIndex(index);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected T showPage(URI uri) {
+        String u1 = uri.getPath();
+        int i = tabbedPane.getTabCount();
+
+        while (i-- > 0) {
+            T page = (T)tabbedPane.getComponentAt(i);
+            String u2 = page.getUri().getPath();
+            if (u1.startsWith(u2)) {
+                tabbedPane.setSelectedIndex(i);
+                return page;
+            }
+        }
+
+        return null;
+    }
+
+    protected class PopupTabMenu extends JPopupMenu {
+        public PopupTabMenu(Component component) {
+            // Add default popup menu entries
+            JMenuItem menuItem = new JMenuItem("Close", null);
+            menuItem.addActionListener(e -> removeComponent(component));
+            add(menuItem);
+
+            menuItem = new JMenuItem("Close Others", null);
+            menuItem.addActionListener(e -> removeOtherComponents(component));
+            add(menuItem);
+
+            menuItem = new JMenuItem("Close All", null);
+            menuItem.addActionListener(e -> removeAllComponents());
+            add(menuItem);
+
+            // Add "Select Tab" popup menu entry
+            if ((tabbedPane.getTabCount() > 1) && (PlatformService.getInstance().isMac() || "true".equals(preferences.get(TAB_LAYOUT)))) {
+                addSeparator();
+                JMenu menu = new JMenu("Select Tab");
+                int count = tabbedPane.getTabCount();
+
+                for (int i = 0; i < count; i++) {
+                    JPanel tab = (JPanel) tabbedPane.getTabComponentAt(i);
+                    JLabel label = (JLabel) tab.getComponent(0);
+                    JMenuItem subMenuItem = new JMenuItem(label.getText(), label.getIcon());
+                    subMenuItem.addActionListener(new SubMenuItemActionListener(i));
+                    if (component == tabbedPane.getComponentAt(i)) {
+                        subMenuItem.setFont(subMenuItem.getFont().deriveFont(Font.BOLD));
+                    }
+                    menu.add(subMenuItem);
+                }
+
+                add(menu);
+            }
+
+            // Add SPI popup menu entries
+            if (component instanceof ContainerEntryGettable) {
+                Collection<Action> actions = api.getContextualActions(((ContainerEntryGettable)component).getEntry(), null);
+
+                if (actions != null) {
+                    addSeparator();
+
+                    for (Action action : actions) {
+                        if (action != null) {
+                            add(action);
+                        } else {
+                            addSeparator();
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public JTabbedPane getTabbedPane() {
+        return tabbedPane;
+    }
+
+    protected class SubMenuItemActionListener implements ActionListener {
+        protected int index;
+
+        public SubMenuItemActionListener(int index) {
+            this.index = index;
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            tabbedPane.setSelectedIndex(index);
+        }
+    }
+
+
+    // --- Popup menu actions --- //
+    public void removeComponent(Component component) {
+        tabbedPane.remove(component);
+        if (tabbedPane.getTabCount() == 0) {
+            cardLayout.show(this, "panel");
+        }
+    }
+
+    protected void removeOtherComponents(Component component) {
+        int i = tabbedPane.getTabCount();
+        while (i-- > 0) {
+            Component c = tabbedPane.getComponentAt(i);
+            if (c != component) {
+                tabbedPane.remove(i);
+            }
+        }
+        if (tabbedPane.getTabCount() == 0) {
+            cardLayout.show(this, "panel");
+        }
+    }
+
+    protected void removeAllComponents() {
+        tabbedPane.removeAll();
+        if (tabbedPane.getTabCount() == 0) {
+            cardLayout.show(this, "panel");
+        }
+    }
+
+    // --- PreferencesChangeListener --- //
+    @Override
+    public void preferencesChanged(Map<String, String> preferences) {
+        // Store preferences
+        this.preferences = preferences;
+        // Update layout
+        if ("true".equals(preferences.get(TAB_LAYOUT))) {
+            tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
+        } else {
+            tabbedPane.setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT);
+        }
+        setSelectedIndex(tabbedPane.getSelectedIndex());
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/component/panel/TreeTabbedPanel.java b/app/src/main/java/org/jd/gui/view/component/panel/TreeTabbedPanel.java
new file mode 100644
index 0000000..0d3af7f
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/component/panel/TreeTabbedPanel.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component.panel;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.*;
+import org.jd.gui.api.model.TreeNodeData;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.view.component.Tree;
+import org.jd.gui.view.renderer.TreeNodeRenderer;
+
+import javax.swing.*;
+import javax.swing.event.TreeExpansionEvent;
+import javax.swing.event.TreeExpansionListener;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreeNode;
+import javax.swing.tree.TreePath;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.Map;
+
+public class TreeTabbedPanel<T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> extends JPanel implements UriGettable, UriOpenable, PageChangeable, PageClosable, PreferencesChangeListener {
+    protected API api;
+    protected URI uri;
+    protected Tree tree;
+    protected TabbedPanel tabbedPanel;
+    protected ArrayList<PageChangeListener> pageChangedListeners = new ArrayList<>();
+    // Flags to prevent the event cascades
+    protected boolean updateTreeMenuEnabled = true;
+    protected boolean openUriEnabled = true;
+    protected boolean treeNodeChangedEnabled = true;
+
+    @SuppressWarnings("unchecked")
+    public TreeTabbedPanel(API api, URI uri) {
+        this.api = api;
+        this.uri = uri;
+
+        tree = new Tree();
+        tree.setShowsRootHandles(true);
+        tree.setMinimumSize(new Dimension(150, 10));
+        tree.setExpandsSelectedPaths(true);
+        tree.setCellRenderer(new TreeNodeRenderer() {
+            @Override
+            public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
+                // Always render the left tree with focus
+                return super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, true);
+            }
+        });
+        tree.addTreeSelectionListener(e -> treeNodeChanged((T)tree.getLastSelectedPathComponent()));
+        tree.addTreeExpansionListener(new TreeExpansionListener() {
+            @Override
+            public void treeExpanded(TreeExpansionEvent e) {
+                TreeNode node = (TreeNode)e.getPath().getLastPathComponent();
+                if (node instanceof TreeNodeExpandable) {
+                    TreeNodeExpandable tne = (TreeNodeExpandable)node;
+                    int oldHashCode = createHashCode(node.children());
+                    tne.populateTreeNode(api);
+                    int newHashCode = createHashCode(node.children());
+                    if (oldHashCode != newHashCode) {
+                        ((DefaultTreeModel)tree.getModel()).reload(node);
+                    }
+                }
+            }
+            @Override
+            public void treeCollapsed(TreeExpansionEvent e) {}
+        });
+        tree.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                if (SwingUtilities.isRightMouseButton(e)) {
+                    TreePath path = tree.getPathForLocation(e.getX(), e.getY());
+
+                    if (path != null) {
+                        tree.setSelectionPath(path);
+
+                        T node = (T)path.getLastPathComponent();
+                        Collection<Action> actions = api.getContextualActions(node.getEntry(), node.getUri().getFragment());
+
+                        if (actions != null) {
+                            JPopupMenu popup = new JPopupMenu();
+                            for (Action action : actions) {
+                                if (action != null) {
+                                    popup.add(action);
+                                } else {
+                                    popup.addSeparator();
+                                }
+                            }
+                            popup.show(e.getComponent(), e.getX(), e.getY());
+                        }
+                    }
+                }
+            }
+        });
+
+        tabbedPanel = new TabbedPanel(api);
+        tabbedPanel.setMinimumSize(new Dimension(150, 10));
+        tabbedPanel.tabbedPane.addChangeListener(e -> pageChanged());
+
+        setLayout(new BorderLayout());
+
+        JSplitPane splitter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new JScrollPane(tree), tabbedPanel);
+        splitter.setResizeWeight(0.2);
+
+        add(splitter, BorderLayout.CENTER);
+    }
+
+    protected static int createHashCode(Enumeration enumeration) {
+        int hashCode = 1;
+
+        while (enumeration.hasMoreElements()) {
+            hashCode *= 31;
+
+            Object element = enumeration.nextElement();
+
+            if (element != null) {
+                hashCode += element.hashCode();
+            }
+        }
+
+        return hashCode;
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void treeNodeChanged(T node) {
+        if (treeNodeChangedEnabled && (node != null)) {
+            try {
+                // Disable tabbedPane.changeListener
+                updateTreeMenuEnabled = false;
+
+                // Search base tree node
+                URI uri = node.getUri();
+
+                if ((uri.getFragment() == null) && (uri.getQuery() == null)) {
+                    showPage(uri, uri, node);
+                } else {
+                    URI baseUri = new URI(uri.getScheme(), uri.getHost(), uri.getPath(), null);
+                    T baseNode = node;
+
+                    while ((baseNode != null) && !baseNode.getUri().equals(baseUri)) {
+                        baseNode = (T)baseNode.getParent();
+                    }
+
+                    if ((baseNode != null) && baseNode.getUri().equals(baseUri)) {
+                        showPage(uri, baseUri, baseNode);
+                    }
+                }
+            } catch (URISyntaxException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            } finally {
+                // Enable tabbedPane.changeListener
+                updateTreeMenuEnabled = true;
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected <P extends JComponent & UriGettable> boolean showPage(URI uri, URI baseUri, DefaultMutableTreeNode baseNode) {
+        P page = (P)tabbedPanel.showPage(baseUri);
+
+        if ((page == null) && (baseNode instanceof PageCreator)) {
+            page = ((PageCreator)baseNode).createPage(api);
+            page.putClientProperty("node", baseNode);
+
+            String path = baseUri.getPath();
+            String label = path.substring(path.lastIndexOf('/')+1);
+            Object data = baseNode.getUserObject();
+
+            if (data instanceof TreeNodeData) {
+                TreeNodeData tnd = (TreeNodeData)data;
+                tabbedPanel.addPage(label, tnd.getIcon(), tnd.getTip(), page);
+            } else {
+                tabbedPanel.addPage(label, null, null, page);
+            }
+        }
+
+        if (openUriEnabled && page instanceof UriOpenable) {
+            ((UriOpenable)page).openUri(uri);
+        }
+
+        return (page != null);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected <P extends JComponent & UriGettable> void pageChanged() {
+        try {
+            // Disable highlight
+            openUriEnabled = false;
+
+            P page = (P)tabbedPanel.tabbedPane.getSelectedComponent();
+
+            if (updateTreeMenuEnabled) {
+                // Synchronize tree
+                if (page != null) {
+                    T node = (T)page.getClientProperty("node");
+                    // Select tree node
+                    TreePath treePath = new TreePath(node.getPath());
+                    tree.setSelectionPath(treePath);
+                    tree.scrollPathToVisible(treePath);
+                } else {
+                    tree.clearSelection();
+                }
+            }
+            // Fire page changed event
+            for (PageChangeListener listener : pageChangedListeners) {
+                listener.pageChanged(page);
+            }
+        } finally {
+            // Enable highlight
+            openUriEnabled = true;
+        }
+    }
+
+    // --- URIGetter --- //
+    @Override public URI getUri() { return uri; }
+
+    // --- URIOpener --- //
+    @Override
+    public boolean openUri(URI uri) {
+        try {
+            URI baseUri = new URI(uri.getScheme(), uri.getHost(), uri.getPath(), null);
+
+            if (this.uri.equals(baseUri)) {
+                return true;
+            } else {
+                DefaultMutableTreeNode node = searchTreeNode(baseUri, (DefaultMutableTreeNode) tree.getModel().getRoot());
+
+                if (showPage(uri, baseUri, node)) {
+                    DefaultMutableTreeNode childNode = searchTreeNode(uri, node);
+                    if (childNode != null) {
+                        node = childNode;
+                    }
+                }
+
+                if (node != null) {
+                    try {
+                        // Disable tree node changed listener
+                        treeNodeChangedEnabled = false;
+                        // Populate and expand node
+                        if (!(node instanceof PageCreator) && (node instanceof TreeNodeExpandable)) {
+                            ((TreeNodeExpandable) node).populateTreeNode(api);
+                            tree.expandPath(new TreePath(node.getPath()));
+                        }
+                        // Select tree node
+                        TreePath treePath = new TreePath(node.getPath());
+                        tree.setSelectionPath(treePath);
+                        tree.scrollPathToVisible(treePath);
+                    } finally {
+                        // Enable tree node changed listener
+                        treeNodeChangedEnabled = true;
+                    }
+                    return true;
+                }
+            }
+        } catch (URISyntaxException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        return false;
+    }
+
+    @SuppressWarnings("unchecked")
+    protected DefaultMutableTreeNode searchTreeNode(URI uri, DefaultMutableTreeNode node) {
+        if (node instanceof TreeNodeExpandable) {
+            ((TreeNodeExpandable)node).populateTreeNode(api);
+        }
+
+        String u = uri.toString();
+        T child = null;
+        Enumeration enumeration = node.children();
+
+        while (enumeration.hasMoreElements()) {
+            T element = (T)enumeration.nextElement();
+            String childU = element.getUri().toString();
+
+            if (u.length() > childU.length()) {
+                if (u.startsWith(childU)) {
+                    char c = u.charAt(childU.length());
+                    if ((c == '/') || (c == '!')) {
+                        child = element;
+                        break;
+                    }
+                }
+            } else if (u.equals(childU)) {
+                child = element;
+                break;
+            }
+        }
+
+        if (child != null) {
+            if (u.equals(child.getUri().toString())) {
+                return child;
+            } else {
+                // Parent tree node found -> Recursive call
+                return searchTreeNode(uri, child);
+            }
+        } else {
+            // Not found
+            return null;
+        }
+    }
+
+    // --- PageChanger --- //
+    @Override
+    public void addPageChangeListener(PageChangeListener listener) {
+        pageChangedListeners.add(listener);
+    }
+
+    // --- PageCloser --- //
+    @Override
+    public boolean closePage() {
+        Component component = tabbedPanel.tabbedPane.getSelectedComponent();
+
+        if (component != null) {
+            tabbedPanel.removeComponent(component);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    // --- PreferencesChangeListener --- //
+    @Override
+    @SuppressWarnings("unchecked")
+    public void preferencesChanged(Map<String, String> preferences) {
+        tabbedPanel.preferencesChanged(preferences);
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/renderer/OpenTypeListCellRenderer.java b/app/src/main/java/org/jd/gui/view/renderer/OpenTypeListCellRenderer.java
new file mode 100644
index 0000000..960e81d
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/renderer/OpenTypeListCellRenderer.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.renderer;
+
+import org.jd.gui.view.bean.OpenTypeListCellBean;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class OpenTypeListCellRenderer implements ListCellRenderer<OpenTypeListCellBean> {
+    protected Color textSelectionColor;
+    protected Color textNonSelectionColor;
+    protected Color infoSelectionColor;
+    protected Color infoNonSelectionColor;
+    protected Color backgroundSelectionColor;
+    protected Color backgroundNonSelectionColor;
+
+    protected JPanel panel;
+    protected JLabel label, info;
+
+    public OpenTypeListCellRenderer() {
+        textSelectionColor = UIManager.getColor("List.selectionForeground");
+        textNonSelectionColor = UIManager.getColor("List.foreground");
+        backgroundSelectionColor = UIManager.getColor("List.selectionBackground");
+        backgroundNonSelectionColor = UIManager.getColor("List.background");
+
+        infoSelectionColor = infoColor(textSelectionColor);
+        infoNonSelectionColor = infoColor(textNonSelectionColor);
+
+        panel = new JPanel(new BorderLayout());
+        panel.add(label = new JLabel(), BorderLayout.WEST);
+        panel.add(info = new JLabel(), BorderLayout.CENTER);
+    }
+
+    static protected Color infoColor(Color c) {
+        if (c.getRed() + c.getGreen() + c.getBlue() > (3*127)) {
+            return new Color(
+                    (int)((c.getRed()-127)  *0.7 + 127),
+                    (int)((c.getGreen()-127)*0.7 + 127),
+                    (int)((c.getBlue()-127) *0.7 + 127),
+                    c.getAlpha());
+        } else {
+            return new Color(
+                    (int)(127 - (127-c.getRed())  *0.7),
+                    (int)(127 - (127-c.getGreen())*0.7),
+                    (int)(127 - (127-c.getBlue()) *0.7),
+                    c.getAlpha());
+        }
+    }
+
+    @Override
+    public Component getListCellRendererComponent(JList<? extends OpenTypeListCellBean> list, OpenTypeListCellBean value, int index, boolean selected, boolean hasFocus) {
+        if (value != null) {
+            // Display first level item
+            label.setText(value.label);
+            label.setIcon(value.icon);
+
+            info.setText((value.packag != null) ? " - "+value.packag : "");
+
+            if (selected) {
+                label.setForeground(textSelectionColor);
+                info.setForeground(infoSelectionColor);
+                panel.setBackground(backgroundSelectionColor);
+            } else {
+                label.setForeground(textNonSelectionColor);
+                info.setForeground(infoNonSelectionColor);
+                panel.setBackground(backgroundNonSelectionColor);
+            }
+        } else {
+            label.setText(" ...");
+            label.setIcon(null);
+            info.setText("");
+            label.setForeground(textNonSelectionColor);
+            panel.setBackground(backgroundNonSelectionColor);
+        }
+
+        return panel;
+    }
+}
diff --git a/app/src/main/java/org/jd/gui/view/renderer/TreeNodeRenderer.java b/app/src/main/java/org/jd/gui/view/renderer/TreeNodeRenderer.java
new file mode 100644
index 0000000..5fbd3b1
--- /dev/null
+++ b/app/src/main/java/org/jd/gui/view/renderer/TreeNodeRenderer.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.renderer;
+
+import org.jd.gui.api.model.TreeNodeData;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.TreeCellRenderer;
+import java.awt.*;
+
+public class TreeNodeRenderer implements TreeCellRenderer {
+    protected Color textSelectionColor;
+    protected Color backgroundSelectionColor;
+    protected Color textNonSelectionColor;
+    protected Color backgroundNonSelectionColor;
+    protected Color textDisabledColor;
+    protected Color backgroundDisabledColor;
+
+    protected JPanel panel;
+    protected JLabel icon, label;
+
+    public TreeNodeRenderer() {
+        panel = new JPanel(new BorderLayout());
+        panel.add(icon = new JLabel(), BorderLayout.WEST);
+        panel.add(label = new JLabel(), BorderLayout.CENTER);
+        panel.setOpaque(false);
+
+        textSelectionColor = UIManager.getColor("Tree.selectionForeground");
+        backgroundSelectionColor = UIManager.getColor("Tree.selectionBackground");
+        textNonSelectionColor = UIManager.getColor("Tree.textForeground");
+        backgroundNonSelectionColor = UIManager.getColor("Tree.textBackground");
+        textDisabledColor = UIManager.getColor("Tree.disabledText");
+        backgroundDisabledColor = UIManager.getColor("Tree.disabled");
+        Insets margins = UIManager.getInsets("Tree.rendererMargins");
+
+        icon.setForeground(textNonSelectionColor);
+        icon.setOpaque(false);
+        icon.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 2));
+
+        label.setOpaque(false);
+
+        if (margins != null) {
+            label.setBorder(BorderFactory.createEmptyBorder(margins.top, margins.left, margins.bottom, margins.right));
+        } else {
+            label.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 4));
+        }
+    }
+
+    @Override
+    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
+        Object data = ((DefaultMutableTreeNode)value).getUserObject();
+
+        if (data instanceof TreeNodeData) {
+            TreeNodeData tnd = (TreeNodeData)data;
+            icon.setIcon(expanded && (tnd.getOpenIcon() != null) ? tnd.getOpenIcon() : tnd.getIcon());
+            label.setText(tnd.getLabel());
+        } else {
+            icon.setIcon(null);
+            label.setText("" + data);
+        }
+
+        if (selected) {
+            if (hasFocus) {
+                label.setForeground(textSelectionColor);
+                label.setBackground(backgroundSelectionColor);
+            } else {
+                label.setForeground(textDisabledColor);
+                label.setBackground(backgroundDisabledColor);
+            }
+            label.setOpaque(true);
+        } else {
+            label.setForeground(textNonSelectionColor);
+            label.setOpaque(false);
+        }
+
+        return panel;
+    }
+}
diff --git a/app/src/main/resources/META-INF/services/org.jd.gui.spi.PanelFactory b/app/src/main/resources/META-INF/services/org.jd.gui.spi.PanelFactory
new file mode 100644
index 0000000..91df551
--- /dev/null
+++ b/app/src/main/resources/META-INF/services/org.jd.gui.spi.PanelFactory
@@ -0,0 +1 @@
+org.jd.gui.service.mainpanel.ContainerPanelFactoryProvider
diff --git a/app/src/main/resources/META-INF/services/org.jd.gui.spi.PreferencesPanel b/app/src/main/resources/META-INF/services/org.jd.gui.spi.PreferencesPanel
new file mode 100644
index 0000000..3f669ad
--- /dev/null
+++ b/app/src/main/resources/META-INF/services/org.jd.gui.spi.PreferencesPanel
@@ -0,0 +1,2 @@
+org.jd.gui.service.preferencespanel.UISingleInstancePreferencesProvider
+org.jd.gui.service.preferencespanel.UITabsPreferencesProvider
diff --git a/app/src/main/resources/org/jd/gui/images/backward_nav.png b/app/src/main/resources/org/jd/gui/images/backward_nav.png
new file mode 100644
index 0000000..8b022d1
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/backward_nav.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/close.gif b/app/src/main/resources/org/jd/gui/images/close.gif
new file mode 100644
index 0000000..17b7573
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/close.gif differ
diff --git a/app/src/main/resources/org/jd/gui/images/close_active.gif b/app/src/main/resources/org/jd/gui/images/close_active.gif
new file mode 100644
index 0000000..8816521
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/close_active.gif differ
diff --git a/app/src/main/resources/org/jd/gui/images/copy.png b/app/src/main/resources/org/jd/gui/images/copy.png
new file mode 100644
index 0000000..bb5451c
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/copy.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/forward_nav.png b/app/src/main/resources/org/jd/gui/images/forward_nav.png
new file mode 100644
index 0000000..a6afdee
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/forward_nav.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/generate_class.png b/app/src/main/resources/org/jd/gui/images/generate_class.png
new file mode 100644
index 0000000..904ee07
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/generate_class.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/generate_int.png b/app/src/main/resources/org/jd/gui/images/generate_int.png
new file mode 100644
index 0000000..82a5d80
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/generate_int.png differ
diff --git a/jd_icon_128.png b/app/src/main/resources/org/jd/gui/images/jd_icon_128.png
old mode 100755
new mode 100644
similarity index 100%
rename from jd_icon_128.png
rename to app/src/main/resources/org/jd/gui/images/jd_icon_128.png
diff --git a/app/src/main/resources/org/jd/gui/images/jd_icon_32.png b/app/src/main/resources/org/jd/gui/images/jd_icon_32.png
new file mode 100644
index 0000000..2d5cfab
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/jd_icon_32.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/jd_icon_64.png b/app/src/main/resources/org/jd/gui/images/jd_icon_64.png
new file mode 100644
index 0000000..2e457df
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/jd_icon_64.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/next_nav.png b/app/src/main/resources/org/jd/gui/images/next_nav.png
new file mode 100644
index 0000000..0328357
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/next_nav.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/open.png b/app/src/main/resources/org/jd/gui/images/open.png
new file mode 100644
index 0000000..0430baa
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/open.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/open_type.png b/app/src/main/resources/org/jd/gui/images/open_type.png
new file mode 100644
index 0000000..f0ba8f8
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/open_type.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/paste.png b/app/src/main/resources/org/jd/gui/images/paste.png
new file mode 100644
index 0000000..b66e9dd
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/paste.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/preferences.png b/app/src/main/resources/org/jd/gui/images/preferences.png
new file mode 100644
index 0000000..1c23d0e
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/preferences.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/prev_nav.png b/app/src/main/resources/org/jd/gui/images/prev_nav.png
new file mode 100644
index 0000000..830c2c9
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/prev_nav.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/save.png b/app/src/main/resources/org/jd/gui/images/save.png
new file mode 100644
index 0000000..5f0fb5b
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/save.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/save_all.png b/app/src/main/resources/org/jd/gui/images/save_all.png
new file mode 100644
index 0000000..eeea6be
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/save_all.png differ
diff --git a/app/src/main/resources/org/jd/gui/images/search_src.png b/app/src/main/resources/org/jd/gui/images/search_src.png
new file mode 100644
index 0000000..c549447
Binary files /dev/null and b/app/src/main/resources/org/jd/gui/images/search_src.png differ
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..5163340
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,188 @@
+buildscript {
+    repositories {
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.netflix.nebula:gradle-ospackage-plugin:5.3.0'    // RPM & DEB support
+        classpath 'edu.sc.seis.gradle:launch4j:2.4.4'
+        classpath 'net.sf.proguard:proguard-gradle:6.1.0'
+    }
+}
+
+apply plugin: 'java'
+apply plugin: 'distribution'
+apply plugin: 'edu.sc.seis.launch4j'
+apply plugin: 'nebula.ospackage'
+
+// Common configuration //
+rootProject.version='1.6.6'
+rootProject.ext.set('jdCoreVersion', '1.1.3')
+targetCompatibility = '1.8'
+
+allprojects {
+    apply plugin: 'eclipse'
+    apply plugin: 'idea'
+
+    tasks.withType(JavaCompile) {
+        sourceCompatibility = targetCompatibility = '1.8'
+        options.compilerArgs << '-Xlint:deprecation'
+        options.compilerArgs << '-Xlint:unchecked'
+        options.encoding = 'UTF-8'
+    }
+
+    repositories {
+        jcenter()
+    }
+
+    configurations {
+        provided
+        compile.extendsFrom provided
+    }
+}
+
+// 'cleanIdea' task extension //
+cleanIdea.doFirst {
+    delete project.name + '.iws'
+    delete 'out'
+    followSymlinks = true
+}
+
+// All in one JAR file //
+subprojects.each { subproject ->
+    evaluationDependsOn(subproject.path)
+}
+
+jar {
+    dependsOn subprojects.tasks['jar']
+
+    // Add SPI directory
+    def tmpSpiDir = file('build/tmp/spi')
+    from tmpSpiDir
+    // Add dependencies
+    def deps = []
+    subprojects.each { subproject ->
+        from subproject.sourceSets.main.output.classesDirs
+        from subproject.sourceSets.main.output.resourcesDir
+        deps += subproject.configurations.runtime - subproject.configurations.provided
+    }
+    subprojects.each { subproject ->
+        deps -= subproject.jar.archivePath
+    }
+    deps = deps.unique().collect { it.isDirectory() ? it : zipTree(it) }
+    from deps
+
+    manifest {
+        attributes 'Main-Class': 'org.jd.gui.App',
+                'SplashScreen-Image': 'org/jd/gui/images/jd_icon_128.png',
+                'JD-GUI-Version': project.version,
+                'JD-Core-Version': project.jdCoreVersion
+    }
+    exclude 'META-INF/licenses/**', 'META-INF/maven/**', 'META-INF/INDEX.LIST'
+    exclude '**/ErrorStrip_*.properties', '**/RSyntaxTextArea_*.properties', '**/RTextArea_*.properties'
+    exclude '**/FocusableTip_*.properties', '**/RSyntaxTextArea_License.txt'
+    duplicatesStrategy DuplicatesStrategy.EXCLUDE
+    doFirst {
+        // Create SPI directory
+        tmpSpiDir.deleteDir()
+        def tmpSpiServicesDir = file(tmpSpiDir.path + '/META-INF/services')
+        tmpSpiServicesDir.mkdirs()
+        // Copy and merge SPI config files
+        subprojects.each { subproject ->
+            def servicesDir = file(subproject.sourceSets.main.output.resourcesDir.path + '/META-INF/services')
+            if (servicesDir.exists()) {
+                servicesDir.eachFile { serviceFile ->
+                    def target = file(tmpSpiServicesDir.path + '/' + serviceFile.name)
+                    target << serviceFile.text
+                }
+            }
+        }
+    }
+}
+
+// Minify JAR file //
+task proguard(type: proguard.gradle.ProGuardTask, dependsOn: 'jar') {
+    configuration 'src/proguard/resources/proguard.config.txt'
+    injars jar.archivePath
+    outjars 'build/libs/' + project.name + '-' + project.version + '-min.jar'
+    libraryjars System.getProperty('java.home') + '/lib/rt.jar'
+    libraryjars System.getProperty('java.home') + '/jmods/'
+}
+
+// Java executable wrapper for Windows //
+launch4j {
+    createExe.dependsOn 'proguard'
+
+    version = textVersion = project.version
+    fileDescription = productName = 'JD-GUI'
+    errTitle 'JD-GUI Windows Wrapper'
+    copyright 'JD-GUI (C) 2008-2019 Emmanuel Dupuy'
+    icon projectDir.path + '/src/launch4j/resources/images/jd-gui.ico'
+    jar projectDir.path + '/' + proguard.outJarFiles[0]
+    bundledJrePath = '%JAVA_HOME%'
+}
+
+// Packages for Linux //
+ospackage {
+    buildDeb.dependsOn 'proguard'
+    buildRpm.dependsOn 'proguard'
+
+    license = file('LICENSE')
+    maintainer 'Emmanuel Dupuy <emmanue1@users.noreply.github.com>'
+    os LINUX
+    packageDescription 'JD-GUI, a standalone graphical utility that displays Java sources from CLASS files'
+    packageGroup 'java'
+    packageName project.name
+    release '0'
+    summary 'A Java Decompiler'
+    url 'https://github.com/java-decompiler/jd-gui'
+
+    into '/opt/' + project.name
+    from (proguard.outJarFiles[0]) {
+        fileMode 0755
+    }
+    from ('src/linux/resources/') {
+        fileMode 0755
+    }
+    from 'LICENSE', 'NOTICE', 'README.md'
+
+    postInstall 'cd /opt/' + project.name + '; ln -s ./' + file(proguard.outJarFiles[0]).name + ' ./jd-gui.jar; xdg-icon-resource install --size 128 --novendor ./jd_icon_128.png jd-gui; xdg-desktop-menu install ./*.desktop'
+    preUninstall 'cd /opt/' + project.name + '; rm -f ./jd-gui.jar; rm -fr ./ext; xdg-desktop-menu uninstall ./*.desktop'
+}
+
+// Distributions for OSX and Windows //
+distributions {
+    osx.contents {
+        into('JD-GUI.app/Contents') {
+            from('src/osx/resources') {
+                include 'Info.plist'
+                expand VERSION: project.version,
+                       JAR: file(proguard.outJarFiles[0]).name
+            }
+        }
+        into('JD-GUI.app/Contents/MacOS') {
+            from('src/osx/resources') {
+                include 'universalJavaApplicationStub.sh'
+                fileMode 0755
+            }
+        }
+        into('JD-GUI.app/Contents/Resources/Java') {
+            from proguard.outJarFiles[0]
+        }
+        from 'LICENSE', 'NOTICE', 'README.md'
+    }
+    windows.contents {
+        from 'build/launch4j/jd-gui.exe'
+        from 'LICENSE', 'NOTICE', 'README.md'
+    }
+
+    installWindowsDist.dependsOn createExe
+    windowsDistTar.dependsOn createExe
+    windowsDistZip.dependsOn createExe
+
+    installOsxDist.dependsOn 'proguard'
+    osxDistTar.dependsOn 'proguard'
+    osxDistZip.dependsOn 'proguard'
+}
+
+build.finalizedBy buildDeb
+build.finalizedBy buildRpm
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..b5166da
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..9a6bf73
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Mar 02 11:11:32 CET 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100755
index 0000000..aec9973
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/jd-gui-1.6.3-min.jar b/jd-gui-1.6.3-min.jar
deleted file mode 100755
index 6380e26..0000000
Binary files a/jd-gui-1.6.3-min.jar and /dev/null differ
diff --git a/services/build.gradle b/services/build.gradle
new file mode 100644
index 0000000..6ed884b
--- /dev/null
+++ b/services/build.gradle
@@ -0,0 +1,67 @@
+apply plugin: 'java'
+
+dependencies {
+    compile 'com.fifesoft:rsyntaxtextarea:3.0.4'
+    compile 'org.ow2.asm:asm:7.1'
+    compile 'org.jd:jd-core:' + parent.jdCoreVersion
+    compile project(':api')
+    testCompile 'junit:junit:4.12'
+}
+
+version = parent.version
+
+// ANTLR //
+ext.antlr4 = [
+        antlrSource: 'src/main/antlr',
+        destinationDir: 'src-generated/antlr/java',
+        grammarPackage: 'org.jd.gui.util.parser.antlr'
+]
+
+configurations {
+    antlr4 {
+        description = "ANTLR4"
+    }
+}
+
+dependencies {
+    compile 'org.antlr:antlr4-runtime:4.5'
+    antlr4 'org.antlr:antlr4:4.5'
+}
+
+task antlr4OutputDir() {
+    mkdir antlr4.destinationDir
+}
+
+task antlr4GenerateGrammarSource(dependsOn: antlr4OutputDir, type: JavaExec) {
+    description = 'Generates Java sources from ANTLR4 grammars.'
+
+    inputs.dir file(antlr4.antlrSource)
+    outputs.dir file(antlr4.destinationDir)
+
+    def grammars = fileTree(antlr4.antlrSource).include('**/*.g4')
+    def pkg = antlr4.grammarPackage.replaceAll("\\.", "/")
+
+    main = 'org.antlr.v4.Tool'
+    classpath = configurations.antlr4
+    args = ['-o', "${antlr4.destinationDir}/${pkg}", '-package', antlr4.grammarPackage, grammars.files].flatten()
+}
+
+compileJava {
+    dependsOn antlr4GenerateGrammarSource
+    source antlr4.destinationDir
+}
+
+clean {
+    delete 'src-generated'
+}
+
+idea.module {
+    sourceDirs += file(antlr4.destinationDir)
+}
+ideaModule.dependsOn antlr4GenerateGrammarSource
+
+eclipse.classpath.file.withXml { xml ->
+    def node = xml.asNode()
+    node.appendNode( 'classpathentry', [ kind: 'src', path: antlr4.destinationDir])
+}
+eclipseClasspath.dependsOn antlr4GenerateGrammarSource
diff --git a/services/src/main/antlr/Java.g4 b/services/src/main/antlr/Java.g4
new file mode 100644
index 0000000..2997ff2
--- /dev/null
+++ b/services/src/main/antlr/Java.g4
@@ -0,0 +1,1020 @@
+/*
+ [The "BSD licence"]
+ Copyright (c) 2013 Terence Parr, Sam Harwell
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+ 3. The name of the author may not be used to endorse or promote products
+    derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/** A Java 1.7 grammar for ANTLR v4 derived from ANTLR v3 Java grammar.
+ *  Uses ANTLR v4's left-recursive expression notation.
+ *  It parses ECJ, Netbeans, JDK etc...
+ *
+ *  Sam Harwell cleaned this up significantly and updated to 1.7!
+ *
+ *  You can test with
+ *
+ *  $ antlr4 Java.g4
+ *  $ javac *.java
+ *  $ grun Java compilationUnit *.java
+ */
+grammar Java;
+
+// starting point for parsing a java file
+compilationUnit
+    :   packageDeclaration? importDeclaration* typeDeclaration* EOF
+    ;
+
+packageDeclaration
+    :   annotation* 'package' qualifiedName ';'
+    ;
+
+importDeclaration
+    :   'import' 'static'? qualifiedName ('.' '*')? ';'
+    ;
+
+typeDeclaration
+    :   classOrInterfaceModifier* classDeclaration
+    |   classOrInterfaceModifier* enumDeclaration
+    |   classOrInterfaceModifier* interfaceDeclaration
+    |   classOrInterfaceModifier* annotationTypeDeclaration
+    |   ';'
+    ;
+
+modifier
+    :   classOrInterfaceModifier
+    |   (   'native'
+        |   'synchronized'
+        |   'transient'
+        |   'volatile'
+        )
+    ;
+
+classOrInterfaceModifier
+    :   annotation       // class or interface
+    |   (   'public'     // class or interface
+        |   'protected'  // class or interface
+        |   'private'    // class or interface
+        |   'static'     // class or interface
+        |   'abstract'   // class or interface
+        |   'final'      // class only -- does not apply to interfaces
+        |   'strictfp'   // class or interface
+        )
+    ;
+
+variableModifier
+    :   'final'
+    |   annotation
+    ;
+
+classDeclaration
+    :   'class' Identifier typeParameters?
+        ('extends' type)?
+        ('implements' typeList)?
+        classBody
+    ;
+
+typeParameters
+    :   '<' typeParameter (',' typeParameter)* '>'
+    ;
+
+typeParameter
+    :   Identifier ('extends' typeBound)?
+    ;
+
+typeBound
+    :   type ('&' type)*
+    ;
+
+enumDeclaration
+    :   ENUM Identifier ('implements' typeList)?
+        '{' enumConstants? ','? enumBodyDeclarations? '}'
+    ;
+
+enumConstants
+    :   enumConstant (',' enumConstant)*
+    ;
+
+enumConstant
+    :   annotation* Identifier arguments? classBody?
+    ;
+
+enumBodyDeclarations
+    :   ';' classBodyDeclaration*
+    ;
+
+interfaceDeclaration
+    :   'interface' Identifier typeParameters? ('extends' typeList)? interfaceBody
+    ;
+
+typeList
+    :   type (',' type)*
+    ;
+
+classBody
+    :   '{' classBodyDeclaration* '}'
+    ;
+
+interfaceBody
+    :   '{' interfaceBodyDeclaration* '}'
+    ;
+
+classBodyDeclaration
+    :   ';'
+    |   'static'? block
+    |   modifier* memberDeclaration
+    ;
+
+memberDeclaration
+    :   methodDeclaration
+    |   genericMethodDeclaration
+    |   fieldDeclaration
+    |   constructorDeclaration
+    |   genericConstructorDeclaration
+    |   interfaceDeclaration
+    |   annotationTypeDeclaration
+    |   classDeclaration
+    |   enumDeclaration
+    ;
+
+/* We use rule this even for void methods which cannot have [] after parameters.
+   This simplifies grammar and we can consider void to be a type, which
+   renders the [] matching as a context-sensitive issue or a semantic check
+   for invalid return type after parsing.
+ */
+methodDeclaration
+    :   (type|'void') Identifier formalParameters ('[' ']')*
+        ('throws' qualifiedNameList)?
+        (   methodBody
+        |   ';'
+        )
+    ;
+
+genericMethodDeclaration
+    :   typeParameters methodDeclaration
+    ;
+
+constructorDeclaration
+    :   Identifier formalParameters ('throws' qualifiedNameList)?
+        constructorBody
+    ;
+
+genericConstructorDeclaration
+    :   typeParameters constructorDeclaration
+    ;
+
+fieldDeclaration
+    :   type variableDeclarators ';'
+    ;
+
+interfaceBodyDeclaration
+    :   modifier* interfaceMemberDeclaration
+    |   ';'
+    ;
+
+interfaceMemberDeclaration
+    :   constDeclaration
+    |   interfaceMethodDeclaration
+    |   genericInterfaceMethodDeclaration
+    |   interfaceDeclaration
+    |   annotationTypeDeclaration
+    |   classDeclaration
+    |   enumDeclaration
+    ;
+
+constDeclaration
+    :   type constantDeclarator (',' constantDeclarator)* ';'
+    ;
+
+constantDeclarator
+    :   Identifier ('[' ']')* '=' variableInitializer
+    ;
+
+// see matching of [] comment in methodDeclaratorRest
+interfaceMethodDeclaration
+    :   (type|'void') Identifier formalParameters ('[' ']')*
+        ('throws' qualifiedNameList)?
+        ';'
+    ;
+
+genericInterfaceMethodDeclaration
+    :   typeParameters interfaceMethodDeclaration
+    ;
+
+variableDeclarators
+    :   variableDeclarator (',' variableDeclarator)*
+    ;
+
+variableDeclarator
+    :   variableDeclaratorId ('=' variableInitializer)?
+    ;
+
+variableDeclaratorId
+    :   Identifier ('[' ']')*
+    ;
+
+variableInitializer
+    :   arrayInitializer
+    |   expression
+    ;
+
+arrayInitializer
+    :   '{' (variableInitializer (',' variableInitializer)* (',')? )? '}'
+    ;
+
+enumConstantName
+    :   Identifier
+    ;
+
+type
+    :   classOrInterfaceType ('[' ']')*
+    |   primitiveType ('[' ']')*
+    ;
+
+classOrInterfaceType
+    :   Identifier typeArguments? ('.' Identifier typeArguments? )*
+    ;
+
+primitiveType
+    :   'boolean'
+    |   'char'
+    |   'byte'
+    |   'short'
+    |   'int'
+    |   'long'
+    |   'float'
+    |   'double'
+    ;
+
+typeArguments
+    :   '<' typeArgument (',' typeArgument)* '>'
+    ;
+
+typeArgument
+    :   type
+    |   '?' (('extends' | 'super') type)?
+    ;
+
+qualifiedNameList
+    :   qualifiedName (',' qualifiedName)*
+    ;
+
+formalParameters
+    :   '(' formalParameterList? ')'
+    ;
+
+formalParameterList
+    :   formalParameter (',' formalParameter)* (',' lastFormalParameter)?
+    |   lastFormalParameter
+    ;
+
+formalParameter
+    :   variableModifier* type variableDeclaratorId
+    ;
+
+lastFormalParameter
+    :   variableModifier* type '...' variableDeclaratorId
+    ;
+
+methodBody
+    :   block
+    ;
+
+constructorBody
+    :   block
+    ;
+
+qualifiedName
+    :   Identifier ('.' Identifier)*
+    ;
+
+literal
+    :   IntegerLiteral
+    |   FloatingPointLiteral
+    |   CharacterLiteral
+    |   StringLiteral
+    |   BooleanLiteral
+    |   'null'
+    ;
+
+// ANNOTATIONS
+
+annotation
+    :   '@' annotationName ( '(' ( elementValuePairs | elementValue )? ')' )?
+    ;
+
+annotationName : qualifiedName ;
+
+elementValuePairs
+    :   elementValuePair (',' elementValuePair)*
+    ;
+
+elementValuePair
+    :   Identifier '=' elementValue
+    ;
+
+elementValue
+    :   expression
+    |   annotation
+    |   elementValueArrayInitializer
+    ;
+
+elementValueArrayInitializer
+    :   '{' (elementValue (',' elementValue)*)? (',')? '}'
+    ;
+
+annotationTypeDeclaration
+    :   '@' 'interface' Identifier annotationTypeBody
+    ;
+
+annotationTypeBody
+    :   '{' (annotationTypeElementDeclaration)* '}'
+    ;
+
+annotationTypeElementDeclaration
+    :   modifier* annotationTypeElementRest
+    |   ';' // this is not allowed by the grammar, but apparently allowed by the actual compiler
+    ;
+
+annotationTypeElementRest
+    :   type annotationMethodOrConstantRest ';'
+    |   classDeclaration ';'?
+    |   interfaceDeclaration ';'?
+    |   enumDeclaration ';'?
+    |   annotationTypeDeclaration ';'?
+    ;
+
+annotationMethodOrConstantRest
+    :   annotationMethodRest
+    |   annotationConstantRest
+    ;
+
+annotationMethodRest
+    :   Identifier '(' ')' defaultValue?
+    ;
+
+annotationConstantRest
+    :   variableDeclarators
+    ;
+
+defaultValue
+    :   'default' elementValue
+    ;
+
+// STATEMENTS / BLOCKS
+
+block
+    :   '{' blockStatement* '}'
+    ;
+
+blockStatement
+    :   localVariableDeclarationStatement
+    |   statement
+    |   typeDeclaration
+    ;
+
+localVariableDeclarationStatement
+    :    localVariableDeclaration ';'
+    ;
+
+localVariableDeclaration
+    :   variableModifier* type variableDeclarators
+    ;
+
+statement
+    :   block
+    |   ASSERT expression (':' expression)? ';'
+    |   'if' parExpression statement ('else' statement)?
+    |   'for' '(' forControl ')' statement
+    |   'while' parExpression statement
+    |   'do' statement 'while' parExpression ';'
+    |   'try' block (catchClause+ finallyBlock? | finallyBlock)
+    |   'try' resourceSpecification block catchClause* finallyBlock?
+    |   'switch' parExpression '{' switchBlockStatementGroup* switchLabel* '}'
+    |   'synchronized' parExpression block
+    |   'return' expression? ';'
+    |   'throw' expression ';'
+    |   'break' Identifier? ';'
+    |   'continue' Identifier? ';'
+    |   ';'
+    |   statementExpression ';'
+    |   Identifier ':' statement
+    ;
+
+catchClause
+    :   'catch' '(' variableModifier* catchType Identifier ')' block
+    ;
+
+catchType
+    :   qualifiedName ('|' qualifiedName)*
+    ;
+
+finallyBlock
+    :   'finally' block
+    ;
+
+resourceSpecification
+    :   '(' resources ';'? ')'
+    ;
+
+resources
+    :   resource (';' resource)*
+    ;
+
+resource
+    :   variableModifier* classOrInterfaceType variableDeclaratorId '=' expression
+    ;
+
+/** Matches cases then statements, both of which are mandatory.
+ *  To handle empty cases at the end, we add switchLabel* to statement.
+ */
+switchBlockStatementGroup
+    :   switchLabel+ blockStatement+
+    ;
+
+switchLabel
+    :   'case' constantExpression ':'
+    |   'case' enumConstantName ':'
+    |   'default' ':'
+    ;
+
+forControl
+    :   enhancedForControl
+    |   forInit? ';' expression? ';' forUpdate?
+    ;
+
+forInit
+    :   localVariableDeclaration
+    |   expressionList
+    ;
+
+enhancedForControl
+    :   variableModifier* type variableDeclaratorId ':' expression
+    ;
+
+forUpdate
+    :   expressionList
+    ;
+
+// EXPRESSIONS
+
+parExpression
+    :   '(' expression ')'
+    ;
+
+expressionList
+    :   expression (',' expression)*
+    ;
+
+statementExpression
+    :   expression
+    ;
+
+constantExpression
+    :   expression
+    ;
+
+expression
+    :   primary
+    |   expression '.' Identifier
+    |   expression '.' 'this'
+    |   expression '.' 'new' nonWildcardTypeArguments? innerCreator
+    |   expression '.' 'super' superSuffix
+    |   expression '.' explicitGenericInvocation
+    |   expression '[' expression ']'
+    |   expression '(' expressionList? ')'
+    |   'new' creator
+    |   '(' type ')' expression
+    |   expression ('++' | '--')
+    |   ('+'|'-'|'++'|'--') expression
+    |   ('~'|'!') expression
+    |   expression ('*'|'/'|'%') expression
+    |   expression ('+'|'-') expression
+    |   expression ('<' '<' | '>' '>' '>' | '>' '>') expression
+    |   expression ('<=' | '>=' | '>' | '<') expression
+    |   expression 'instanceof' type
+    |   expression ('==' | '!=') expression
+    |   expression '&' expression
+    |   expression '^' expression
+    |   expression '|' expression
+    |   expression '&&' expression
+    |   expression '||' expression
+    |   expression '?' expression ':' expression
+    |   <assoc=right> expression
+        (   '='
+        |   '+='
+        |   '-='
+        |   '*='
+        |   '/='
+        |   '&='
+        |   '|='
+        |   '^='
+        |   '>>='
+        |   '>>>='
+        |   '<<='
+        |   '%='
+        )
+        expression
+    ;
+
+primary
+    :   '(' expression ')'
+    |   'this'
+    |   'super'
+    |   literal
+    |   Identifier
+    |   type '.' 'class'
+    |   'void' '.' 'class'
+    |   nonWildcardTypeArguments (explicitGenericInvocationSuffix | 'this' arguments)
+    ;
+
+creator
+    :   nonWildcardTypeArguments createdName classCreatorRest
+    |   createdName (arrayCreatorRest | classCreatorRest)
+    ;
+
+createdName
+    :   Identifier typeArgumentsOrDiamond? ('.' Identifier typeArgumentsOrDiamond?)*
+    |   primitiveType
+    ;
+
+innerCreator
+    :   Identifier nonWildcardTypeArgumentsOrDiamond? classCreatorRest
+    ;
+
+arrayCreatorRest
+    :   '['
+        (   ']' ('[' ']')* arrayInitializer
+        |   expression ']' ('[' expression ']')* ('[' ']')*
+        )
+    ;
+
+classCreatorRest
+    :   arguments classBody?
+    ;
+
+explicitGenericInvocation
+    :   nonWildcardTypeArguments explicitGenericInvocationSuffix
+    ;
+
+nonWildcardTypeArguments
+    :   '<' typeList '>'
+    ;
+
+typeArgumentsOrDiamond
+    :   '<' '>'
+    |   typeArguments
+    ;
+
+nonWildcardTypeArgumentsOrDiamond
+    :   '<' '>'
+    |   nonWildcardTypeArguments
+    ;
+
+superSuffix
+    :   arguments
+    |   '.' Identifier arguments?
+    ;
+
+explicitGenericInvocationSuffix
+    :   'super' superSuffix
+    |   Identifier arguments
+    ;
+
+arguments
+    :   '(' expressionList? ')'
+    ;
+
+// LEXER
+
+// §3.9 Keywords
+
+ABSTRACT      : 'abstract';
+ASSERT        : 'assert';
+BOOLEAN       : 'boolean';
+BREAK         : 'break';
+BYTE          : 'byte';
+CASE          : 'case';
+CATCH         : 'catch';
+CHAR          : 'char';
+CLASS         : 'class';
+CONST         : 'const';
+CONTINUE      : 'continue';
+DEFAULT       : 'default';
+DO            : 'do';
+DOUBLE        : 'double';
+ELSE          : 'else';
+ENUM          : 'enum';
+EXTENDS       : 'extends';
+FINAL         : 'final';
+FINALLY       : 'finally';
+FLOAT         : 'float';
+FOR           : 'for';
+IF            : 'if';
+GOTO          : 'goto';
+IMPLEMENTS    : 'implements';
+IMPORT        : 'import';
+INSTANCEOF    : 'instanceof';
+INT           : 'int';
+INTERFACE     : 'interface';
+LONG          : 'long';
+NATIVE        : 'native';
+NEW           : 'new';
+PACKAGE       : 'package';
+PRIVATE       : 'private';
+PROTECTED     : 'protected';
+PUBLIC        : 'public';
+RETURN        : 'return';
+SHORT         : 'short';
+STATIC        : 'static';
+STRICTFP      : 'strictfp';
+SUPER         : 'super';
+SWITCH        : 'switch';
+SYNCHRONIZED  : 'synchronized';
+THIS          : 'this';
+THROW         : 'throw';
+THROWS        : 'throws';
+TRANSIENT     : 'transient';
+TRY           : 'try';
+VOID          : 'void';
+VOLATILE      : 'volatile';
+WHILE         : 'while';
+
+// §3.10.1 Integer Literals
+
+IntegerLiteral
+    :   DecimalIntegerLiteral
+    |   HexIntegerLiteral
+    |   OctalIntegerLiteral
+    |   BinaryIntegerLiteral
+    ;
+
+fragment
+DecimalIntegerLiteral
+    :   DecimalNumeral IntegerTypeSuffix?
+    ;
+
+fragment
+HexIntegerLiteral
+    :   HexNumeral IntegerTypeSuffix?
+    ;
+
+fragment
+OctalIntegerLiteral
+    :   OctalNumeral IntegerTypeSuffix?
+    ;
+
+fragment
+BinaryIntegerLiteral
+    :   BinaryNumeral IntegerTypeSuffix?
+    ;
+
+fragment
+IntegerTypeSuffix
+    :   [lL]
+    ;
+
+fragment
+DecimalNumeral
+    :   '0'
+    |   NonZeroDigit (Digits? | Underscores Digits)
+    ;
+
+fragment
+Digits
+    :   Digit (DigitOrUnderscore* Digit)?
+    ;
+
+fragment
+Digit
+    :   '0'
+    |   NonZeroDigit
+    ;
+
+fragment
+NonZeroDigit
+    :   [1-9]
+    ;
+
+fragment
+DigitOrUnderscore
+    :   Digit
+    |   '_'
+    ;
+
+fragment
+Underscores
+    :   '_'+
+    ;
+
+fragment
+HexNumeral
+    :   '0' [xX] HexDigits
+    ;
+
+fragment
+HexDigits
+    :   HexDigit (HexDigitOrUnderscore* HexDigit)?
+    ;
+
+fragment
+HexDigit
+    :   [0-9a-fA-F]
+    ;
+
+fragment
+HexDigitOrUnderscore
+    :   HexDigit
+    |   '_'
+    ;
+
+fragment
+OctalNumeral
+    :   '0' Underscores? OctalDigits
+    ;
+
+fragment
+OctalDigits
+    :   OctalDigit (OctalDigitOrUnderscore* OctalDigit)?
+    ;
+
+fragment
+OctalDigit
+    :   [0-7]
+    ;
+
+fragment
+OctalDigitOrUnderscore
+    :   OctalDigit
+    |   '_'
+    ;
+
+fragment
+BinaryNumeral
+    :   '0' [bB] BinaryDigits
+    ;
+
+fragment
+BinaryDigits
+    :   BinaryDigit (BinaryDigitOrUnderscore* BinaryDigit)?
+    ;
+
+fragment
+BinaryDigit
+    :   [01]
+    ;
+
+fragment
+BinaryDigitOrUnderscore
+    :   BinaryDigit
+    |   '_'
+    ;
+
+// §3.10.2 Floating-Point Literals
+
+FloatingPointLiteral
+    :   DecimalFloatingPointLiteral
+    |   HexadecimalFloatingPointLiteral
+    ;
+
+fragment
+DecimalFloatingPointLiteral
+    :   Digits '.' Digits? ExponentPart? FloatTypeSuffix?
+    |   '.' Digits ExponentPart? FloatTypeSuffix?
+    |   Digits ExponentPart FloatTypeSuffix?
+    |   Digits FloatTypeSuffix
+    ;
+
+fragment
+ExponentPart
+    :   ExponentIndicator SignedInteger
+    ;
+
+fragment
+ExponentIndicator
+    :   [eE]
+    ;
+
+fragment
+SignedInteger
+    :   Sign? Digits
+    ;
+
+fragment
+Sign
+    :   [+-]
+    ;
+
+fragment
+FloatTypeSuffix
+    :   [fFdD]
+    ;
+
+fragment
+HexadecimalFloatingPointLiteral
+    :   HexSignificand BinaryExponent FloatTypeSuffix?
+    ;
+
+fragment
+HexSignificand
+    :   HexNumeral '.'?
+    |   '0' [xX] HexDigits? '.' HexDigits
+    ;
+
+fragment
+BinaryExponent
+    :   BinaryExponentIndicator SignedInteger
+    ;
+
+fragment
+BinaryExponentIndicator
+    :   [pP]
+    ;
+
+// §3.10.3 Boolean Literals
+
+BooleanLiteral
+    :   'true'
+    |   'false'
+    ;
+
+// §3.10.4 Character Literals
+
+CharacterLiteral
+    :   '\'' SingleCharacter '\''
+    |   '\'' EscapeSequence '\''
+    ;
+
+fragment
+SingleCharacter
+    :   ~['\\]
+    ;
+
+// §3.10.5 String Literals
+
+StringLiteral
+    :   '"' StringCharacters? '"'
+    ;
+
+fragment
+StringCharacters
+    :   StringCharacter+
+    ;
+
+fragment
+StringCharacter
+    :   ~["\\]
+    |   EscapeSequence
+    ;
+
+// §3.10.6 Escape Sequences for Character and String Literals
+
+fragment
+EscapeSequence
+    :   '\\' [btnfr"'\\]
+    |   OctalEscape
+    |   UnicodeEscape
+    ;
+
+fragment
+OctalEscape
+    :   '\\' OctalDigit
+    |   '\\' OctalDigit OctalDigit
+    |   '\\' ZeroToThree OctalDigit OctalDigit
+    ;
+
+fragment
+UnicodeEscape
+    :   '\\' 'u' HexDigit HexDigit HexDigit HexDigit
+    ;
+
+fragment
+ZeroToThree
+    :   [0-3]
+    ;
+
+// §3.10.7 The Null Literal
+
+NullLiteral
+    :   'null'
+    ;
+
+// §3.11 Separators
+
+LPAREN          : '(';
+RPAREN          : ')';
+LBRACE          : '{';
+RBRACE          : '}';
+LBRACK          : '[';
+RBRACK          : ']';
+SEMI            : ';';
+COMMA           : ',';
+DOT             : '.';
+
+// §3.12 Operators
+
+ASSIGN          : '=';
+GT              : '>';
+LT              : '<';
+BANG            : '!';
+TILDE           : '~';
+QUESTION        : '?';
+COLON           : ':';
+EQUAL           : '==';
+LE              : '<=';
+GE              : '>=';
+NOTEQUAL        : '!=';
+AND             : '&&';
+OR              : '||';
+INC             : '++';
+DEC             : '--';
+ADD             : '+';
+SUB             : '-';
+MUL             : '*';
+DIV             : '/';
+BITAND          : '&';
+BITOR           : '|';
+CARET           : '^';
+MOD             : '%';
+
+ADD_ASSIGN      : '+=';
+SUB_ASSIGN      : '-=';
+MUL_ASSIGN      : '*=';
+DIV_ASSIGN      : '/=';
+AND_ASSIGN      : '&=';
+OR_ASSIGN       : '|=';
+XOR_ASSIGN      : '^=';
+MOD_ASSIGN      : '%=';
+LSHIFT_ASSIGN   : '<<=';
+RSHIFT_ASSIGN   : '>>=';
+URSHIFT_ASSIGN  : '>>>=';
+
+// §3.8 Identifiers (must appear after all keywords in the grammar)
+
+Identifier
+    :   JavaLetter JavaLetterOrDigit*
+    ;
+
+fragment
+JavaLetter
+    :   [a-zA-Z$_] // these are the "java letters" below 0xFF
+    |   // covers all characters above 0xFF which are not a surrogate
+        ~[\u0000-\u00FF\uD800-\uDBFF]
+        {Character.isJavaIdentifierStart(_input.LA(-1))}?
+    |   // covers UTF-16 surrogate pairs encodings for U+10000 to U+10FFFF
+        [\uD800-\uDBFF] [\uDC00-\uDFFF]
+        {Character.isJavaIdentifierStart(Character.toCodePoint((char)_input.LA(-2), (char)_input.LA(-1)))}?
+    ;
+
+fragment
+JavaLetterOrDigit
+    :   [a-zA-Z0-9$_] // these are the "java letters or digits" below 0xFF
+    |   // covers all characters above 0xFF which are not a surrogate
+        ~[\u0000-\u00FF\uD800-\uDBFF]
+        {Character.isJavaIdentifierPart(_input.LA(-1))}?
+    |   // covers UTF-16 surrogate pairs encodings for U+10000 to U+10FFFF
+        [\uD800-\uDBFF] [\uDC00-\uDFFF]
+        {Character.isJavaIdentifierPart(Character.toCodePoint((char)_input.LA(-2), (char)_input.LA(-1)))}?
+    ;
+
+//
+// Additional symbols not defined in the lexical specification
+//
+
+AT : '@';
+ELLIPSIS : '...';
+
+//
+// Whitespace and comments
+//
+
+WS  :  [ \t\r\n\u000C]+ -> skip
+    ;
+
+COMMENT
+    :   '/*' .*? '*/' -> skip
+    ;
+
+LINE_COMMENT
+    :   '//' ~[\r\n]* -> skip
+    ;
diff --git a/services/src/main/java/org/fife/ui/rtextarea/Marker.java b/services/src/main/java/org/fife/ui/rtextarea/Marker.java
new file mode 100644
index 0000000..0fd7fb8
--- /dev/null
+++ b/services/src/main/java/org/fife/ui/rtextarea/Marker.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.fife.ui.rtextarea;
+
+import org.fife.ui.rsyntaxtextarea.DocumentRange;
+
+import java.util.List;
+
+/*
+ * An utility class to call the restricted access methods of 'RTextArea'.
+ *
+ * JD-GUI uses two workarounds for RSyntaxTextArea:
+ * - org.fife.ui.rtextarea.Marker
+ * - org.jd.gui.view.component.RoundMarkErrorStrip
+ */
+public class Marker {
+    public static void markAll(RTextArea textArea, List<DocumentRange> ranges) {
+        textArea.markAll(ranges);
+    }
+
+    public static void clearMarkAllHighlights(RTextArea textArea) {
+        textArea.clearMarkAllHighlights();
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/model/container/ContainerEntryComparator.java b/services/src/main/java/org/jd/gui/model/container/ContainerEntryComparator.java
new file mode 100644
index 0000000..cd56d51
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/model/container/ContainerEntryComparator.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.model.container;
+
+import org.jd.gui.api.model.Container;
+
+import java.util.Comparator;
+
+/**
+ * Directories before files, sorted by path
+ */
+public class ContainerEntryComparator implements Comparator<Container.Entry> {
+    public static final ContainerEntryComparator COMPARATOR = new ContainerEntryComparator();
+
+    public int compare(Container.Entry e1, Container.Entry e2) {
+        if (e1.isDirectory()) {
+            if (!e2.isDirectory()) {
+                return -1;
+            }
+        } else {
+            if (e2.isDirectory()) {
+                return 1;
+            }
+        }
+        return e1.getPath().compareTo(e2.getPath());
+    }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/jd/gui/model/container/EarContainer.java b/services/src/main/java/org/jd/gui/model/container/EarContainer.java
new file mode 100644
index 0000000..bff9332
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/model/container/EarContainer.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.model.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+
+import java.nio.file.Path;
+
+public class EarContainer extends GenericContainer {
+    public EarContainer(API api, Container.Entry parentEntry, Path rootPath) {
+        super(api, parentEntry, rootPath);
+    }
+
+    public String getType() { return "ear"; }
+}
diff --git a/services/src/main/java/org/jd/gui/model/container/GenericContainer.java b/services/src/main/java/org/jd/gui/model/container/GenericContainer.java
new file mode 100644
index 0000000..3477eab
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/model/container/GenericContainer.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.model.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.spi.ContainerFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+
+public class GenericContainer implements Container {
+    protected static final long TIMESTAMP = System.currentTimeMillis();
+
+    protected static long tmpFileCounter = 0;
+
+    protected API api;
+    protected int rootNameCount;
+    protected Container.Entry root;
+
+    public GenericContainer(API api, Container.Entry parentEntry, Path rootPath) {
+        try {
+            URI uri = parentEntry.getUri();
+
+            this.api = api;
+            this.rootNameCount = rootPath.getNameCount();
+            this.root = new Entry(parentEntry, rootPath, new URI(uri.getScheme(), uri.getHost(), uri.getPath() + "!/", null)) {
+                public Entry newChildEntry(Path fsPath) {
+                    return new Entry(parent, fsPath, null);
+                }
+            };
+        } catch (URISyntaxException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    public String getType() { return "generic"; }
+    public Container.Entry getRoot() { return root; }
+
+    protected class Entry implements Container.Entry {
+        protected Container.Entry parent;
+        protected Path fsPath;
+        protected String strPath;
+        protected URI uri;
+        protected Boolean isDirectory;
+        protected Collection<Container.Entry> children;
+
+        public Entry(Container.Entry parent, Path fsPath, URI uri) {
+            this.parent = parent;
+            this.fsPath = fsPath;
+            this.strPath = null;
+            this.uri = uri;
+            this.isDirectory = null;
+            this.children = null;
+        }
+
+        public Entry newChildEntry(Path fsPath) { return new Entry(this, fsPath, null); }
+
+        public Container getContainer() { return GenericContainer.this; }
+        public Container.Entry getParent() { return parent; }
+
+        public URI getUri() {
+            if (uri == null) {
+                try {
+                    URI rootUri = root.getUri();
+                    uri = new URI(rootUri.getScheme(), rootUri.getHost(), rootUri.getPath() + getPath(), null);
+                } catch (URISyntaxException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            }
+            return uri;
+        }
+
+        public String getPath() {
+            if (strPath == null) {
+                int nameCount = fsPath.getNameCount();
+
+                if (rootNameCount == nameCount) {
+                    strPath = "";
+                } else {
+                    strPath = fsPath.subpath(rootNameCount, nameCount).toString().replace(fsPath.getFileSystem().getSeparator(), "/");
+
+                    int strPathLength = strPath.length();
+
+                    if ((strPathLength > 0) && strPath.charAt(strPathLength-1) == '/') {
+                        // Cut last separator
+                        strPath = strPath.substring(0, strPathLength-1);
+                    }
+                }
+            }
+            return strPath;
+        }
+
+        public boolean isDirectory() {
+            if (isDirectory == null) {
+                isDirectory = Boolean.valueOf(Files.isDirectory(fsPath));
+            }
+            return isDirectory;
+        }
+
+        public long length() {
+            try {
+                return Files.size(fsPath);
+            } catch (IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+                return -1L;
+            }
+        }
+
+        public InputStream getInputStream() {
+            try {
+                return Files.newInputStream(fsPath);
+            } catch (IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+                return null;
+            }
+        }
+
+        public Collection<Container.Entry> getChildren() {
+            if (children == null) {
+                try {
+                    if (Files.isDirectory(fsPath)) {
+                        children = loadChildrenFromDirectoryEntry();
+                    } else {
+                        children = loadChildrenFromFileEntry();
+                    }
+                } catch (IOException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            }
+            return children;
+        }
+
+        protected Collection<Container.Entry> loadChildrenFromDirectoryEntry() throws IOException {
+            try (DirectoryStream<Path> stream = Files.newDirectoryStream(fsPath)) {
+                ArrayList<Container.Entry> children = new ArrayList<>();
+                int parentNameCount = fsPath.getNameCount();
+
+                for (Path subPath : stream) {
+                    if (subPath.getNameCount() > parentNameCount) {
+                        children.add(newChildEntry(subPath));
+                    }
+                }
+
+                children.sort(ContainerEntryComparator.COMPARATOR);
+                return Collections.unmodifiableCollection(children);
+            }
+        }
+
+        protected Collection<Container.Entry> loadChildrenFromFileEntry() throws IOException {
+            StringBuilder suffix = new StringBuilder(".").append(TIMESTAMP).append('.').append(tmpFileCounter++).append('.').append(fsPath.getFileName().toString());
+            File tmpFile = File.createTempFile("jd-gui.tmp.", suffix.toString());
+            Path tmpPath = Paths.get(tmpFile.toURI());
+
+            tmpFile.delete();
+            tmpFile.deleteOnExit();
+            Files.copy(fsPath, tmpPath);
+
+            FileSystem subFileSystem = FileSystems.newFileSystem(tmpPath, null);
+
+            if (subFileSystem != null) {
+                Iterator<Path> rootDirectories = subFileSystem.getRootDirectories().iterator();
+
+                if (rootDirectories.hasNext()) {
+                    Path rootPath = rootDirectories.next();
+                    ContainerFactory containerFactory = api.getContainerFactory(rootPath);
+
+                    if (containerFactory != null) {
+                        Container container = containerFactory.make(api, this, rootPath);
+
+                        if (container != null) {
+                            return container.getRoot().getChildren();
+                        }
+                    }
+                }
+            }
+
+            tmpFile.delete();
+            return Collections.emptyList();
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/model/container/JarContainer.java b/services/src/main/java/org/jd/gui/model/container/JarContainer.java
new file mode 100644
index 0000000..e19a210
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/model/container/JarContainer.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.model.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+
+import java.nio.file.Path;
+
+public class JarContainer extends GenericContainer {
+    public JarContainer(API api, Container.Entry parentEntry, Path rootPath) {
+        super(api, parentEntry, rootPath);
+    }
+
+    public String getType() { return "jar"; }
+}
diff --git a/services/src/main/java/org/jd/gui/model/container/JavaModuleContainer.java b/services/src/main/java/org/jd/gui/model/container/JavaModuleContainer.java
new file mode 100644
index 0000000..7c5aa59
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/model/container/JavaModuleContainer.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.model.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+
+import java.nio.file.Path;
+
+public class JavaModuleContainer extends GenericContainer {
+    public JavaModuleContainer(API api, Container.Entry parentEntry, Path rootPath) {
+        super(api, parentEntry, rootPath);
+    }
+
+    public String getType() { return "jmod"; }
+}
diff --git a/services/src/main/java/org/jd/gui/model/container/KarContainer.java b/services/src/main/java/org/jd/gui/model/container/KarContainer.java
new file mode 100644
index 0000000..10712e0
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/model/container/KarContainer.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.model.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+
+import java.nio.file.Path;
+
+public class KarContainer extends GenericContainer {
+    public KarContainer(API api, Container.Entry parentEntry, Path rootPath) {
+        super(api, parentEntry, rootPath);
+    }
+
+    public String getType() { return "kar"; }
+}
diff --git a/services/src/main/java/org/jd/gui/model/container/WarContainer.java b/services/src/main/java/org/jd/gui/model/container/WarContainer.java
new file mode 100644
index 0000000..2edae7a
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/model/container/WarContainer.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.model.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+
+import java.nio.file.Path;
+
+public class WarContainer extends GenericContainer {
+    public WarContainer(API api, Container.Entry parentEntry, Path rootPath) {
+        super(api, parentEntry, rootPath);
+    }
+
+    public String getType() { return "war"; }
+}
diff --git a/services/src/main/java/org/jd/gui/service/actions/CopyQualifiedNameContextualActionsFactory.java b/services/src/main/java/org/jd/gui/service/actions/CopyQualifiedNameContextualActionsFactory.java
new file mode 100644
index 0000000..89eb680
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/actions/CopyQualifiedNameContextualActionsFactory.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.actions;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Type;
+import org.jd.gui.spi.ContextualActionsFactory;
+import org.jd.gui.spi.TypeFactory;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.datatransfer.StringSelection;
+import java.awt.event.ActionEvent;
+import java.util.Collection;
+import java.util.Collections;
+
+public class CopyQualifiedNameContextualActionsFactory implements ContextualActionsFactory {
+
+    public Collection<Action> make(API api, Container.Entry entry, String fragment) {
+        return Collections.<Action>singletonList(new CopyQualifiedNameAction(api, entry, fragment));
+    }
+
+    public static class CopyQualifiedNameAction extends AbstractAction {
+        protected static final ImageIcon ICON = new ImageIcon(CopyQualifiedNameAction.class.getClassLoader().getResource("org/jd/gui/images/cpyqual_menu.png"));
+
+        protected API api;
+        protected Container.Entry entry;
+        protected String fragment;
+
+        public CopyQualifiedNameAction(API api, Container.Entry entry, String fragment) {
+            this.api = api;
+            this.entry = entry;
+            this.fragment = fragment;
+
+            putValue(GROUP_NAME, "Edit > CutCopyPaste");
+            putValue(NAME, "Copy Qualified Name");
+            putValue(SMALL_ICON, ICON);
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            TypeFactory typeFactory = api.getTypeFactory(entry);
+
+            if (typeFactory != null) {
+                Type type = typeFactory.make(api, entry, fragment);
+
+                if (type != null) {
+                    StringBuilder sb = new StringBuilder(type.getDisplayPackageName());
+
+                    if (sb.length() > 0) {
+                        sb.append('.');
+                    }
+
+                    sb.append(type.getDisplayTypeName());
+
+                    if (fragment != null) {
+                        int dashIndex = fragment.indexOf('-');
+
+                        if (dashIndex != -1) {
+                            int lastDashIndex = fragment.lastIndexOf('-');
+
+                            if (dashIndex == lastDashIndex) {
+                                // See jd.gui.api.feature.UriOpenable
+                                throw new InvalidFormatException("fragment: " + fragment);
+                            } else {
+                                String name = fragment.substring(dashIndex + 1, lastDashIndex);
+                                String descriptor = fragment.substring(lastDashIndex + 1);
+
+                                if (descriptor.startsWith("(")) {
+                                    for (Type.Method method : type.getMethods()) {
+                                        if (method.getName().equals(name) && method.getDescriptor().equals(descriptor)) {
+                                            sb.append('.').append(method.getDisplayName());
+                                            break;
+                                        }
+                                    }
+                                } else {
+                                    for (Type.Field field : type.getFields()) {
+                                        if (field.getName().equals(name) && field.getDescriptor().equals(descriptor)) {
+                                            sb.append('.').append(field.getDisplayName());
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+
+                    Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(sb.toString()), null);
+                    return;
+                }
+            }
+
+            // Create qualified name from URI
+            String path = entry.getUri().getPath();
+            String rootPath = entry.getContainer().getRoot().getUri().getPath();
+            String qualifiedName = path.substring(rootPath.length()).replace('/', '.');
+
+            if (qualifiedName.endsWith(".class")) {
+                qualifiedName = qualifiedName.substring(0, qualifiedName.length()-6);
+            }
+
+            Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(qualifiedName), null);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/actions/InvalidFormatException.java b/services/src/main/java/org/jd/gui/service/actions/InvalidFormatException.java
new file mode 100644
index 0000000..092c160
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/actions/InvalidFormatException.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.actions;
+
+public class InvalidFormatException extends RuntimeException{
+    public InvalidFormatException(String message) { super(message); }
+}
diff --git a/services/src/main/java/org/jd/gui/service/container/EarContainerFactoryProvider.java b/services/src/main/java/org/jd/gui/service/container/EarContainerFactoryProvider.java
new file mode 100644
index 0000000..f7bc3ae
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/container/EarContainerFactoryProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.model.container.EarContainer;
+import org.jd.gui.spi.ContainerFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+
+public class EarContainerFactoryProvider implements ContainerFactory {
+    @Override
+    public String getType() { return "ear"; }
+
+    @Override
+    public boolean accept(API api, Path rootPath) {
+        if (rootPath.toUri().toString().toLowerCase().endsWith(".ear!/")) {
+            return true;
+        } else {
+            // Extension: accept uncompressed EAR file containing a folder 'META-INF/application.xml'
+            try {
+                return rootPath.getFileSystem().provider().getScheme().equals("file") && Files.exists(rootPath.resolve("META-INF/application.xml"));
+            } catch (InvalidPathException e) {
+                assert ExceptionUtil.printStackTrace(e);
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public Container make(API api, Container.Entry parentEntry, Path rootPath) {
+        return new EarContainer(api, parentEntry, rootPath);
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/container/GenericContainerFactoryProvider.java b/services/src/main/java/org/jd/gui/service/container/GenericContainerFactoryProvider.java
new file mode 100644
index 0000000..bb3faec
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/container/GenericContainerFactoryProvider.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.model.container.GenericContainer;
+import org.jd.gui.spi.ContainerFactory;
+
+import java.nio.file.Path;
+
+public class GenericContainerFactoryProvider implements ContainerFactory {
+    @Override
+    public String getType() { return "generic"; }
+
+    @Override
+    public boolean accept(API api, Path rootPath) { return true; }
+
+    @Override
+    public Container make(API api, Container.Entry parentEntry, Path rootPath) {
+        return new GenericContainer(api, parentEntry, rootPath);
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/container/JarContainerFactoryProvider.java b/services/src/main/java/org/jd/gui/service/container/JarContainerFactoryProvider.java
new file mode 100644
index 0000000..d078403
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/container/JarContainerFactoryProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.model.container.JarContainer;
+import org.jd.gui.spi.ContainerFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+
+public class JarContainerFactoryProvider implements ContainerFactory {
+    @Override
+    public String getType() { return "jar"; }
+
+    @Override
+    public boolean accept(API api, Path rootPath) {
+        if (rootPath.toUri().toString().toLowerCase().endsWith(".jar!/")) {
+            // Specification: http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html
+            return true;
+        } else {
+            // Extension: accept uncompressed JAR file containing a folder 'META-INF'
+            try {
+                return rootPath.getFileSystem().provider().getScheme().equals("file") && Files.exists(rootPath.resolve("META-INF"));
+            } catch (InvalidPathException e) {
+                assert ExceptionUtil.printStackTrace(e);
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public Container make(API api, Container.Entry parentEntry, Path rootPath) {
+        return new JarContainer(api, parentEntry, rootPath);
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/container/JavaModuleContainerFactoryProvider.java b/services/src/main/java/org/jd/gui/service/container/JavaModuleContainerFactoryProvider.java
new file mode 100644
index 0000000..bbd7229
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/container/JavaModuleContainerFactoryProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.model.container.JavaModuleContainer;
+import org.jd.gui.spi.ContainerFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+
+public class JavaModuleContainerFactoryProvider implements ContainerFactory {
+    @Override
+    public String getType() { return "jmod"; }
+
+    @Override
+    public boolean accept(API api, Path rootPath) {
+        if (rootPath.toUri().toString().toLowerCase().endsWith(".jmod!/")) {
+            return true;
+        } else {
+            // Extension: accept uncompressed JMOD file containing a folder 'classes'
+            try {
+                return rootPath.getFileSystem().provider().getScheme().equals("file") && Files.exists(rootPath.resolve("classes"));
+            } catch (InvalidPathException e) {
+                assert ExceptionUtil.printStackTrace(e);
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public Container make(API api, Container.Entry parentEntry, Path rootPath) {
+        return new JavaModuleContainer(api, parentEntry, rootPath);
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/container/KarContainerFactoryProvider.java b/services/src/main/java/org/jd/gui/service/container/KarContainerFactoryProvider.java
new file mode 100644
index 0000000..3d60071
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/container/KarContainerFactoryProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.model.container.KarContainer;
+import org.jd.gui.spi.ContainerFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+
+public class KarContainerFactoryProvider implements ContainerFactory {
+    @Override
+    public String getType() { return "kar"; }
+
+    @Override
+    public boolean accept(API api, Path rootPath) {
+        if (rootPath.toUri().toString().toLowerCase().endsWith(".kar!/")) {
+            return true;
+        } else {
+            // Extension: accept uncompressed KAR file containing a folder 'repository'
+            try {
+                return rootPath.getFileSystem().provider().getScheme().equals("file") && Files.exists(rootPath.resolve("repository"));
+            } catch (InvalidPathException e) {
+                assert ExceptionUtil.printStackTrace(e);
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public Container make(API api, Container.Entry parentEntry, Path rootPath) {
+        return new KarContainer(api, parentEntry, rootPath);
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/container/WarContainerFactoryProvider.java b/services/src/main/java/org/jd/gui/service/container/WarContainerFactoryProvider.java
new file mode 100644
index 0000000..5b5bec2
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/container/WarContainerFactoryProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.container;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.model.container.WarContainer;
+import org.jd.gui.spi.ContainerFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+
+public class WarContainerFactoryProvider implements ContainerFactory {
+    @Override
+    public String getType() { return "war"; }
+
+    @Override
+    public boolean accept(API api, Path rootPath) {
+        if (rootPath.toUri().toString().toLowerCase().endsWith(".war!/")) {
+            return true;
+        } else {
+            // Extension: accept uncompressed WAR file containing a folder 'WEB-INF'
+            try {
+                return rootPath.getFileSystem().provider().getScheme().equals("file") && Files.exists(rootPath.resolve("WEB-INF"));
+            } catch (InvalidPathException e) {
+                assert ExceptionUtil.printStackTrace(e);
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public Container make(API api, Container.Entry parentEntry, Path rootPath) {
+        return new WarContainer(api, parentEntry, rootPath);
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/AarFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/AarFileLoaderProvider.java
new file mode 100644
index 0000000..cce4ea1
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/AarFileLoaderProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+
+import java.io.File;
+
+public class AarFileLoaderProvider extends ZipFileLoaderProvider {
+    protected static final String[] EXTENSIONS = { "aar" };
+
+    @Override public String[] getExtensions() { return EXTENSIONS; }
+    @Override public String getDescription() { return "Android archive files (*.aar)"; }
+
+    @Override
+    public boolean accept(API api, File file) {
+        return file.exists() && file.isFile() && file.canRead() && file.getName().toLowerCase().endsWith(".aar");
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/AbstractFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/AbstractFileLoaderProvider.java
new file mode 100644
index 0000000..f5694c7
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/AbstractFileLoaderProvider.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.TreeNodeData;
+import org.jd.gui.spi.ContainerFactory;
+import org.jd.gui.spi.FileLoader;
+import org.jd.gui.spi.PanelFactory;
+import org.jd.gui.spi.TreeNodeFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.swing.*;
+import java.io.*;
+import java.net.URI;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+
+public abstract class AbstractFileLoaderProvider implements FileLoader {
+    protected <T extends JComponent & UriGettable> T load(API api, File file, Path rootPath) {
+        ContainerEntry parentEntry = new ContainerEntry(file);
+        ContainerFactory containerFactory = api.getContainerFactory(rootPath);
+
+        if (containerFactory != null) {
+            Container container = containerFactory.make(api, parentEntry, rootPath);
+
+            if (container != null) {
+                parentEntry.setChildren(container.getRoot().getChildren());
+
+                PanelFactory panelFactory = api.getMainPanelFactory(container);
+
+                if (panelFactory != null) {
+                    T mainPanel = panelFactory.make(api, container);
+
+                    if (mainPanel != null) {
+                        TreeNodeFactory treeNodeFactory = api.getTreeNodeFactory(parentEntry);
+                        Object data = (treeNodeFactory != null) ? treeNodeFactory.make(api, parentEntry).getUserObject() : null;
+                        Icon icon = (data instanceof TreeNodeData) ? ((TreeNodeData)data).getIcon() : null;
+                        String location = file.getPath();
+
+                        api.addPanel(file.getName(), icon, "Location: " + location, mainPanel);
+                        return mainPanel;
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    protected static class ContainerEntry implements Container.Entry {
+        protected static final Container PARENT_CONTAINER = new Container() {
+            @Override public String getType() { return "generic"; }
+            @Override public Container.Entry getRoot() { return null; }
+        };
+
+        protected Collection<Container.Entry> children = Collections.emptyList();
+        protected File file;
+        protected URI uri;
+        protected String path;
+
+        public ContainerEntry(File file) {
+            this.file = file;
+            this.uri = file.toURI();
+            this.path = uri.getPath();
+
+            if (path.endsWith("/")) {
+                path = path.substring(0, path.length() - 1);
+            }
+        }
+
+        @Override public Container getContainer() { return PARENT_CONTAINER; }
+        @Override public Container.Entry getParent() { return null; }
+        @Override public URI getUri() { return uri; }
+        @Override public String getPath() { return path; }
+        @Override public boolean isDirectory() { return file.isDirectory(); }
+        @Override public long length() { return file.length(); }
+        @Override public Collection<Container.Entry> getChildren() { return children; }
+
+        @Override
+        public InputStream getInputStream() {
+            try {
+                return new BufferedInputStream(new FileInputStream(file));
+            } catch (FileNotFoundException e) {
+                assert ExceptionUtil.printStackTrace(e);
+                return null;
+            }
+        }
+
+        public void setChildren(Collection<Container.Entry> children) {
+            this.children = children;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/AbstractTypeFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/AbstractTypeFileLoaderProvider.java
new file mode 100644
index 0000000..f6d0c6d
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/AbstractTypeFileLoaderProvider.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.UriOpenable;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.swing.*;
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Paths;
+
+public abstract class AbstractTypeFileLoaderProvider extends AbstractFileLoaderProvider {
+    protected boolean load(API api, File file, String pathInFile) {
+        // Search root path
+        String pathSuffix = pathInFile;
+        String path = file.getPath();
+
+        while (! path.endsWith(pathSuffix)) {
+            int index = pathSuffix.indexOf(File.separator);
+
+            if (index == -1) {
+                pathSuffix = "";
+            } else {
+                pathSuffix = pathSuffix.substring(index+1);
+            }
+        }
+
+        if (! pathSuffix.isEmpty()) {
+            // Init root file
+            File rootFile = file;
+            int index = pathSuffix.indexOf(File.separator);
+
+            while (index != -1) {
+                rootFile = rootFile.getParentFile();
+                pathSuffix = pathSuffix.substring(index+1);
+                index = pathSuffix.indexOf(File.separator);
+            }
+            rootFile = rootFile.getParentFile();
+
+            // Create panel
+            JComponent mainPanel = load(api, rootFile, Paths.get(rootFile.toURI()));
+
+            if (mainPanel instanceof UriOpenable) {
+                try {
+                    // Open page
+                    pathSuffix = file.getAbsolutePath().substring(rootFile.getAbsolutePath().length()).replace(File.separator, "/");
+                    URI rootUri = rootFile.toURI();
+                    URI uri = new URI(rootUri.getScheme(), rootUri.getHost(), rootUri.getPath() + '!' + pathSuffix, null);
+                    ((UriOpenable)mainPanel).openUri(uri);
+                    return true;
+                } catch (URISyntaxException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            } else {
+                return mainPanel != null;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/ClassFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/ClassFileLoaderProvider.java
new file mode 100644
index 0000000..68ffdee
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/ClassFileLoaderProvider.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.objectweb.asm.ClassReader;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+public class ClassFileLoaderProvider extends AbstractTypeFileLoaderProvider {
+    protected static final String[] EXTENSIONS = { "class" };
+
+    @Override public String[] getExtensions() { return EXTENSIONS; }
+    @Override public String getDescription() { return "Class files (*.class)"; }
+
+    @Override
+    public boolean accept(API api, File file) {
+        return file.exists() && file.isFile() && file.canRead() && file.getName().toLowerCase().endsWith(".class");
+    }
+
+    @Override
+    public boolean load(API api, File file) {
+        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
+            ClassReader classReader = new ClassReader(bis);
+            String pathInFile = classReader.getClassName().replace("/", File.separator) + ".class";
+
+            return load(api, file, pathInFile);
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+            return false;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/EarFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/EarFileLoaderProvider.java
new file mode 100644
index 0000000..58bfcf3
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/EarFileLoaderProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+
+import java.io.File;
+
+public class EarFileLoaderProvider extends ZipFileLoaderProvider {
+    protected static final String[] EXTENSIONS = { "ear" };
+
+    @Override public String[] getExtensions() { return EXTENSIONS; }
+    @Override public String getDescription() { return "Enterprise application archive files (*.ear)"; }
+
+    @Override
+    public boolean accept(API api, File file) {
+        return file.exists() && file.isFile() && file.canRead() && file.getName().toLowerCase().endsWith(".ear");
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/JarFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/JarFileLoaderProvider.java
new file mode 100644
index 0000000..bf51960
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/JarFileLoaderProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+
+import java.io.File;
+
+public class JarFileLoaderProvider extends ZipFileLoaderProvider {
+    protected static final String[] EXTENSIONS = { "jar" };
+
+    @Override public String[] getExtensions() { return EXTENSIONS; }
+    @Override public String getDescription() { return "Java archive files (*.jar)"; }
+
+    @Override
+    public boolean accept(API api, File file) {
+        return file.exists() && file.isFile() && file.canRead() && file.getName().toLowerCase().endsWith(".jar");
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/JavaFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/JavaFileLoaderProvider.java
new file mode 100644
index 0000000..a6411ab
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/JavaFileLoaderProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+import org.jd.gui.util.io.TextReader;
+
+import java.io.File;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class JavaFileLoaderProvider extends AbstractTypeFileLoaderProvider {
+    protected static final String[] EXTENSIONS = { "java" };
+
+    @Override public String[] getExtensions() { return EXTENSIONS; }
+    @Override public String getDescription() { return "Java files (*.java)"; }
+
+    @Override
+    public boolean accept(API api, File file) {
+        return file.exists() && file.isFile() && file.canRead() && file.getName().toLowerCase().endsWith(".java");
+    }
+
+    @Override
+    public boolean load(API api, File file) {
+        String text = TextReader.getText(file);
+        Pattern pattern = Pattern.compile("(?s)(.*\\s)?package\\s+(\\S+)\\s*;.*");
+        Matcher matcher = pattern.matcher(text);
+
+        if (matcher.matches()) {
+            // Package name found
+            String pathInFile = matcher.group(2).replace(".", File.separator) + File.separator + file.getName();
+
+            return load(api, file, pathInFile);
+        } else {
+            // Package name not found
+            return load(api, file, file.getName());
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/JavaModuleFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/JavaModuleFileLoaderProvider.java
new file mode 100644
index 0000000..e30eeee
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/JavaModuleFileLoaderProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+
+import java.io.File;
+
+public class JavaModuleFileLoaderProvider extends ZipFileLoaderProvider {
+    protected static final String[] EXTENSIONS = { "jmod" };
+
+    @Override public String[] getExtensions() { return EXTENSIONS; }
+    @Override public String getDescription() { return "Java module files (*.jmod)"; }
+
+    @Override
+    public boolean accept(API api, File file) {
+        return file.exists() && file.isFile() && file.canRead() && file.getName().toLowerCase().endsWith(".jmod");
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/KarFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/KarFileLoaderProvider.java
new file mode 100644
index 0000000..f3eeffc
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/KarFileLoaderProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+
+import java.io.File;
+
+public class KarFileLoaderProvider extends ZipFileLoaderProvider {
+    protected static final String[] EXTENSIONS = { "kar" };
+
+    @Override public String[] getExtensions() { return EXTENSIONS; }
+    @Override public String getDescription() { return "Karaf archive files (*.kar)"; }
+
+    @Override
+    public boolean accept(API api, File file) {
+        return file.exists() && file.isFile() && file.canRead() && file.getName().toLowerCase().endsWith(".kar");
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/LogFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/LogFileLoaderProvider.java
new file mode 100644
index 0000000..01cd233
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/LogFileLoaderProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+import org.jd.gui.util.io.TextReader;
+import org.jd.gui.view.component.LogPage;
+
+import java.io.File;
+
+public class LogFileLoaderProvider extends ZipFileLoaderProvider {
+    protected static final String[] EXTENSIONS = { "log" };
+
+    @Override public String[] getExtensions() { return EXTENSIONS; }
+    @Override public String getDescription() { return "Log files (*.log)"; }
+
+    @Override
+    public boolean accept(API api, File file) {
+        return file.exists() && file.isFile() && file.canRead() && file.getName().toLowerCase().endsWith(".log");
+    }
+
+    @Override
+    public boolean load(API api, File file) {
+        api.addPanel(file.getName(), null, "Location: " + file.getAbsolutePath(), new LogPage(api, file.toURI(), TextReader.getText(file)));
+        return true;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/WarFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/WarFileLoaderProvider.java
new file mode 100644
index 0000000..7784a7c
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/WarFileLoaderProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+
+import java.io.File;
+
+public class WarFileLoaderProvider extends ZipFileLoaderProvider {
+    protected static final String[] EXTENSIONS = { "war" };
+
+    @Override public String[] getExtensions() { return EXTENSIONS; }
+    @Override public String getDescription() { return "Web application archive files (*.war)"; }
+
+    @Override
+    public boolean accept(API api, File file) {
+        return file.exists() && file.isFile() && file.canRead() && file.getName().toLowerCase().endsWith(".war");
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/fileloader/ZipFileLoaderProvider.java b/services/src/main/java/org/jd/gui/service/fileloader/ZipFileLoaderProvider.java
new file mode 100644
index 0000000..4594c7d
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/fileloader/ZipFileLoaderProvider.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.fileloader;
+
+import org.jd.gui.api.API;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Iterator;
+
+public class ZipFileLoaderProvider extends AbstractFileLoaderProvider {
+    protected static final String[] EXTENSIONS = { "zip" };
+
+    @Override public String[] getExtensions() { return EXTENSIONS; }
+    @Override public String getDescription() { return "Zip files (*.zip)"; }
+
+    @Override
+    public boolean accept(API api, File file) {
+        return file.exists() && file.isFile() && file.canRead() && file.getName().toLowerCase().endsWith(".zip");
+    }
+
+    @Override
+    public boolean load(API api, File file) {
+        try {
+            URI fileUri = file.toURI();
+            URI uri = new URI("jar:" + fileUri.getScheme(), fileUri.getHost(), fileUri.getPath() + "!/", null);
+            FileSystem fileSystem;
+
+            try {
+                fileSystem = FileSystems.getFileSystem(uri);
+            } catch (FileSystemNotFoundException e) {
+                fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap());
+            }
+
+            if (fileSystem != null) {
+                Iterator<Path> rootDirectories = fileSystem.getRootDirectories().iterator();
+
+                if (rootDirectories.hasNext()) {
+                    return load(api, file, rootDirectories.next()) != null;
+                }
+            }
+        } catch (URISyntaxException|IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        return false;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/AbstractIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/AbstractIndexerProvider.java
new file mode 100644
index 0000000..3794be4
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/AbstractIndexerProvider.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.spi.Indexer;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+import java.util.regex.Pattern;
+
+public abstract class AbstractIndexerProvider implements Indexer {
+    protected List<String> externalSelectors;
+    protected Pattern externalPathPattern;
+
+    /**
+     * Initialize "selectors" and "pathPattern" with optional external properties file
+     */
+    public AbstractIndexerProvider() {
+        Properties properties = new Properties();
+        Class clazz = this.getClass();
+
+        try (InputStream is = clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/') + ".properties")) {
+            if (is != null) {
+                properties.load(is);
+            }
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        init(properties);
+    }
+
+    protected void init(Properties properties) {
+        String selectors = properties.getProperty("selectors");
+
+        if (selectors != null) {
+            externalSelectors = Arrays.asList(selectors.split(","));
+        }
+
+        String pathRegExp = properties.getProperty("pathRegExp");
+
+        if (pathRegExp != null) {
+            externalPathPattern = Pattern.compile(pathRegExp);
+        }
+    }
+
+    protected String[] appendSelectors(String selector) {
+        if (externalSelectors == null) {
+            return new String[] { selector };
+        } else {
+            int size = externalSelectors.size();
+            String[] array = new String[size+1];
+            externalSelectors.toArray(array);
+            array[size] = selector;
+            return array;
+        }
+    }
+
+    protected String[] appendSelectors(String... selectors) {
+        if (externalSelectors == null) {
+            return selectors;
+        } else {
+            int size = externalSelectors.size();
+            String[] array = new String[size+selectors.length];
+            externalSelectors.toArray(array);
+            System.arraycopy(selectors, 0, array, size, selectors.length);
+            return array;
+        }
+    }
+
+    @Override public Pattern getPathPattern() { return externalPathPattern; }
+
+    @SuppressWarnings("unchecked")
+    protected static void addToIndexes(Indexes indexes, String indexName, Set<String> set, Container.Entry entry) {
+        if (set.size() > 0) {
+            Map<String, Collection> index = indexes.getIndex(indexName);
+
+            for (String key : set) {
+                index.get(key).add(entry);
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/ClassFileIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/ClassFileIndexerProvider.java
new file mode 100644
index 0000000..4a3ce8c
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/ClassFileIndexerProvider.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.objectweb.asm.*;
+import org.objectweb.asm.signature.SignatureReader;
+import org.objectweb.asm.signature.SignatureVisitor;
+
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import static org.objectweb.asm.ClassReader.*;
+
+/**
+ * Unsafe thread implementation of class file indexer.
+ */
+public class ClassFileIndexerProvider extends AbstractIndexerProvider {
+    protected HashSet<String> typeDeclarationSet = new HashSet<>();
+    protected HashSet<String> constructorDeclarationSet = new HashSet<>();
+    protected HashSet<String> methodDeclarationSet = new HashSet<>();
+    protected HashSet<String> fieldDeclarationSet = new HashSet<>();
+    protected HashSet<String> typeReferenceSet = new HashSet<>();
+    protected HashSet<String> constructorReferenceSet = new HashSet<>();
+    protected HashSet<String> methodReferenceSet = new HashSet<>();
+    protected HashSet<String> fieldReferenceSet = new HashSet<>();
+    protected HashSet<String> stringSet = new HashSet<>();
+    protected HashSet<String> superTypeNameSet = new HashSet<>();
+    protected HashSet<String> descriptorSet = new HashSet<>();
+
+    protected ClassIndexer classIndexer = new ClassIndexer();
+    protected SignatureIndexer signatureIndexer = new SignatureIndexer();
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.class"); }
+
+    @Override
+    public Pattern getPathPattern() {
+        if (externalPathPattern == null) {
+            return Pattern.compile("^((?!module-info\\.class).)*$");
+        } else {
+            return externalPathPattern;
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        // Cleaning sets...
+        typeDeclarationSet.clear();
+        constructorDeclarationSet.clear();
+        methodDeclarationSet.clear();
+        fieldDeclarationSet.clear();
+        typeReferenceSet.clear();
+        constructorReferenceSet.clear();
+        methodReferenceSet.clear();
+        fieldReferenceSet.clear();
+        stringSet.clear();
+        superTypeNameSet.clear();
+        descriptorSet.clear();
+
+        try (InputStream inputStream = entry.getInputStream()) {
+            // Index field, method, interfaces & super type
+            ClassReader classReader = new ClassReader(inputStream);
+            classReader.accept(classIndexer, SKIP_CODE|SKIP_DEBUG|SKIP_FRAMES);
+
+            // Index descriptors
+            for (String descriptor : descriptorSet) {
+                new SignatureReader(descriptor).accept(signatureIndexer);
+            }
+
+            // Index references
+            char[] buffer = new char[classReader.getMaxStringLength()];
+
+            for (int i=classReader.getItemCount()-1; i>0; i--) {
+                int startIndex = classReader.getItem(i);
+
+                if (startIndex != 0) {
+                    int tag = classReader.readByte(startIndex-1);
+
+                    switch (tag) {
+                        case 7: // CONSTANT_Class
+                            String className = classReader.readUTF8(startIndex, buffer);
+                            if (className.startsWith("[")) {
+                                new SignatureReader(className).acceptType(signatureIndexer);
+                            } else {
+                                typeReferenceSet.add(className);
+                            }
+                            break;
+                        case 8: // CONSTANT_String
+                            String str = classReader.readUTF8(startIndex, buffer);
+                            stringSet.add(str);
+                            break;
+                        case 9: // CONSTANT_Fieldref
+                            int nameAndTypeItem = classReader.readUnsignedShort(startIndex+2);
+                            int nameAndTypeIndex = classReader.getItem(nameAndTypeItem);
+                            tag = classReader.readByte(nameAndTypeIndex-1);
+                            if (tag == 12) { // CONSTANT_NameAndType
+                                String fieldName = classReader.readUTF8(nameAndTypeIndex, buffer);
+                                fieldReferenceSet.add(fieldName);
+                            }
+                            break;
+                        case 10: // CONSTANT_Methodref:
+                        case 11: // CONSTANT_InterfaceMethodref:
+                            nameAndTypeItem = classReader.readUnsignedShort(startIndex+2);
+                            nameAndTypeIndex = classReader.getItem(nameAndTypeItem);
+                            tag = classReader.readByte(nameAndTypeIndex-1);
+                            if (tag == 12) { // CONSTANT_NameAndType
+                                String methodName = classReader.readUTF8(nameAndTypeIndex, buffer);
+                                if ("<init>".equals(methodName)) {
+                                    int classItem = classReader.readUnsignedShort(startIndex);
+                                    int classIndex = classReader.getItem(classItem);
+                                    className = classReader.readUTF8(classIndex, buffer);
+                                    constructorReferenceSet.add(className);
+                                } else {
+                                    methodReferenceSet.add(methodName);
+                                }
+                            }
+                            break;
+                    }
+                }
+            }
+
+            String typeName = classIndexer.name;
+
+            // Append sets to indexes
+            addToIndexes(indexes, "typeDeclarations", typeDeclarationSet, entry);
+            addToIndexes(indexes, "constructorDeclarations", constructorDeclarationSet, entry);
+            addToIndexes(indexes, "methodDeclarations", methodDeclarationSet, entry);
+            addToIndexes(indexes, "fieldDeclarations", fieldDeclarationSet, entry);
+            addToIndexes(indexes, "typeReferences", typeReferenceSet, entry);
+            addToIndexes(indexes, "constructorReferences", constructorReferenceSet, entry);
+            addToIndexes(indexes, "methodReferences", methodReferenceSet, entry);
+            addToIndexes(indexes, "fieldReferences", fieldReferenceSet, entry);
+            addToIndexes(indexes, "strings", stringSet, entry);
+
+            // Populate map [super type name : [sub type name]]
+            if (superTypeNameSet.size() > 0) {
+                Map<String, Collection> index = indexes.getIndex("subTypeNames");
+
+                for (String superTypeName : superTypeNameSet) {
+                    index.get(superTypeName).add(typeName);
+                }
+            }
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    protected class ClassIndexer extends ClassVisitor {
+        protected AnnotationIndexer annotationIndexer = new AnnotationIndexer();
+        protected FieldIndexer fieldIndexer = new FieldIndexer(annotationIndexer);
+        protected MethodIndexer methodIndexer = new MethodIndexer(annotationIndexer);
+
+        protected String name;
+
+        public ClassIndexer() { super(Opcodes.ASM7); }
+
+        @Override
+        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
+            this.name = name;
+            typeDeclarationSet.add(name);
+
+            if (superName != null) {
+                superTypeNameSet.add(superName);
+            }
+
+            if (interfaces != null) {
+                for (int i=interfaces.length-1; i>=0; i--) {
+                    superTypeNameSet.add(interfaces[i]);
+                }
+            }
+        }
+
+        @Override
+        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+            descriptorSet.add(desc);
+            return annotationIndexer;
+        }
+
+        @Override
+        public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) {
+            descriptorSet.add(desc);
+            return annotationIndexer;
+        }
+
+        @Override
+        public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
+            fieldDeclarationSet.add(name);
+            descriptorSet.add(signature==null ? desc : signature);
+            return fieldIndexer;
+        }
+
+        @Override
+        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
+            if ("<init>".equals(name)) {
+                constructorDeclarationSet.add(this.name);
+            } else if (! "<clinit>".equals(name)) {
+                methodDeclarationSet.add(name);
+            }
+
+            descriptorSet.add(signature==null ? desc : signature);
+
+            if (exceptions != null) {
+                for (int i=exceptions.length-1; i>=0; i--) {
+                    typeReferenceSet.add(exceptions[i]);
+                }
+            }
+            return methodIndexer;
+        }
+    }
+
+    protected class SignatureIndexer extends SignatureVisitor {
+        SignatureIndexer() { super(Opcodes.ASM7); }
+
+        @Override public void visitClassType(String name) { typeReferenceSet.add(name); }
+    }
+
+    protected class AnnotationIndexer extends AnnotationVisitor {
+        public AnnotationIndexer() { super(Opcodes.ASM7); }
+
+        @Override public void visitEnum(String name, String desc, String value) { descriptorSet.add(desc); }
+
+        @Override
+        public AnnotationVisitor visitAnnotation(String name, String desc) {
+            descriptorSet.add(desc);
+            return this;
+        }
+    }
+
+    protected class FieldIndexer extends FieldVisitor {
+        protected AnnotationIndexer annotationIndexer;
+
+        public FieldIndexer(AnnotationIndexer annotationIndexer) {
+            super(Opcodes.ASM7);
+            this.annotationIndexer = annotationIndexer;
+        }
+
+        @Override
+        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+            descriptorSet.add(desc);
+            return annotationIndexer;
+        }
+
+        @Override
+        public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) {
+            descriptorSet.add(desc);
+            return annotationIndexer;
+        }
+    }
+
+    protected class MethodIndexer extends MethodVisitor {
+        protected AnnotationIndexer annotationIndexer;
+
+        public MethodIndexer(AnnotationIndexer annotationIndexer) {
+            super(Opcodes.ASM7);
+            this.annotationIndexer = annotationIndexer;
+        }
+
+        @Override
+        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+            descriptorSet.add(desc);
+            return annotationIndexer;
+        }
+
+        @Override
+        public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) {
+            descriptorSet.add(desc);
+            return annotationIndexer;
+        }
+
+        @Override
+        public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
+            descriptorSet.add(desc);
+            return annotationIndexer;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/DirectoryIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/DirectoryIndexerProvider.java
new file mode 100644
index 0000000..f0e3729
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/DirectoryIndexerProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.spi.Indexer;
+
+public class DirectoryIndexerProvider extends AbstractIndexerProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("*:dir:*"); }
+
+    @Override
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        int depth = 15;
+
+        try {
+            depth = Integer.valueOf(api.getPreferences().get("DirectoryIndexerPreferences.maximumDepth"));
+        } catch (NumberFormatException ignore) {
+        }
+
+        index(api, entry, indexes, depth);
+    }
+
+    public void index(API api, Container.Entry entry, Indexes indexes, int depth) {
+        if (depth-- > 0) {
+            for (Container.Entry e : entry.getChildren()) {
+                if (e.isDirectory()) {
+                    index(api, e, indexes, depth);
+                } else {
+                    Indexer indexer = api.getIndexer(e);
+
+                    if (indexer != null) {
+                        indexer.index(api, e, indexes);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/EjbJarXmlFileIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/EjbJarXmlFileIndexerProvider.java
new file mode 100644
index 0000000..973c315
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/EjbJarXmlFileIndexerProvider.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.io.TextReader;
+import org.jd.gui.util.xml.AbstractXmlPathFinder;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+
+public class EjbJarXmlFileIndexerProvider extends XmlBasedFileIndexerProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:META-INF/ejb-jar.xml"); }
+
+    @Override
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        super.index(api, entry, indexes);
+
+        new EjbJarXmlPathFinder(entry, indexes).find(TextReader.getText(entry.getInputStream()));
+    }
+
+    public static class EjbJarXmlPathFinder extends AbstractXmlPathFinder {
+        protected Container.Entry entry;
+        protected Map<String, Collection> index;
+
+        public EjbJarXmlPathFinder(Container.Entry entry, Indexes indexes) {
+            super(Arrays.asList(
+                "ejb-jar/assembly-descriptor/application-exception/exception-class",
+                "ejb-jar/assembly-descriptor/interceptor-binding/interceptor-class",
+
+                "ejb-jar/enterprise-beans/entity/home",
+                "ejb-jar/enterprise-beans/entity/remote",
+                "ejb-jar/enterprise-beans/entity/ejb-class",
+                "ejb-jar/enterprise-beans/entity/prim-key-class",
+
+                "ejb-jar/enterprise-beans/message-driven/ejb-class",
+                "ejb-jar/enterprise-beans/message-driven/messaging-type",
+                "ejb-jar/enterprise-beans/message-driven/resource-ref/injection-target/injection-target-class",
+                "ejb-jar/enterprise-beans/message-driven/resource-env-ref/injection-target/injection-target-class",
+
+                "ejb-jar/enterprise-beans/session/home",
+                "ejb-jar/enterprise-beans/session/local",
+                "ejb-jar/enterprise-beans/session/remote",
+                "ejb-jar/enterprise-beans/session/business-local",
+                "ejb-jar/enterprise-beans/session/business-remote",
+                "ejb-jar/enterprise-beans/session/service-endpoint",
+                "ejb-jar/enterprise-beans/session/ejb-class",
+                "ejb-jar/enterprise-beans/session/ejb-ref/home",
+                "ejb-jar/enterprise-beans/session/ejb-ref/remote",
+
+                "ejb-jar/interceptors/interceptor/around-invoke/class",
+                "ejb-jar/interceptors/interceptor/ejb-ref/home",
+                "ejb-jar/interceptors/interceptor/ejb-ref/remote",
+                "ejb-jar/interceptors/interceptor/interceptor-class"
+            ));
+            this.entry = entry;
+            this.index = indexes.getIndex("typeReferences");
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public void handle(String path, String text, int position) {
+            index.get(text.replace(".", "/")).add(entry);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/JavaFileIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/JavaFileIndexerProvider.java
new file mode 100644
index 0000000..eaf155d
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/JavaFileIndexerProvider.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.antlr.v4.runtime.ANTLRInputStream;
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.TerminalNode;
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.parser.antlr.ANTLRJavaParser;
+import org.jd.gui.util.parser.antlr.AbstractJavaListener;
+import org.jd.gui.util.parser.antlr.JavaParser;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+
+/**
+ * Unsafe thread implementation of java file indexer.
+ */
+public class JavaFileIndexerProvider extends AbstractIndexerProvider {
+
+    static {
+        // Early class loading
+        ANTLRJavaParser.parse(new ANTLRInputStream("class EarlyLoading{}"), new Listener(null));
+    }
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.java"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        try (InputStream inputStream = entry.getInputStream()) {
+            Listener listener = new Listener(entry);
+            ANTLRJavaParser.parse(new ANTLRInputStream(inputStream), listener);
+
+            // Append sets to indexes
+            addToIndexes(indexes, "typeDeclarations", listener.getTypeDeclarationSet(), entry);
+            addToIndexes(indexes, "constructorDeclarations", listener.getConstructorDeclarationSet(), entry);
+            addToIndexes(indexes, "methodDeclarations", listener.getMethodDeclarationSet(), entry);
+            addToIndexes(indexes, "fieldDeclarations", listener.getFieldDeclarationSet(), entry);
+            addToIndexes(indexes, "typeReferences", listener.getTypeReferenceSet(), entry);
+            addToIndexes(indexes, "constructorReferences", listener.getConstructorReferenceSet(), entry);
+            addToIndexes(indexes, "methodReferences", listener.getMethodReferenceSet(), entry);
+            addToIndexes(indexes, "fieldReferences", listener.getFieldReferenceSet(), entry);
+            addToIndexes(indexes, "strings", listener.getStringSet(), entry);
+
+            // Populate map [super type name : [sub type name]]
+            Map<String, Collection> index = indexes.getIndex("subTypeNames");
+
+            for (Map.Entry<String, HashSet<String>> e : listener.getSuperTypeNamesMap().entrySet()) {
+                String typeName = e.getKey();
+
+                for (String superTypeName : e.getValue()) {
+                    index.get(superTypeName).add(typeName);
+                }
+            }
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    protected static class Listener extends AbstractJavaListener {
+
+        protected HashSet<String> typeDeclarationSet = new HashSet<>();
+        protected HashSet<String> constructorDeclarationSet = new HashSet<>();
+        protected HashSet<String> methodDeclarationSet = new HashSet<>();
+        protected HashSet<String> fieldDeclarationSet = new HashSet<>();
+        protected HashSet<String> typeReferenceSet = new HashSet<>();
+        protected HashSet<String> constructorReferenceSet = new HashSet<>();
+        protected HashSet<String> methodReferenceSet = new HashSet<>();
+        protected HashSet<String> fieldReferenceSet = new HashSet<>();
+        protected HashSet<String> stringSet = new HashSet<>();
+        protected HashMap<String, HashSet<String>> superTypeNamesMap = new HashMap<>();
+
+        protected StringBuilder sbTypeDeclaration = new StringBuilder();
+
+        public Listener(Container.Entry entry) {
+            super(entry);
+        }
+
+        public HashSet<String> getTypeDeclarationSet() { return typeDeclarationSet; }
+        public HashSet<String> getConstructorDeclarationSet() { return constructorDeclarationSet; }
+        public HashSet<String> getMethodDeclarationSet() { return methodDeclarationSet; }
+        public HashSet<String> getFieldDeclarationSet() { return fieldDeclarationSet; }
+        public HashSet<String> getTypeReferenceSet() { return typeReferenceSet; }
+        public HashSet<String> getConstructorReferenceSet() { return constructorReferenceSet; }
+        public HashSet<String> getMethodReferenceSet() { return methodReferenceSet; }
+        public HashSet<String> getFieldReferenceSet() { return fieldReferenceSet; }
+        public HashSet<String> getStringSet() { return stringSet; }
+        public HashMap<String, HashSet<String>> getSuperTypeNamesMap() { return superTypeNamesMap; }
+
+        // --- ANTLR Listener --- //
+
+        public void enterPackageDeclaration(JavaParser.PackageDeclarationContext ctx) {
+            super.enterPackageDeclaration(ctx);
+
+            if (! packageName.isEmpty()) {
+                sbTypeDeclaration.append(packageName).append('/');
+            }
+        }
+
+        public void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterEnumDeclaration(JavaParser.EnumDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitEnumDeclaration(JavaParser.EnumDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterInterfaceDeclaration(JavaParser.InterfaceDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitInterfaceDeclaration(JavaParser.InterfaceDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterAnnotationTypeDeclaration(JavaParser.AnnotationTypeDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitAnnotationTypeDeclaration(JavaParser.AnnotationTypeDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        protected void enterTypeDeclaration(ParserRuleContext ctx) {
+            // Add type declaration
+            TerminalNode identifier = ctx.getToken(JavaParser.Identifier, 0);
+
+            if (identifier != null) {
+                String typeName = identifier.getText();
+                int length = sbTypeDeclaration.length();
+
+                if ((length == 0) || (sbTypeDeclaration.charAt(length - 1) == '/')) {
+                    sbTypeDeclaration.append(typeName);
+                } else {
+                    sbTypeDeclaration.append('$').append(typeName);
+                }
+
+                String internalTypeName = sbTypeDeclaration.toString();
+                typeDeclarationSet.add(internalTypeName);
+                nameToInternalTypeName.put(typeName, internalTypeName);
+
+                HashSet<String> superInternalTypeNameSet = new HashSet<>();
+
+                // Add super type reference
+                JavaParser.TypeContext superType = ctx.getRuleContext(JavaParser.TypeContext.class, 0);
+                if (superType != null) {
+                    String superQualifiedTypeName = resolveInternalTypeName(superType.classOrInterfaceType().Identifier());
+
+                    if (superQualifiedTypeName.charAt(0) != '*')
+                        superInternalTypeNameSet.add(superQualifiedTypeName);
+                }
+
+                // Add implementation references
+                JavaParser.TypeListContext superInterfaces = ctx.getRuleContext(JavaParser.TypeListContext.class, 0);
+                if (superInterfaces != null) {
+                    for (JavaParser.TypeContext superInterface : superInterfaces.type()) {
+                        String superQualifiedInterfaceName = resolveInternalTypeName(superInterface.classOrInterfaceType().Identifier());
+
+                        if (superQualifiedInterfaceName.charAt(0) != '*')
+                            superInternalTypeNameSet.add(superQualifiedInterfaceName);
+                    }
+                }
+
+                if (!superInternalTypeNameSet.isEmpty()) {
+                    superTypeNamesMap.put(internalTypeName, superInternalTypeNameSet);
+                }
+            }
+        }
+
+        protected void exitTypeDeclaration() {
+            int index = sbTypeDeclaration.lastIndexOf("$");
+
+            if (index == -1) {
+                index = sbTypeDeclaration.lastIndexOf("/") + 1;
+            }
+
+            if (index == -1) {
+                sbTypeDeclaration.setLength(0);
+            } else {
+                sbTypeDeclaration.setLength(index);
+            }
+        }
+
+        public void enterType(JavaParser.TypeContext ctx) {
+            // Add type reference
+            JavaParser.ClassOrInterfaceTypeContext classOrInterfaceType = ctx.classOrInterfaceType();
+
+            if (classOrInterfaceType != null) {
+                String internalTypeName = resolveInternalTypeName(classOrInterfaceType.Identifier());
+
+                if (internalTypeName.charAt(0) != '*')
+                    typeReferenceSet.add(internalTypeName);
+            }
+        }
+
+        public void enterConstDeclaration(JavaParser.ConstDeclarationContext ctx) {
+            for (JavaParser.ConstantDeclaratorContext constantDeclaratorContext : ctx.constantDeclarator()) {
+                String name = constantDeclaratorContext.Identifier().getText();
+                fieldDeclarationSet.add(name);
+            }
+        }
+
+        public void enterFieldDeclaration(JavaParser.FieldDeclarationContext ctx) {
+            for (JavaParser.VariableDeclaratorContext declaration : ctx.variableDeclarators().variableDeclarator()) {
+                TerminalNode identifier = declaration.variableDeclaratorId().Identifier();
+
+                if (identifier != null) {
+                    String name = identifier.getText();
+                    fieldDeclarationSet.add(name);
+                }
+            }
+        }
+
+        public void enterMethodDeclaration(JavaParser.MethodDeclarationContext ctx) {
+            TerminalNode identifier = ctx.Identifier();
+
+            if (identifier != null) {
+                String name = identifier.getText();
+                methodDeclarationSet.add(name);
+            }
+        }
+
+        public void enterInterfaceMethodDeclaration(JavaParser.InterfaceMethodDeclarationContext ctx) {
+            TerminalNode identifier = ctx.Identifier();
+
+            if (identifier != null) {
+                String name = identifier.getText();
+                methodDeclarationSet.add(name);
+            }
+        }
+
+        public void enterConstructorDeclaration(JavaParser.ConstructorDeclarationContext ctx) {
+            String name = ctx.Identifier().getText();
+            constructorDeclarationSet.add(name);
+        }
+
+        public void enterCreatedName(JavaParser.CreatedNameContext ctx) {
+            String internalTypeName = resolveInternalTypeName(ctx.Identifier());
+
+            if ((internalTypeName != null) && (internalTypeName.charAt(0) != '*'))
+                constructorReferenceSet.add(internalTypeName);
+        }
+
+        public void enterExpression(JavaParser.ExpressionContext ctx) {
+            switch (ctx.getChildCount()) {
+                case 3:
+                    if (getToken(ctx.children, 1, JavaParser.DOT) != null) {
+                        // Search "expression '.' Identifier" : field
+                        TerminalNode identifier3 = getToken(ctx.children, 2, JavaParser.Identifier);
+
+                        if (identifier3 != null) {
+                            String fieldName = identifier3.getText();
+                            fieldReferenceSet.add(fieldName);
+                        }
+                    } else if (getToken(ctx.children, 1, JavaParser.LPAREN) != null) {
+                        // Search "expression '(' ')'" : method
+                        if (getToken(ctx.children, 2, JavaParser.RPAREN) != null) {
+                            TerminalNode identifier0 = getRightTerminalNode(ctx.children.get(0));
+
+                            if (identifier0 != null) {
+                                String methodName = identifier0.getText();
+                                methodReferenceSet.add(methodName);
+                            }
+                        }
+                    }
+                    break;
+                case 4:
+                    if (getToken(ctx.children, 1, JavaParser.LPAREN) != null) {
+                        // Search "expression '(' expressionList ')'" : method
+                        if (getToken(ctx.children, 3, JavaParser.RPAREN) != null) {
+                            JavaParser.ExpressionListContext expressionListContext = ctx.expressionList();
+
+                            if ((expressionListContext != null) && (expressionListContext == ctx.children.get(2))) {
+                                TerminalNode identifier0 = getRightTerminalNode(ctx.children.get(0));
+
+                                if (identifier0 != null) {
+                                    String methodName = identifier0.getText();
+                                    methodReferenceSet.add(methodName);
+                                }
+                            }
+                        }
+                    }
+                    break;
+            }
+        }
+
+        protected TerminalNode getToken(List<ParseTree> children, int i, int type) {
+            ParseTree pt = children.get(i);
+
+            if (pt instanceof TerminalNode) {
+                if (((TerminalNode)pt).getSymbol().getType() == type) {
+                    return (TerminalNode)pt;
+                }
+            }
+
+            return null;
+        }
+
+        protected TerminalNode getRightTerminalNode(ParseTree pt) {
+            if (pt instanceof ParserRuleContext) {
+                List<ParseTree> children = ((ParserRuleContext)pt).children;
+
+                if (children != null) {
+                    int size = children.size();
+
+                    if (size > 0) {
+                        ParseTree last = children.get(size - 1);
+
+                        if (last instanceof TerminalNode) {
+                            return (TerminalNode) last;
+                        } else {
+                            return getRightTerminalNode(last);
+                        }
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        public void enterLiteral(JavaParser.LiteralContext ctx) {
+            TerminalNode stringLiteral = ctx.StringLiteral();
+            if (stringLiteral != null) {
+                stringSet.add(stringLiteral.getSymbol().getText());
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/JavaModuleFileIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/JavaModuleFileIndexerProvider.java
new file mode 100644
index 0000000..e419b34
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/JavaModuleFileIndexerProvider.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.spi.Indexer;
+
+import java.util.Collection;
+import java.util.Map;
+
+public class JavaModuleFileIndexerProvider extends AbstractIndexerProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.jmod"); }
+
+    @Override
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        for (Container.Entry e : entry.getChildren()) {
+            if (e.isDirectory() && e.getPath().equals("classes")) {
+                Map<String, Collection> packageDeclarationIndex = indexes.getIndex("packageDeclarations");
+
+                // Index module-info, packages and CLASS files
+                index(api, e, indexes, packageDeclarationIndex);
+                break;
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected static void index(API api, Container.Entry entry, Indexes indexes, Map<String, Collection> packageDeclarationIndex) {
+        for (Container.Entry e : entry.getChildren()) {
+            if (e.isDirectory()) {
+                String path = e.getPath();
+
+                if (!path.startsWith("classes/META-INF")) {
+                    packageDeclarationIndex.get(path.substring(8)).add(e); // 8 = "classes/".length()
+                }
+
+                index(api, e, indexes, packageDeclarationIndex);
+            } else {
+                Indexer indexer = api.getIndexer(e);
+
+                if (indexer != null) {
+                    indexer.index(api, e, indexes);
+                }
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/JavaModuleInfoFileIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/JavaModuleInfoFileIndexerProvider.java
new file mode 100644
index 0000000..9f22e10
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/JavaModuleInfoFileIndexerProvider.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.objectweb.asm.*;
+
+import java.io.InputStream;
+import java.util.HashSet;
+
+import static org.objectweb.asm.ClassReader.*;
+
+/**
+ * Unsafe thread implementation of class file indexer.
+ */
+public class JavaModuleInfoFileIndexerProvider extends AbstractIndexerProvider {
+    protected HashSet<String> javaModuleDeclarationSet = new HashSet<>();
+    protected HashSet<String> javaModuleReferenceSet = new HashSet<>();
+    protected HashSet<String> typeReferenceSet = new HashSet<>();
+
+    protected ClassIndexer classIndexer = new ClassIndexer();
+
+    @Override public String[] getSelectors() { return appendSelectors("jmod:file:classes/module-info.class"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        // Cleaning sets...
+        javaModuleDeclarationSet.clear();
+        javaModuleReferenceSet.clear();
+        typeReferenceSet.clear();
+
+        try (InputStream inputStream = entry.getInputStream()) {
+            // Index field, method, interfaces & super type
+            ClassReader classReader = new ClassReader(inputStream);
+            classReader.accept(classIndexer, SKIP_CODE|SKIP_DEBUG|SKIP_FRAMES);
+
+            // Append sets to indexes
+            addToIndexes(indexes, "javaModuleDeclarations", javaModuleDeclarationSet, entry);
+            addToIndexes(indexes, "javaModuleReferences", javaModuleReferenceSet, entry);
+            addToIndexes(indexes, "typeReferences", typeReferenceSet, entry);
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    protected class ClassIndexer extends ClassVisitor {
+        protected ModuleIndexer moduleIndexer = new ModuleIndexer();
+
+        public ClassIndexer() { super(Opcodes.ASM7); }
+
+        @Override
+        public ModuleVisitor visitModule(String moduleName, int moduleFlags, String moduleVersion) {
+            javaModuleDeclarationSet.add(moduleName);
+            return moduleIndexer;
+        }
+    }
+
+    protected class ModuleIndexer extends ModuleVisitor {
+        public ModuleIndexer() { super(Opcodes.ASM7); }
+
+        @Override public void visitMainClass(final String mainClass) { typeReferenceSet.add(mainClass); }
+        @Override public void visitRequire(final String module, final int access, final String version) { javaModuleReferenceSet.add(module); }
+        @Override public void visitUse(final String service) { typeReferenceSet.add(service); }
+
+        @Override
+        public void visitExport(final String packaze, final int access, final String... modules) {
+            if (modules != null) {
+                for (String module : modules) {
+                    javaModuleReferenceSet.add(module);
+                }
+            }
+        }
+
+        @Override
+        public void visitOpen(final String packaze, final int access, final String... modules) {
+            if (modules != null) {
+                for (String module : modules) {
+                    javaModuleReferenceSet.add(module);
+                }
+            }
+        }
+
+        @Override
+        public void visitProvide(final String service, final String... providers) {
+            typeReferenceSet.add(service);
+
+            if (providers != null) {
+                for (String provider : providers) {
+                    typeReferenceSet.add(provider);
+                }
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/MetainfServiceFileIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/MetainfServiceFileIndexerProvider.java
new file mode 100644
index 0000000..1fc151d
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/MetainfServiceFileIndexerProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+public class MetainfServiceFileIndexerProvider extends AbstractIndexerProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*"); }
+
+    @Override public Pattern getPathPattern() { return (externalPathPattern != null) ? externalPathPattern : Pattern.compile("META-INF\\/services\\/[^\\/]+"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        Map<String, Collection> index = indexes.getIndex("typeReferences");
+
+        try (BufferedReader br = new BufferedReader(new InputStreamReader(entry.getInputStream()))) {
+            String line;
+
+            while ((line = br.readLine()) != null) {
+                String trim = line.trim();
+
+                if (!trim.isEmpty() && (trim.charAt(0) != '#')) {
+                    String internalTypeName = trim.replace(".", "/");
+
+                    index.get(internalTypeName).add(entry);
+                }
+            }
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/TextFileIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/TextFileIndexerProvider.java
new file mode 100644
index 0000000..1e5231b
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/TextFileIndexerProvider.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.io.TextReader;
+
+public class TextFileIndexerProvider extends AbstractIndexerProvider {
+
+    @Override public String[] getSelectors() {
+        return appendSelectors(
+                "*:file:*.txt", "*:file:*.html", "*:file:*.xhtml", "*:file:*.js", "*:file:*.jsp", "*:file:*.jspf",
+                "*:file:*.xml", "*:file:*.xsl", "*:file:*.xslt", "*:file:*.xsd", "*:file:*.properties", "*:file:*.sql",
+                "*:file:*.yaml", "*:file:*.yml", "*:file:*.json");
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        indexes.getIndex("strings").get(TextReader.getText(entry.getInputStream())).add(entry);
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/WebXmlFileIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/WebXmlFileIndexerProvider.java
new file mode 100644
index 0000000..314a3d0
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/WebXmlFileIndexerProvider.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.io.TextReader;
+import org.jd.gui.util.xml.AbstractXmlPathFinder;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+
+public class WebXmlFileIndexerProvider extends XmlBasedFileIndexerProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:WEB-INF/web.xml"); }
+
+    @Override
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        super.index(api, entry, indexes);
+
+        new WebXmlPathFinder(entry, indexes).find(TextReader.getText(entry.getInputStream()));
+    }
+
+    protected static class WebXmlPathFinder extends AbstractXmlPathFinder {
+        Container.Entry entry;
+        Map<String, Collection> index;
+
+        public WebXmlPathFinder(Container.Entry entry, Indexes indexes) {
+            super(Arrays.asList(
+                "web-app/filter/filter-class",
+                "web-app/listener/listener-class",
+                "web-app/servlet/servlet-class"
+            ));
+            this.entry = entry;
+            this.index = indexes.getIndex("typeReferences");
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public void handle(String path, String text, int position) {
+            index.get(text.replace(".", "/")).add(entry);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/XmlBasedFileIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/XmlBasedFileIndexerProvider.java
new file mode 100644
index 0000000..d277e41
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/XmlBasedFileIndexerProvider.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+
+public class XmlBasedFileIndexerProvider extends AbstractIndexerProvider {
+    protected XMLInputFactory factory;
+
+    public XmlBasedFileIndexerProvider() {
+        factory = XMLInputFactory.newInstance();
+        factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
+    }
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.xsl", "*:file:*.xslt", "*:file:*.xsd"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        HashSet<String> stringSet = new HashSet<>();
+        XMLStreamReader reader = null;
+
+        try {
+            reader = factory.createXMLStreamReader(entry.getInputStream());
+
+            stringSet.add(reader.getVersion());
+            stringSet.add(reader.getEncoding());
+            stringSet.add(reader.getCharacterEncodingScheme());
+
+            while (reader.hasNext()) {
+                switch (reader.next()) {
+                    case XMLStreamConstants.START_ELEMENT:
+                        stringSet.add(reader.getLocalName());
+                        for (int i = reader.getAttributeCount() - 1; i >= 0; i--) {
+                            stringSet.add(reader.getAttributeLocalName(i));
+                            stringSet.add(reader.getAttributeValue(i));
+                        }
+                        for (int i = reader.getNamespaceCount() - 1; i >= 0; i--) {
+                            stringSet.add(reader.getNamespacePrefix(i));
+                            stringSet.add(reader.getNamespaceURI(i));
+                        }
+                        break;
+                    case XMLStreamConstants.PROCESSING_INSTRUCTION:
+                        stringSet.add(reader.getPITarget());
+                        stringSet.add(reader.getPIData());
+                        break;
+                    case XMLStreamConstants.START_DOCUMENT:
+                        stringSet.add(reader.getVersion());
+                        stringSet.add(reader.getEncoding());
+                        stringSet.add(reader.getCharacterEncodingScheme());
+                        break;
+                    case XMLStreamConstants.ENTITY_REFERENCE:
+                        stringSet.add(reader.getLocalName());
+                        stringSet.add(reader.getText());
+                        break;
+                    case XMLStreamConstants.ATTRIBUTE:
+                        stringSet.add(reader.getPrefix());
+                        stringSet.add(reader.getNamespaceURI());
+                        stringSet.add(reader.getLocalName());
+                        stringSet.add(reader.getText());
+                        break;
+                    case XMLStreamConstants.COMMENT:
+                    case XMLStreamConstants.DTD:
+                    case XMLStreamConstants.CDATA:
+                    case XMLStreamConstants.CHARACTERS:
+                        stringSet.add(reader.getText().trim());
+                        break;
+                    case XMLStreamConstants.NAMESPACE:
+                        for (int i = reader.getNamespaceCount() - 1; i >= 0; i--) {
+                            stringSet.add(reader.getNamespacePrefix(i));
+                            stringSet.add(reader.getNamespaceURI(i));
+                        }
+                        break;
+                }
+            }
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (XMLStreamException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            }
+        }
+
+        Map<String, Collection> stringIndex = indexes.getIndex("strings");
+
+        for (String string : stringSet) {
+            if ((string != null) && !string.isEmpty()) {
+                stringIndex.get(string).add(entry);
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/XmlFileIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/XmlFileIndexerProvider.java
new file mode 100644
index 0000000..4eb5c3f
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/XmlFileIndexerProvider.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+
+public class XmlFileIndexerProvider extends AbstractIndexerProvider {
+    protected XMLInputFactory factory;
+
+    public XmlFileIndexerProvider() {
+        factory = XMLInputFactory.newInstance();
+        factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
+    }
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.xml"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        HashSet<String> stringSet = new HashSet<>();
+        HashSet<String> typeReferenceSet = new HashSet<>();
+        XMLStreamReader reader = null;
+
+        try {
+            reader = factory.createXMLStreamReader(entry.getInputStream());
+
+            stringSet.add(reader.getVersion());
+            stringSet.add(reader.getEncoding());
+            stringSet.add(reader.getCharacterEncodingScheme());
+
+            while (reader.hasNext()) {
+                switch (reader.next()) {
+                    case XMLStreamConstants.START_ELEMENT:
+                        boolean beanFlag = reader.getLocalName().equals("bean");
+
+                        stringSet.add(reader.getLocalName());
+                        for (int i = reader.getAttributeCount() - 1; i >= 0; i--) {
+                            String attributeName = reader.getAttributeLocalName(i);
+
+                            stringSet.add(attributeName);
+
+                            if (beanFlag && attributeName.equals("class")) {
+                                // String bean reference
+                                typeReferenceSet.add(reader.getAttributeValue(i).replace(".", "/"));
+                            } else {
+                                stringSet.add(reader.getAttributeValue(i));
+                            }
+                        }
+                        for (int i = reader.getNamespaceCount() - 1; i >= 0; i--) {
+                            stringSet.add(reader.getNamespacePrefix(i));
+                            stringSet.add(reader.getNamespaceURI(i));
+                        }
+                        break;
+                    case XMLStreamConstants.PROCESSING_INSTRUCTION:
+                        stringSet.add(reader.getPITarget());
+                        stringSet.add(reader.getPIData());
+                        break;
+                    case XMLStreamConstants.START_DOCUMENT:
+                        stringSet.add(reader.getVersion());
+                        stringSet.add(reader.getEncoding());
+                        stringSet.add(reader.getCharacterEncodingScheme());
+                        break;
+                    case XMLStreamConstants.ENTITY_REFERENCE:
+                        stringSet.add(reader.getLocalName());
+                        stringSet.add(reader.getText());
+                        break;
+                    case XMLStreamConstants.ATTRIBUTE:
+                        stringSet.add(reader.getPrefix());
+                        stringSet.add(reader.getNamespaceURI());
+                        stringSet.add(reader.getLocalName());
+                        stringSet.add(reader.getText());
+                        break;
+                    case XMLStreamConstants.COMMENT:
+                    case XMLStreamConstants.DTD:
+                    case XMLStreamConstants.CDATA:
+                    case XMLStreamConstants.CHARACTERS:
+                        stringSet.add(reader.getText().trim());
+                        break;
+                    case XMLStreamConstants.NAMESPACE:
+                        for (int i = reader.getNamespaceCount() - 1; i >= 0; i--) {
+                            stringSet.add(reader.getNamespacePrefix(i));
+                            stringSet.add(reader.getNamespaceURI(i));
+                        }
+                        break;
+                }
+            }
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (XMLStreamException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            }
+        }
+
+        Map<String, Collection> stringIndex = indexes.getIndex("strings");
+        Map<String, Collection> typeReferenceIndex = indexes.getIndex("typeReferences");
+
+        for (String string : stringSet) {
+            if ((string != null) && !string.isEmpty()) {
+                stringIndex.get(string).add(entry);
+            }
+        }
+
+        for (String ref : typeReferenceSet) {
+            if ((ref != null) && !ref.isEmpty()) {
+                typeReferenceIndex.get(ref).add(entry);
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/indexer/ZipFileIndexerProvider.java b/services/src/main/java/org/jd/gui/service/indexer/ZipFileIndexerProvider.java
new file mode 100644
index 0000000..a715bfe
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/indexer/ZipFileIndexerProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.indexer;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.spi.Indexer;
+
+public class ZipFileIndexerProvider extends AbstractIndexerProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.zip", "*:file:*.jar", "*:file:*.war", "*:file:*.ear", "*:file:*.aar", "*:file:*.kar"); }
+
+    @Override
+    public void index(API api, Container.Entry entry, Indexes indexes) {
+        for (Container.Entry e : entry.getChildren()) {
+            if (e.isDirectory()) {
+                index(api, e, indexes);
+            } else {
+                Indexer indexer = api.getIndexer(e);
+
+                if (indexer != null) {
+                    indexer.index(api, e, indexes);
+                }
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/pastehandler/LogPasteHandler.java b/services/src/main/java/org/jd/gui/service/pastehandler/LogPasteHandler.java
new file mode 100644
index 0000000..a6aad62
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/pastehandler/LogPasteHandler.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.pastehandler;
+
+import org.jd.gui.api.API;
+import org.jd.gui.spi.PasteHandler;
+import org.jd.gui.view.component.LogPage;
+
+import java.net.URI;
+
+public class LogPasteHandler implements PasteHandler {
+    protected static int counter = 0;
+
+    public boolean accept(Object obj) { return obj instanceof String; }
+
+    public void paste(API api, Object obj) {
+        String title = "clipboard-" + (++counter) + ".log";
+        URI uri = URI.create("memory://" + title);
+        String content = (obj == null) ? null : obj.toString();
+        api.addPanel(title, null, null, new LogPage(api, uri, content));
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/preferencespanel/ClassFileDecompilerPreferencesProvider.java b/services/src/main/java/org/jd/gui/service/preferencespanel/ClassFileDecompilerPreferencesProvider.java
new file mode 100644
index 0000000..e84a828
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/preferencespanel/ClassFileDecompilerPreferencesProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.preferencespanel;
+
+import org.jd.gui.spi.PreferencesPanel;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Map;
+
+public class ClassFileDecompilerPreferencesProvider extends JPanel implements PreferencesPanel {
+    protected static final String ESCAPE_UNICODE_CHARACTERS = "ClassFileDecompilerPreferences.escapeUnicodeCharacters";
+    protected static final String REALIGN_LINE_NUMBERS = "ClassFileDecompilerPreferences.realignLineNumbers";
+
+    protected PreferencesPanel.PreferencesPanelChangeListener listener = null;
+    protected JCheckBox escapeUnicodeCharactersCheckBox;
+    protected JCheckBox realignLineNumbersCheckBox;
+
+    public ClassFileDecompilerPreferencesProvider() {
+        super(new GridLayout(0,1));
+
+        escapeUnicodeCharactersCheckBox = new JCheckBox("Escape unicode characters");
+        realignLineNumbersCheckBox = new JCheckBox("Realign line numbers");
+
+        add(escapeUnicodeCharactersCheckBox);
+        add(realignLineNumbersCheckBox);
+    }
+
+    // --- PreferencesPanel --- //
+    @Override public String getPreferencesGroupTitle() { return "Decompiler"; }
+    @Override public String getPreferencesPanelTitle() { return "Class file"; }
+    @Override public JComponent getPanel() { return this; }
+
+    @Override public void init(Color errorBackgroundColor) {}
+
+    @Override public boolean isActivated() { return true; }
+
+    @Override
+    public void loadPreferences(Map<String, String> preferences) {
+        escapeUnicodeCharactersCheckBox.setSelected("true".equals(preferences.get(ESCAPE_UNICODE_CHARACTERS)));
+        realignLineNumbersCheckBox.setSelected("true".equals(preferences.get(REALIGN_LINE_NUMBERS)));
+    }
+
+    @Override
+    public void savePreferences(Map<String, String> preferences) {
+        preferences.put(ESCAPE_UNICODE_CHARACTERS, Boolean.toString(escapeUnicodeCharactersCheckBox.isSelected()));
+        preferences.put(REALIGN_LINE_NUMBERS, Boolean.toString(realignLineNumbersCheckBox.isSelected()));
+    }
+
+    @Override public boolean arePreferencesValid() { return true; }
+
+    @Override public void addPreferencesChangeListener(PreferencesPanel.PreferencesPanelChangeListener listener) {}
+}
diff --git a/services/src/main/java/org/jd/gui/service/preferencespanel/ClassFileSaverPreferencesProvider.java b/services/src/main/java/org/jd/gui/service/preferencespanel/ClassFileSaverPreferencesProvider.java
new file mode 100644
index 0000000..f7718f0
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/preferencespanel/ClassFileSaverPreferencesProvider.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.preferencespanel;
+
+import org.jd.gui.spi.PreferencesPanel;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Map;
+
+public class ClassFileSaverPreferencesProvider extends JPanel implements PreferencesPanel {
+    protected static final String WRITE_LINE_NUMBERS = "ClassFileSaverPreferences.writeLineNumbers";
+    protected static final String WRITE_METADATA = "ClassFileSaverPreferences.writeMetadata";
+
+    protected JCheckBox writeLineNumbersCheckBox;
+    protected JCheckBox writeMetadataCheckBox;
+
+    public ClassFileSaverPreferencesProvider() {
+        super(new GridLayout(0,1));
+
+        writeLineNumbersCheckBox = new JCheckBox("Write original line numbers");
+        writeMetadataCheckBox = new JCheckBox("Write metadata");
+
+        add(writeLineNumbersCheckBox);
+        add(writeMetadataCheckBox);
+    }
+
+    // --- PreferencesPanel --- //
+    @Override public String getPreferencesGroupTitle() { return "Source Saver"; }
+    @Override public String getPreferencesPanelTitle() { return "Class file"; }
+    @Override public JComponent getPanel() { return this; }
+
+    @Override public void init(Color errorBackgroundColor) {}
+
+    @Override public boolean isActivated() { return true; }
+
+    @Override
+    public void loadPreferences(Map<String, String> preferences) {
+        writeLineNumbersCheckBox.setSelected(!"false".equals(preferences.get(WRITE_LINE_NUMBERS)));
+        writeMetadataCheckBox.setSelected(!"false".equals(preferences.get(WRITE_METADATA)));
+    }
+
+    @Override
+    public void savePreferences(Map<String, String> preferences) {
+        preferences.put(WRITE_LINE_NUMBERS, Boolean.toString(writeLineNumbersCheckBox.isSelected()));
+        preferences.put(WRITE_METADATA, Boolean.toString(writeMetadataCheckBox.isSelected()));
+    }
+
+    @Override public boolean arePreferencesValid() { return true; }
+
+    @Override public void addPreferencesChangeListener(PreferencesPanel.PreferencesPanelChangeListener listener) {}
+}
diff --git a/services/src/main/java/org/jd/gui/service/preferencespanel/DirectoryIndexerPreferencesProvider.java b/services/src/main/java/org/jd/gui/service/preferencespanel/DirectoryIndexerPreferencesProvider.java
new file mode 100644
index 0000000..a7772a6
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/preferencespanel/DirectoryIndexerPreferencesProvider.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.preferencespanel;
+
+import org.jd.gui.spi.PreferencesPanel;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import java.awt.*;
+import java.util.Map;
+
+public class DirectoryIndexerPreferencesProvider extends JPanel implements PreferencesPanel, DocumentListener {
+    protected static final int MAX_VALUE = 30;
+    protected static final String MAXIMUM_DEPTH_KEY = "DirectoryIndexerPreferences.maximumDepth";
+
+    protected PreferencesPanel.PreferencesPanelChangeListener listener = null;
+    protected JTextField maximumDepthTextField;
+    protected Color errorBackgroundColor = Color.RED;
+    protected Color defaultBackgroundColor;
+
+    public DirectoryIndexerPreferencesProvider() {
+        super(new BorderLayout());
+
+        add(new JLabel("Maximum depth (1.." + MAX_VALUE + "): "), BorderLayout.WEST);
+
+        maximumDepthTextField = new JTextField();
+        maximumDepthTextField.getDocument().addDocumentListener(this);
+        add(maximumDepthTextField, BorderLayout.CENTER);
+
+        defaultBackgroundColor = maximumDepthTextField.getBackground();
+    }
+
+    // --- PreferencesPanel --- //
+    @Override public String getPreferencesGroupTitle() { return "Indexer"; }
+    @Override public String getPreferencesPanelTitle() { return "Directory exploration"; }
+    @Override public JComponent getPanel() { return this; }
+
+    @Override public void init(Color errorBackgroundColor) {
+        this.errorBackgroundColor = errorBackgroundColor;
+    }
+
+    @Override public boolean isActivated() { return true; }
+
+    @Override public void loadPreferences(Map<String, String> preferences) {
+        String preference = preferences.get(MAXIMUM_DEPTH_KEY);
+
+        maximumDepthTextField.setText((preference != null) ? preference : "15");
+        maximumDepthTextField.setCaretPosition(maximumDepthTextField.getText().length());
+    }
+
+    @Override
+    public void savePreferences(Map<String, String> preferences) {
+        preferences.put(MAXIMUM_DEPTH_KEY, maximumDepthTextField.getText());
+    }
+
+    @Override
+    public boolean arePreferencesValid() {
+        try {
+            int i = Integer.valueOf(maximumDepthTextField.getText());
+            return (i > 0) && (i <= MAX_VALUE);
+        } catch (NumberFormatException e) {
+            assert ExceptionUtil.printStackTrace(e);
+            return false;
+        }
+    }
+
+    @Override
+    public void addPreferencesChangeListener(PreferencesPanel.PreferencesPanelChangeListener listener) {
+        this.listener = listener;
+    }
+
+    // --- DocumentListener --- //
+    @Override public void insertUpdate(DocumentEvent e) { onTextChange(); }
+    @Override public void removeUpdate(DocumentEvent e) { onTextChange(); }
+    @Override public void changedUpdate(DocumentEvent e) { onTextChange(); }
+
+    public void onTextChange() {
+        maximumDepthTextField.setBackground(arePreferencesValid() ? defaultBackgroundColor : errorBackgroundColor);
+
+        if (listener != null) {
+            listener.preferencesPanelChanged(this);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/preferencespanel/MavenOrgSourceLoaderPreferencesProvider.java b/services/src/main/java/org/jd/gui/service/preferencespanel/MavenOrgSourceLoaderPreferencesProvider.java
new file mode 100644
index 0000000..49fdd94
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/preferencespanel/MavenOrgSourceLoaderPreferencesProvider.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.preferencespanel;
+
+import org.jd.gui.spi.PreferencesPanel;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+public class MavenOrgSourceLoaderPreferencesProvider extends JPanel implements PreferencesPanel, DocumentListener, ActionListener {
+    public static final String ACTIVATED = "MavenOrgSourceLoaderPreferencesProvider.activated";
+    public static final String FILTERS = "MavenOrgSourceLoaderPreferencesProvider.filters";
+
+    public static final String DEFAULT_FILTERS_VALUE =
+            "+org +com.google +com.springsource +com.sun -com +java +javax +sun +sunw " +
+            "+spring +springframework +springmodules +tomcat +maven +edu";
+
+    protected static final Pattern CONTROL_PATTERN = Pattern.compile("([+-][a-zA-Z_0-9$_.]+(\\s+[+-][a-zA-Z_0-9$_.]+)*)?\\s*");
+
+    protected JCheckBox enableCheckBox;
+    protected JTextArea filtersTextArea;
+    protected JButton resetButton;
+    protected Color errorBackgroundColor = Color.RED;
+    protected Color defaultBackgroundColor;
+
+    protected PreferencesPanel.PreferencesPanelChangeListener listener;
+
+    public MavenOrgSourceLoaderPreferencesProvider() {
+        super(new BorderLayout());
+
+        enableCheckBox = new JCheckBox("Search source code on maven.org for:");
+        enableCheckBox.addActionListener(this);
+
+        filtersTextArea = new JTextArea();
+        filtersTextArea.setFont(getFont());
+        filtersTextArea.setLineWrap(true);
+        filtersTextArea.getDocument().addDocumentListener(this);
+        defaultBackgroundColor = filtersTextArea.getBackground();
+
+        JComponent spacer = new JComponent() {};
+        JScrollPane scrollPane = new JScrollPane(filtersTextArea);
+
+        String osName = System.getProperty("os.name").toLowerCase();
+
+        if (osName.contains("windows")) {
+            spacer.setPreferredSize(new Dimension(22, -1));
+            scrollPane.setPreferredSize(new Dimension(-1, 50));
+        } else if (osName.contains("mac os")) {
+            spacer.setPreferredSize(new Dimension(28, -1));
+            scrollPane.setPreferredSize(new Dimension(-1, 56));
+        } else {
+            spacer.setPreferredSize(new Dimension(22, -1));
+            scrollPane.setPreferredSize(new Dimension(-1, 56));
+        }
+
+        resetButton = new JButton("Reset");
+        resetButton.addActionListener(this);
+
+        JPanel southPanel = new JPanel(new BorderLayout());
+        southPanel.add(resetButton, BorderLayout.EAST);
+
+        add(enableCheckBox, BorderLayout.NORTH);
+        add(spacer, BorderLayout.WEST);
+        add(scrollPane, BorderLayout.CENTER);
+        add(southPanel, BorderLayout.SOUTH);
+    }
+
+    // --- PreferencesPanel --- //
+    @Override public String getPreferencesGroupTitle() { return "Source loader"; }
+    @Override public String getPreferencesPanelTitle() { return "maven.org"; }
+    @Override public JComponent getPanel() { return this; }
+
+    @Override
+    public void init(Color errorBackgroundColor) {
+        this.errorBackgroundColor = errorBackgroundColor;
+    }
+
+    @Override public boolean isActivated() { return true; }
+
+    @Override
+    public void loadPreferences(Map<String, String> preferences) {
+        boolean enabled = !"false".equals(preferences.get(ACTIVATED));
+
+        enableCheckBox.setSelected(enabled);
+        filtersTextArea.setEnabled(enabled);
+        resetButton.setEnabled(enabled);
+
+        String filters = preferences.get(FILTERS);
+
+        filtersTextArea.setText((filters == null) || filters.isEmpty() ? DEFAULT_FILTERS_VALUE : filters);
+    }
+
+    @Override
+    public void savePreferences(Map<String, String> preferences) {
+        preferences.put(ACTIVATED, Boolean.toString(enableCheckBox.isSelected()));
+        preferences.put(FILTERS, filtersTextArea.getText().trim());
+    }
+
+    @Override public boolean arePreferencesValid() {
+        return CONTROL_PATTERN.matcher(filtersTextArea.getText()).matches();
+    }
+
+    @Override public void addPreferencesChangeListener(PreferencesPanelChangeListener listener) {
+        this.listener = listener;
+    }
+
+
+    // --- DocumentListener --- //
+    @Override public void insertUpdate(DocumentEvent e) { onTextChange(); }
+    @Override public void removeUpdate(DocumentEvent e) { onTextChange(); }
+    @Override public void changedUpdate(DocumentEvent e) { onTextChange(); }
+
+    protected void onTextChange() {
+        filtersTextArea.setBackground(arePreferencesValid() ? defaultBackgroundColor : errorBackgroundColor);
+
+        if (listener != null) {
+            listener.preferencesPanelChanged(this);
+        }
+    }
+
+    // --- ActionListener --- //
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        if (e.getSource() == enableCheckBox) {
+            boolean enabled = enableCheckBox.isSelected();
+            filtersTextArea.setEnabled(enabled);
+            resetButton.setEnabled(enabled);
+        } else {
+            // Reset button
+            filtersTextArea.setText(DEFAULT_FILTERS_VALUE);
+            filtersTextArea.requestFocus();
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/preferencespanel/ViewerPreferencesProvider.java b/services/src/main/java/org/jd/gui/service/preferencespanel/ViewerPreferencesProvider.java
new file mode 100644
index 0000000..932f2ec
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/preferencespanel/ViewerPreferencesProvider.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.preferencespanel;
+
+import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
+import org.fife.ui.rsyntaxtextarea.Theme;
+import org.jd.gui.spi.PreferencesPanel;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import java.awt.*;
+import java.io.IOException;
+import java.util.Map;
+
+public class ViewerPreferencesProvider extends JPanel implements PreferencesPanel, DocumentListener {
+    protected static final int MIN_VALUE = 2;
+    protected static final int MAX_VALUE = 40;
+    protected static final String FONT_SIZE_KEY = "ViewerPreferences.fontSize";
+
+    protected PreferencesPanel.PreferencesPanelChangeListener listener = null;
+    protected JTextField fontSizeTextField;
+    protected Color errorBackgroundColor = Color.RED;
+    protected Color defaultBackgroundColor;
+
+    public ViewerPreferencesProvider() {
+        super(new BorderLayout());
+
+        add(new JLabel("Font size (" + MIN_VALUE + ".." + MAX_VALUE + "): "), BorderLayout.WEST);
+
+        fontSizeTextField = new JTextField();
+        fontSizeTextField.getDocument().addDocumentListener(this);
+        add(fontSizeTextField, BorderLayout.CENTER);
+
+        defaultBackgroundColor = fontSizeTextField.getBackground();
+    }
+
+    // --- PreferencesPanel --- //
+    @Override public String getPreferencesGroupTitle() { return "Viewer"; }
+    @Override public String getPreferencesPanelTitle() { return "Appearance"; }
+    @Override public JComponent getPanel() { return this; }
+
+    @Override public void init(Color errorBackgroundColor) {
+        this.errorBackgroundColor = errorBackgroundColor;
+    }
+
+    @Override public boolean isActivated() { return true; }
+
+    @Override
+    public void loadPreferences(Map<String, String> preferences) {
+        String fontSize = preferences.get(FONT_SIZE_KEY);
+
+        if (fontSize == null) {
+            // Search default value for the current platform
+            RSyntaxTextArea textArea = new RSyntaxTextArea();
+
+            try {
+                Theme theme = Theme.load(getClass().getClassLoader().getResourceAsStream("rsyntaxtextarea/themes/eclipse.xml"));
+                theme.apply(textArea);
+            } catch (IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+
+            fontSize = String.valueOf(textArea.getFont().getSize());
+        }
+
+        fontSizeTextField.setText(fontSize);
+        fontSizeTextField.setCaretPosition(fontSizeTextField.getText().length());
+    }
+
+    @Override
+    public void savePreferences(Map<String, String> preferences) {
+        preferences.put(FONT_SIZE_KEY, fontSizeTextField.getText());
+    }
+
+    @Override
+    public boolean arePreferencesValid() {
+        try {
+            int i = Integer.valueOf(fontSizeTextField.getText());
+            return (i >= MIN_VALUE) && (i <= MAX_VALUE);
+        } catch (NumberFormatException e) {
+            assert ExceptionUtil.printStackTrace(e);
+            return false;
+        }
+    }
+
+    @Override
+    public void addPreferencesChangeListener(PreferencesPanel.PreferencesPanelChangeListener listener) {
+        this.listener = listener;
+    }
+
+    // --- DocumentListener --- //
+    @Override public void insertUpdate(DocumentEvent e) { onTextChange(); }
+    @Override public void removeUpdate(DocumentEvent e) { onTextChange(); }
+    @Override public void changedUpdate(DocumentEvent e) { onTextChange(); }
+
+    public void onTextChange() {
+        fontSizeTextField.setBackground(arePreferencesValid() ? defaultBackgroundColor : errorBackgroundColor);
+
+        if (listener != null) {
+            listener.preferencesPanelChanged(this);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/sourceloader/MavenOrgSourceLoaderProvider.java b/services/src/main/java/org/jd/gui/service/sourceloader/MavenOrgSourceLoaderProvider.java
new file mode 100644
index 0000000..b1bf948
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/sourceloader/MavenOrgSourceLoaderProvider.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.sourceloader;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.service.preferencespanel.MavenOrgSourceLoaderPreferencesProvider;
+import org.jd.gui.spi.SourceLoader;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamReader;
+import java.io.*;
+import java.net.URL;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class MavenOrgSourceLoaderProvider implements SourceLoader {
+    protected static final String MAVENORG_SEARCH_URL_PREFIX = "https://search.maven.org/solrsearch/select?q=1:%22";
+    protected static final String MAVENORG_SEARCH_URL_SUFFIX = "%22&rows=20&wt=xml";
+
+    protected static final String MAVENORG_LOAD_URL_PREFIX = "https://search.maven.org/classic/remotecontent?filepath=";
+    protected static final String MAVENORG_LOAD_URL_SUFFIX = "-sources.jar";
+
+    protected HashSet<Container.Entry> failed = new HashSet<>();
+    protected HashMap<Container.Entry, File> cache = new HashMap<>();
+
+    @Override
+    public String getSource(API api, Container.Entry entry) {
+        if (isActivated(api)) {
+            String filters = api.getPreferences().get(MavenOrgSourceLoaderPreferencesProvider.FILTERS);
+
+            if ((filters == null) || filters.isEmpty()) {
+                filters = MavenOrgSourceLoaderPreferencesProvider.DEFAULT_FILTERS_VALUE;
+            }
+
+            if (accepted(filters, entry.getPath())) {
+                return searchSource(entry, cache.get(entry.getContainer().getRoot().getParent()));
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public String loadSource(API api, Container.Entry entry) {
+        if (isActivated(api)) {
+            String filters = api.getPreferences().get(MavenOrgSourceLoaderPreferencesProvider.FILTERS);
+
+            if ((filters == null) || filters.isEmpty()) {
+                filters = MavenOrgSourceLoaderPreferencesProvider.DEFAULT_FILTERS_VALUE;
+            }
+
+            if (accepted(filters, entry.getPath())) {
+                return searchSource(entry, downloadSourceJarFile(entry.getContainer().getRoot().getParent()));
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public File loadSourceFile(API api, Container.Entry entry) {
+        return isActivated(api) ? downloadSourceJarFile(entry) : null;
+    }
+
+    private static boolean isActivated(API api) {
+        return !"false".equals(api.getPreferences().get(MavenOrgSourceLoaderPreferencesProvider.ACTIVATED));
+    }
+
+    protected String searchSource(Container.Entry entry, File sourceJarFile) {
+        if (sourceJarFile != null) {
+            byte[] buffer = new byte[1024 * 2];
+
+            try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(sourceJarFile)))) {
+                ZipEntry ze = zis.getNextEntry();
+                String name = entry.getPath();
+
+                name = name.substring(0, name.length()-6) + ".java"; // 6 = ".class".length()
+
+                while (ze != null) {
+                    if (ze.getName().equals(name)) {
+                        ByteArrayOutputStream out = new ByteArrayOutputStream();
+                        int read = zis.read(buffer);
+
+                        while (read > 0) {
+                            out.write(buffer, 0, read);
+                            read = zis.read(buffer);
+                        }
+
+                        return new String(out.toByteArray(), "UTF-8");
+                    }
+
+                    ze = zis.getNextEntry();
+                }
+
+                zis.closeEntry();
+            } catch (IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+
+        return null;
+    }
+
+    protected File downloadSourceJarFile(Container.Entry entry) {
+        if (cache.containsKey(entry)) {
+            return cache.get(entry);
+        }
+
+        if (!entry.isDirectory() && !failed.contains(entry)) {
+            try {
+                // SHA-1
+                MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
+                byte[] buffer = new byte[1024 * 2];
+
+                try (DigestInputStream is = new DigestInputStream(entry.getInputStream(), messageDigest)) {
+                    while (is.read(buffer) > -1);
+                }
+
+                byte[] array = messageDigest.digest();
+                StringBuilder sb = new StringBuilder();
+
+                for (byte b : array) {
+                    sb.append(hexa((b & 255) >> 4));
+                    sb.append(hexa(b & 15));
+                }
+
+                String sha1 = sb.toString();
+
+                // Search artifact on maven.org
+                URL searchUrl = new URL(MAVENORG_SEARCH_URL_PREFIX + sha1 + MAVENORG_SEARCH_URL_SUFFIX);
+                boolean sourceAvailable = false;
+                String id = null;
+                String numFound = null;
+
+                try (InputStream is = searchUrl.openStream()) {
+                    XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(is);
+                    String name = "";
+
+                    while (reader.hasNext()) {
+                        switch (reader.next()) {
+                            case XMLStreamConstants.START_ELEMENT:
+                                if ("str".equals(reader.getLocalName())) {
+                                    if ("id".equals(reader.getAttributeValue(null, "name"))) {
+                                        name = "id";
+                                    } else {
+                                        name = "str";
+                                    }
+                                } else if ("result".equals(reader.getLocalName())) {
+                                    numFound = reader.getAttributeValue(null, "numFound");
+                                } else {
+                                    name = "";
+                                }
+                                break;
+                            case XMLStreamConstants.CHARACTERS:
+                                switch (name) {
+                                    case "id":
+                                        id = reader.getText().trim();
+                                        break;
+                                    case "str":
+                                        sourceAvailable |= "-sources.jar".equals(reader.getText().trim());
+                                        break;
+                                }
+                                break;
+                        }
+                    }
+
+                    reader.close();
+                }
+
+                String groupId=null, artifactId=null, version=null;
+
+                if ("0".equals(numFound)) {
+                    // File not indexed by Apache Solr of maven.org -> Try to found groupId, artifactId, version in 'pom.properties'
+                    Properties pomProperties = getPomProperties(entry);
+
+                    if (pomProperties != null) {
+                        groupId = pomProperties.getProperty("groupId");
+                        artifactId = pomProperties.getProperty("artifactId");
+                        version = pomProperties.getProperty("version");
+                    }
+                } else if ("1".equals(numFound) && sourceAvailable) {
+                    int index1 = id.indexOf(':');
+                    int index2 = id.lastIndexOf(':');
+
+                    groupId = id.substring(0, index1);
+                    artifactId = id.substring(index1+1, index2);
+                    version = id.substring(index2+1);
+                }
+
+                if (artifactId != null) {
+                    // Load source
+                    String filePath = groupId.replace('.', '/') + '/' + artifactId + '/' + version + '/' + artifactId + '-' + version;
+                    URL loadUrl = new URL(MAVENORG_LOAD_URL_PREFIX + filePath + MAVENORG_LOAD_URL_SUFFIX);
+                    File tmpFile = File.createTempFile("jd-gui.tmp.", '.' + groupId + '_' + artifactId + '_' + version + "-sources.jar");
+
+                    tmpFile.delete();
+                    tmpFile.deleteOnExit();
+
+                    try (InputStream is = new BufferedInputStream(loadUrl.openStream()); OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile))) {
+                        int read = is.read(buffer);
+                        while (read > 0) {
+                            os.write(buffer, 0, read);
+                            read = is.read(buffer);
+                        }
+                    }
+
+                    cache.put(entry, tmpFile);
+                    return tmpFile;
+                }
+            } catch (Exception e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+
+        failed.add(entry);
+        return null;
+    }
+
+    private static Properties getPomProperties(Container.Entry parent) {
+        // Search 'META-INF/maven/*/*/pom.properties'
+        for (Container.Entry child1 : parent.getChildren()) {
+            if (child1.isDirectory() && child1.getPath().equals("META-INF")) {
+                for (Container.Entry child2 : child1.getChildren()) {
+                    if (child2.isDirectory() && child2.getPath().equals("META-INF/maven")) {
+                        if (child2.isDirectory()) {
+                            Collection<Container.Entry> children = child2.getChildren();
+                            if (children.size() == 1) {
+                                Container.Entry entry = children.iterator().next();
+                                if (entry.isDirectory()) {
+                                    children = entry.getChildren();
+                                    if (children.size() == 1) {
+                                        entry = children.iterator().next();
+                                        for (Container.Entry child3 : entry.getChildren()) {
+                                            if (!child3.isDirectory() && child3.getPath().endsWith("/pom.properties")) {
+                                                // Load properties
+                                                try (InputStream is = child3.getInputStream()) {
+                                                    Properties properties = new Properties();
+                                                    properties.load(is);
+                                                    return properties;
+                                                } catch (Exception e) {
+                                                    assert ExceptionUtil.printStackTrace(e);
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private static char hexa(int i) { return (char)( (i <= 9) ? ('0' + i) : (('a' - 10) + i) ); }
+
+    protected boolean accepted(String filters, String path) {
+        // 'filters' example : '+org +com.google +com.ibm +com.jcraft +com.springsource +com.sun -com +java +javax +sun +sunw'
+        StringTokenizer tokenizer = new StringTokenizer(filters);
+
+        while (tokenizer.hasMoreTokens()) {
+            String filter = tokenizer.nextToken();
+
+            if (filter.length() > 1) {
+                String prefix = filter.substring(1).replace('.', '/');
+
+                if (prefix.charAt(prefix.length() - 1) != '/') {
+                    prefix += '/';
+                }
+
+                if (path.startsWith(prefix)) {
+                    return (filter.charAt(0) == '+');
+                }
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/sourcesaver/AbstractSourceSaverProvider.java b/services/src/main/java/org/jd/gui/service/sourcesaver/AbstractSourceSaverProvider.java
new file mode 100644
index 0000000..9b8230d
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/sourcesaver/AbstractSourceSaverProvider.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.sourcesaver;
+
+import org.jd.gui.spi.SourceSaver;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+import java.util.regex.Pattern;
+
+public abstract class AbstractSourceSaverProvider implements SourceSaver {
+    protected List<String> externalSelectors;
+    protected Pattern externalPathPattern;
+
+    /**
+     * Initialize "selectors" and "pathPattern" with optional external properties file
+     */
+    public AbstractSourceSaverProvider() {
+        Properties properties = new Properties();
+        Class clazz = this.getClass();
+
+        try (InputStream is = clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/') + ".properties")) {
+            if (is != null) {
+                properties.load(is);
+            }
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        init(properties);
+    }
+
+    protected void init(Properties properties) {
+        String selectors = properties.getProperty("selectors");
+
+        if (selectors != null) {
+            externalSelectors = Arrays.asList(selectors.split(","));
+        }
+
+        String pathRegExp = properties.getProperty("pathRegExp");
+
+        if (pathRegExp != null) {
+            externalPathPattern = Pattern.compile(pathRegExp);
+        }
+    }
+
+    protected String[] appendSelectors(String selector) {
+        if (externalSelectors == null) {
+            return new String[] { selector };
+        } else {
+            int size = externalSelectors.size();
+            String[] array = new String[size+1];
+            externalSelectors.toArray(array);
+            array[size] = selector;
+            return array;
+        }
+    }
+
+    protected String[] appendSelectors(String... selectors) {
+        if (externalSelectors == null) {
+            return selectors;
+        } else {
+            int size = externalSelectors.size();
+            String[] array = new String[size+selectors.length];
+            externalSelectors.toArray(array);
+            System.arraycopy(selectors, 0, array, size, selectors.length);
+            return array;
+        }
+    }
+
+    public Pattern getPathPattern() { return externalPathPattern; }
+}
diff --git a/services/src/main/java/org/jd/gui/service/sourcesaver/ClassFileSourceSaverProvider.java b/services/src/main/java/org/jd/gui/service/sourcesaver/ClassFileSourceSaverProvider.java
new file mode 100644
index 0000000..bd39cd7
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/sourcesaver/ClassFileSourceSaverProvider.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.sourcesaver;
+
+import org.jd.core.v1.ClassFileToJavaSourceDecompiler;
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.util.decompiler.ContainerLoader;
+import org.jd.gui.util.decompiler.LineNumberStringBuilderPrinter;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.io.NewlineOutputStream;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ClassFileSourceSaverProvider extends AbstractSourceSaverProvider {
+    protected static final String ESCAPE_UNICODE_CHARACTERS = "ClassFileDecompilerPreferences.escapeUnicodeCharacters";
+    protected static final String REALIGN_LINE_NUMBERS      = "ClassFileDecompilerPreferences.realignLineNumbers";
+    protected static final String WRITE_LINE_NUMBERS        = "ClassFileSaverPreferences.writeLineNumbers";
+    protected static final String WRITE_METADATA            = "ClassFileSaverPreferences.writeMetadata";
+    protected static final String JD_CORE_VERSION           = "JdGuiPreferences.jdCoreVersion";
+
+    protected static final ClassFileToJavaSourceDecompiler DECOMPILER = new ClassFileToJavaSourceDecompiler();
+
+    protected ContainerLoader loader = new ContainerLoader();
+    protected LineNumberStringBuilderPrinter printer = new LineNumberStringBuilderPrinter();
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.class"); }
+
+    @Override
+    public String getSourcePath(Container.Entry entry) {
+        String path = entry.getPath();
+        int index = path.lastIndexOf('.');
+        String prefix = (index == -1) ? path : path.substring(0, index);
+        return prefix + ".java";
+    }
+
+    @Override
+    public int getFileCount(API api, Container.Entry entry) {
+        if (entry.getPath().indexOf('$') == -1) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public void save(API api, Controller controller, Listener listener, Path rootPath, Container.Entry entry) {
+        String sourcePath = getSourcePath(entry);
+        Path path = rootPath.resolve(sourcePath);
+
+        saveContent(api, controller, listener, rootPath, path, entry);
+    }
+
+    @Override
+    public void saveContent(API api, Controller controller, Listener listener, Path rootPath, Path path, Container.Entry entry) {
+        try {
+            // Call listener
+            if (path.toString().indexOf('$') == -1) {
+                listener.pathSaved(path);
+            }
+            // Init preferences
+            Map<String, String> preferences = api.getPreferences();
+            boolean realignmentLineNumbers = getPreferenceValue(preferences, REALIGN_LINE_NUMBERS, true);
+            boolean unicodeEscape = getPreferenceValue(preferences, ESCAPE_UNICODE_CHARACTERS, false);
+            boolean showLineNumbers = getPreferenceValue(preferences, WRITE_LINE_NUMBERS, true);
+
+            Map<String, Object> configuration = new HashMap<>();
+            configuration.put("realignLineNumbers", realignmentLineNumbers);
+
+            // Init loader
+            loader.setEntry(entry);
+
+            // Init printer
+            printer.setRealignmentLineNumber(realignmentLineNumbers);
+            printer.setUnicodeEscape(unicodeEscape);
+            printer.setShowLineNumbers(showLineNumbers);
+
+            // Format internal name
+            String entryPath = entry.getPath();
+            assert entryPath.endsWith(".class");
+            String entryInternalName = entryPath.substring(0, entryPath.length() - 6); // 6 = ".class".length()
+
+            // Decompile class file
+            DECOMPILER.decompile(loader, printer, entryInternalName, configuration);
+
+            StringBuilder stringBuffer = printer.getStringBuffer();
+
+            // Metadata
+            if (getPreferenceValue(preferences, WRITE_METADATA, true)) {
+                // Add location
+                String location =
+                    new File(entry.getUri()).getPath()
+                    // Escape "\ u" sequence to prevent "Invalid unicode" errors
+                    .replaceAll("(^|[^\\\\])\\\\u", "\\\\\\\\u");
+                stringBuffer.append("\n\n/* Location:              ");
+                stringBuffer.append(location);
+                // Add Java compiler version
+                int majorVersion = printer.getMajorVersion();
+
+                if (majorVersion >= 45) {
+                    stringBuffer.append("\n * Java compiler version: ");
+
+                    if (majorVersion >= 49) {
+                        stringBuffer.append(majorVersion - (49 - 5));
+                    } else {
+                        stringBuffer.append(majorVersion - (45 - 1));
+                    }
+
+                    stringBuffer.append(" (");
+                    stringBuffer.append(majorVersion);
+                    stringBuffer.append('.');
+                    stringBuffer.append(printer.getMinorVersion());
+                    stringBuffer.append(')');
+                }
+                // Add JD-Core version
+                stringBuffer.append("\n * JD-Core Version:       ");
+                stringBuffer.append(preferences.get(JD_CORE_VERSION));
+                stringBuffer.append("\n */");
+            }
+
+            try (PrintStream ps = new PrintStream(new NewlineOutputStream(Files.newOutputStream(path)), true, "UTF-8")) {
+                ps.print(stringBuffer.toString());
+            } catch (IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        } catch (Throwable t) {
+            assert ExceptionUtil.printStackTrace(t);
+
+            try (BufferedWriter writer = Files.newBufferedWriter(path, Charset.defaultCharset())) {
+                writer.write("// INTERNAL ERROR //");
+            } catch (IOException ee) {
+                assert ExceptionUtil.printStackTrace(ee);
+            }
+        }
+    }
+
+    protected static boolean getPreferenceValue(Map<String, String> preferences, String key, boolean defaultValue) {
+        String v = preferences.get(key);
+        return (v == null) ? defaultValue : Boolean.valueOf(v);
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/sourcesaver/DirectorySourceSaverProvider.java b/services/src/main/java/org/jd/gui/service/sourcesaver/DirectorySourceSaverProvider.java
new file mode 100644
index 0000000..9d1143b
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/sourcesaver/DirectorySourceSaverProvider.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.sourcesaver;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.spi.SourceSaver;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+
+public class DirectorySourceSaverProvider extends AbstractSourceSaverProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("*:dir:*"); }
+
+    @Override public String getSourcePath(Container.Entry entry) { return entry.getPath() + ".src.zip"; }
+
+    @Override public int getFileCount(API api, Container.Entry entry) { return getFileCount(api, entry.getChildren()); }
+
+    protected int getFileCount(API api, Collection<Container.Entry> entries) {
+        int count = 0;
+
+        for (Container.Entry e : entries) {
+            SourceSaver sourceSaver = api.getSourceSaver(e);
+
+            if (sourceSaver != null) {
+                count += sourceSaver.getFileCount(api, e);
+            }
+        }
+
+        return count;
+    }
+
+    @Override
+    public void save(API api, SourceSaver.Controller controller, SourceSaver.Listener listener, Path rootPath, Container.Entry entry) {
+        Path path = rootPath.resolve(entry.getPath());
+
+        try {
+            Files.createDirectories(path);
+            saveContent(api, controller, listener, rootPath, path, entry);
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    @Override
+    public void saveContent(API api, SourceSaver.Controller controller, SourceSaver.Listener listener, Path rootPath, Path path, Container.Entry entry) {
+        for (Container.Entry e : getChildren(entry)) {
+            if (controller.isCancelled()) {
+                break;
+            }
+
+            SourceSaver sourceSaver = api.getSourceSaver(e);
+
+            if (sourceSaver != null) {
+                sourceSaver.save(api, controller, listener, rootPath, e);
+            }
+        }
+    }
+
+    protected Collection<Container.Entry> getChildren(Container.Entry entry) { return entry.getChildren(); }
+}
diff --git a/services/src/main/java/org/jd/gui/service/sourcesaver/FileSourceSaverProvider.java b/services/src/main/java/org/jd/gui/service/sourcesaver/FileSourceSaverProvider.java
new file mode 100644
index 0000000..93f57b9
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/sourcesaver/FileSourceSaverProvider.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.sourcesaver;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.spi.SourceSaver;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+
+public class FileSourceSaverProvider extends AbstractSourceSaverProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*"); }
+
+    @Override public String getSourcePath(Container.Entry entry) { return entry.getPath(); }
+
+    @Override public int getFileCount(API api, Container.Entry entry) { return 1; }
+
+    @Override
+    public void save(API api, SourceSaver.Controller controller, SourceSaver.Listener listener, Path rootPath, Container.Entry entry) {
+        saveContent(api, controller, listener, rootPath, rootPath.resolve(entry.getPath()), entry);
+    }
+
+    @Override
+    public void saveContent(API api, SourceSaver.Controller controller, SourceSaver.Listener listener, Path rootPath, Path path, Container.Entry entry) {
+        listener.pathSaved(path);
+
+        try (InputStream is = entry.getInputStream()) {
+            Files.copy(is, path, StandardCopyOption.REPLACE_EXISTING);
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+
+            try (BufferedWriter writer = Files.newBufferedWriter(path, Charset.defaultCharset())) {
+                writer.write("// INTERNAL ERROR //");
+            } catch (IOException ee) {
+                assert ExceptionUtil.printStackTrace(ee);
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/sourcesaver/PackageSourceSaverProvider.java b/services/src/main/java/org/jd/gui/service/sourcesaver/PackageSourceSaverProvider.java
new file mode 100644
index 0000000..7ba66e0
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/sourcesaver/PackageSourceSaverProvider.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.sourcesaver;
+
+import org.jd.gui.api.model.Container;
+import org.jd.gui.util.container.JarContainerEntryUtil;
+
+import java.util.Collection;
+
+public class PackageSourceSaverProvider extends DirectorySourceSaverProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("jar:dir:*", "war:dir:*", "ear:dir:*"); }
+
+    @Override
+    protected Collection<Container.Entry> getChildren(Container.Entry entry) {
+        return JarContainerEntryUtil.removeInnerTypeEntries(entry.getChildren());
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/sourcesaver/ZipFileSourceSaverProvider.java b/services/src/main/java/org/jd/gui/service/sourcesaver/ZipFileSourceSaverProvider.java
new file mode 100644
index 0000000..4836a34
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/sourcesaver/ZipFileSourceSaverProvider.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.sourcesaver;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.spi.SourceSaver;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+
+public class ZipFileSourceSaverProvider extends DirectorySourceSaverProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.zip", "*:file:*.jar", "*:file:*.war", "*:file:*.ear", "*:file:*.aar", "*:file:*.jmod", "*:file:*.kar"); }
+
+    @Override
+    public void save(API api, SourceSaver.Controller controller, SourceSaver.Listener listener, Path rootPath, Container.Entry entry) {
+        try {
+            String sourcePath = getSourcePath(entry);
+            Path path = rootPath.resolve(sourcePath);
+            Path parentPath = path.getParent();
+
+            if ((parentPath != null) && !Files.exists(parentPath)) {
+                Files.createDirectories(parentPath);
+            }
+
+            File tmpSourceFile = api.loadSourceFile(entry);
+
+            if (tmpSourceFile != null) {
+                Files.copy(tmpSourceFile.toPath(), path);
+            } else {
+                File tmpFile = File.createTempFile("jd-gui.", ".tmp.zip");
+
+                tmpFile.delete();
+                tmpFile.deleteOnExit();
+
+                URI tmpFileUri = tmpFile.toURI();
+                URI tmpArchiveUri = new URI("jar:" + tmpFileUri.getScheme(), tmpFileUri.getHost(), tmpFileUri.getPath() + "!/", null);
+
+                HashMap<String, String> env = new HashMap<>();
+                env.put("create", "true");
+
+                FileSystem tmpArchiveFs = FileSystems.newFileSystem(tmpArchiveUri, env);
+                Path tmpArchiveRootPath = tmpArchiveFs.getPath("/");
+
+                saveContent(api, controller, listener, tmpArchiveRootPath, tmpArchiveRootPath, entry);
+
+                tmpArchiveFs.close();
+
+                Files.move(tmpFile.toPath(), path);
+            }
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/AbstractTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/AbstractTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..2bb0be6
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/AbstractTreeNodeFactoryProvider.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.spi.TreeNodeFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+import java.util.regex.Pattern;
+
+public abstract class AbstractTreeNodeFactoryProvider implements TreeNodeFactory {
+    protected List<String> externalSelectors;
+    protected Pattern externalPathPattern;
+
+    /**
+     * Initialize "selectors" and "pathPattern" with optional external properties file
+     */
+    public AbstractTreeNodeFactoryProvider() {
+        Properties properties = new Properties();
+        Class clazz = this.getClass();
+
+        try (InputStream is = clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/') + ".properties")) {
+            if (is != null) {
+                properties.load(is);
+            }
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        init(properties);
+    }
+
+    protected void init(Properties properties) {
+        String selectors = properties.getProperty("selectors");
+
+        if (selectors != null) {
+            externalSelectors = Arrays.asList(selectors.split(","));
+        }
+
+        String pathRegExp = properties.getProperty("pathRegExp");
+
+        if (pathRegExp != null) {
+            externalPathPattern = Pattern.compile(pathRegExp);
+        }
+    }
+
+    protected String[] appendSelectors(String selector) {
+        if (externalSelectors == null) {
+            return new String[] { selector };
+        } else {
+            int size = externalSelectors.size();
+            String[] array = new String[size+1];
+            externalSelectors.toArray(array);
+            array[size] = selector;
+            return array;
+        }
+    }
+
+    protected String[] appendSelectors(String... selectors) {
+        if (externalSelectors == null) {
+            return selectors;
+        } else {
+            int size = externalSelectors.size();
+            String[] array = new String[size+selectors.length];
+            externalSelectors.toArray(array);
+            System.arraycopy(selectors, 0, array, size, selectors.length);
+            return array;
+        }
+    }
+
+    @Override public Pattern getPathPattern() { return externalPathPattern; }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/AbstractTypeFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/AbstractTypeFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..f606257
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/AbstractTypeFileTreeNodeFactoryProvider.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.PageCreator;
+import org.jd.gui.api.feature.TreeNodeExpandable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Type;
+import org.jd.gui.spi.TypeFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+
+public abstract class AbstractTypeFileTreeNodeFactoryProvider extends AbstractTreeNodeFactoryProvider {
+    protected static final TypeComparator TYPE_COMPARATOR = new TypeComparator();
+    protected static final FieldOrMethodBeanComparator FIELD_OR_METHOD_BEAN_COMPARATOR = new FieldOrMethodBeanComparator();
+
+    public static class BaseTreeNode extends DefaultMutableTreeNode implements ContainerEntryGettable, UriGettable, PageCreator {
+        protected Container.Entry entry;
+        protected PageAndTipFactory factory;
+        protected URI uri;
+
+        public BaseTreeNode(Container.Entry entry, String fragment, Object userObject, PageAndTipFactory factory) {
+            super(userObject);
+            this.entry = entry;
+            this.factory = factory;
+
+            if (fragment != null) {
+                try {
+                    URI uri = entry.getUri();
+                    this.uri = new URI(uri.getScheme(), uri.getHost(), uri.getPath(), fragment);
+                } catch (URISyntaxException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            } else {
+                this.uri = entry.getUri();
+            }
+        }
+
+        // --- ContainerEntryGettable --- //
+        @Override public Container.Entry getEntry() { return entry; }
+
+        // --- UriGettable --- //
+        @Override public URI getUri() { return uri; }
+
+        // --- PageCreator --- //
+        @Override
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            // Lazy 'tip' initialization
+            ((TreeNodeBean)userObject).setTip(factory.makeTip(api, entry));
+            return factory.makePage(api, entry);
+        }
+    }
+
+    protected static class FileTreeNode extends BaseTreeNode implements TreeNodeExpandable {
+        protected boolean initialized;
+
+        public FileTreeNode(Container.Entry entry, Object userObject, PageAndTipFactory pageAndTipFactory) {
+            this(entry, null, userObject, pageAndTipFactory);
+        }
+
+        public FileTreeNode(Container.Entry entry, String fragment, Object userObject, PageAndTipFactory factory) {
+            super(entry, fragment, userObject, factory);
+            initialized = false;
+            // Add dummy node
+            add(new DefaultMutableTreeNode());
+        }
+
+        // --- TreeNodeExpandable --- //
+        @Override
+        public void populateTreeNode(API api) {
+            if (!initialized) {
+                removeAllChildren();
+                // Create type node
+                TypeFactory typeFactory = api.getTypeFactory(entry);
+
+                if (typeFactory != null) {
+                    Collection<Type> types = typeFactory.make(api, entry);
+
+                    for (Type type : types) {
+                        add(new TypeTreeNode(entry, type, new TreeNodeBean(type.getDisplayTypeName(), type.getIcon()), factory));
+                    }
+                }
+                
+                initialized = true;
+            }
+        }
+    }
+
+    protected static class TypeTreeNode extends BaseTreeNode implements TreeNodeExpandable {
+        protected boolean initialized;
+        protected Type type;
+
+        public TypeTreeNode(Container.Entry entry, Type type, Object userObject, PageAndTipFactory factory) {
+            super(entry, type.getName(), userObject, factory);
+            this.initialized = false;
+            this.type = type;
+            // Add dummy node
+            add(new DefaultMutableTreeNode());
+        }
+
+        // --- TreeNodeExpandable --- //
+        @Override
+        public void populateTreeNode(API api) {
+            if (!initialized) {
+                removeAllChildren();
+
+                String typeName = type.getName();
+
+                // Create inner types
+                Collection<Type> innerTypes = type.getInnerTypes();
+
+                if (innerTypes != null) {
+                    ArrayList<Type> innerTypeList = new ArrayList<>(innerTypes);
+                    innerTypeList.sort(TYPE_COMPARATOR);
+
+                    for (Type innerType : innerTypeList) {
+                        add(new TypeTreeNode(entry, innerType, new TreeNodeBean(innerType.getDisplayInnerTypeName(), innerType.getIcon()), factory));
+                    }
+                }
+
+                // Create fields
+                Collection<Type.Field> fields = type.getFields();
+
+                if (fields != null) {
+                    ArrayList<FieldOrMethodBean> beans = new ArrayList<>(fields.size());
+
+                    for (Type.Field field : fields) {
+                        String fragment = typeName + '-' + field.getName() + '-' + field.getDescriptor();
+                        beans.add(new FieldOrMethodBean(fragment, field.getDisplayName(), field.getIcon()));
+                    }
+
+                    beans.sort(FIELD_OR_METHOD_BEAN_COMPARATOR);
+
+                    for (FieldOrMethodBean bean : beans) {
+                        add(new FieldOrMethodTreeNode(entry, bean.fragment, new TreeNodeBean(bean.label, bean.icon), factory));
+                    }
+                }
+
+                // Create methods
+                Collection<Type.Method> methods = type.getMethods();
+
+                if (methods != null) {
+                    ArrayList<FieldOrMethodBean> beans = new ArrayList<>();
+
+                    for (Type.Method method : methods) {
+                        if (!method.getName().equals("<clinit>")) {
+                            String fragment = typeName + '-' + method.getName() + '-' + method.getDescriptor();
+                            beans.add(new FieldOrMethodBean(fragment, method.getDisplayName(), method.getIcon()));
+                        }
+                    }
+
+                    beans.sort(FIELD_OR_METHOD_BEAN_COMPARATOR);
+
+                    for (FieldOrMethodBean bean : beans) {
+                        add(new FieldOrMethodTreeNode(entry, bean.fragment, new TreeNodeBean(bean.label, bean.icon), factory));
+                    }
+                }
+
+                initialized = true;
+            }
+        }
+    }
+
+    protected static class FieldOrMethodTreeNode extends BaseTreeNode {
+        public FieldOrMethodTreeNode(Container.Entry entry, String fragment, Object userObject, PageAndTipFactory factory) {
+            super(entry, fragment, userObject, factory);
+        }
+    }
+
+    protected static class FieldOrMethodBean {
+        public String fragment, label;
+        public Icon icon;
+
+        public FieldOrMethodBean(String fragment, String label, Icon icon) {
+            this.fragment = fragment;
+            this.label = label;
+            this.icon = icon;
+        }
+    }
+
+    protected interface PageAndTipFactory {
+        <T extends JComponent & UriGettable> T makePage(API api, Container.Entry entry);
+        String makeTip(API api, Container.Entry entry);
+    }
+
+    protected static class TypeComparator implements Comparator<Type> {
+        @Override
+        public int compare(Type type1, Type type2) {
+            return type1.getName().compareTo(type2.getName());
+        }
+    }
+
+    protected static class FieldOrMethodBeanComparator implements Comparator<FieldOrMethodBean> {
+        @Override
+        public int compare(FieldOrMethodBean bean1, FieldOrMethodBean bean2) {
+            return bean1.label.compareTo(bean2.label);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/ClassFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/ClassFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..a1ae468
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/ClassFileTreeNodeFactoryProvider.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.view.component.DynamicPage;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.regex.Pattern;
+
+public class ClassFileTreeNodeFactoryProvider extends AbstractTypeFileTreeNodeFactoryProvider {
+    protected static final ImageIcon CLASS_FILE_ICON = new ImageIcon(ClassFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/classf_obj.png"));
+    protected static final Factory FACTORY = new Factory();
+
+    static {
+        // Early class loading
+        try {
+            Class.forName(DynamicPage.class.getName());
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.class"); }
+
+    @Override
+    public Pattern getPathPattern() {
+        if (externalPathPattern == null) {
+            return Pattern.compile("^((?!module-info\\.class).)*$");
+        } else {
+            return externalPathPattern;
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf('/');
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        return (T)new FileTreeNode(entry, new TreeNodeBean(label, CLASS_FILE_ICON), FACTORY);
+    }
+
+    protected static class Factory implements AbstractTypeFileTreeNodeFactoryProvider.PageAndTipFactory {
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T makePage(API a, Container.Entry e) {
+            return (T)new DynamicPage(a, e);
+        }
+
+        @Override
+        public String makeTip(API api, Container.Entry entry) {
+            String location = new File(entry.getUri()).getPath();
+            StringBuilder tip = new StringBuilder("<html>Location: ");
+
+            tip.append(location);
+            tip.append("<br>Java compiler version: ");
+
+            try (InputStream is = entry.getInputStream()) {
+                is.skip(4); // Skip magic number
+                int minorVersion = readUnsignedShort(is);
+                int majorVersion = readUnsignedShort(is);
+
+                if (majorVersion >= 49) {
+                    tip.append(majorVersion - (49-5));
+                } else if (majorVersion >= 45) {
+                    tip.append("1.");
+                    tip.append(majorVersion - (45-1));
+                }
+                tip.append(" (");
+                tip.append(majorVersion);
+                tip.append('.');
+                tip.append(minorVersion);
+                tip.append(')');
+            } catch (IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+
+            tip.append("</html>");
+
+            return tip.toString();
+        }
+
+        /**
+         * @see java.io.DataInputStream#readUnsignedShort()
+         */
+        protected int readUnsignedShort(InputStream is) throws IOException {
+            int ch1 = is.read();
+            int ch2 = is.read();
+            if ((ch1 | ch2) < 0)
+                throw new EOFException();
+            return (ch1 << 8) + (ch2 << 0);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/ClassesDirectoryTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/ClassesDirectoryTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..510cc68
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/ClassesDirectoryTreeNodeFactoryProvider.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import javax.swing.*;
+
+public class ClassesDirectoryTreeNodeFactoryProvider extends DirectoryTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(ClassesDirectoryTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/packagefolder_obj.png"));
+
+    @Override public String[] getSelectors() {
+        return appendSelectors(
+                "jar:dir:META-INF/versions",
+                "jar:dir:META-INF/versions/5",
+                "jar:dir:META-INF/versions/6",
+                "jar:dir:META-INF/versions/7",
+                "jar:dir:META-INF/versions/8",
+                "jar:dir:META-INF/versions/9",
+                "jar:dir:META-INF/versions/10",
+                "jar:dir:META-INF/versions/11",
+                "jar:dir:META-INF/versions/12",
+                "jar:dir:META-INF/versions/13",
+                "jar:dir:META-INF/versions/14",
+                "war:dir:WEB-INF/classes",
+                "jmod:dir:classes");
+    }
+
+    @Override public ImageIcon getIcon() { return ICON; }
+    @Override public ImageIcon getOpenIcon() { return null; }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/CssFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/CssFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..0193126
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/CssFileTreeNodeFactoryProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class CssFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(HtmlFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/css_obj.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.css"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new TextFileTreeNodeFactoryProvider.Page(entry) {
+                @Override public String getSyntaxStyle() {
+                    return SyntaxConstants.SYNTAX_STYLE_CSS;
+                }
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/jd/gui/service/treenode/DirectoryTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/DirectoryTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..c58a3fa
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/DirectoryTreeNodeFactoryProvider.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.TreeNodeExpandable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Container.Entry;
+import org.jd.gui.spi.TreeNodeFactory;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+import java.net.URI;
+import java.util.Collection;
+
+public class DirectoryTreeNodeFactoryProvider extends AbstractTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(DirectoryTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/folder.gif"));
+    protected static final ImageIcon OPEN_ICON = new ImageIcon(DirectoryTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/folder_open.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:dir:*"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf('/');
+        Collection<Entry> entries = entry.getChildren();
+
+        // Aggregate directory names
+        while (entries.size() == 1) {
+            Entry child = entries.iterator().next();
+            if (!child.isDirectory() || api.getTreeNodeFactory(child) != this || entry.getContainer() != child.getContainer()) break;
+            entry = child;
+            entries = entry.getChildren();
+        }
+
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        TreeNode node = new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, getIcon(), getOpenIcon()));
+
+        if (entries.size() > 0) {
+            // Add dummy node
+            node.add(new DefaultMutableTreeNode());
+        }
+
+        return (T)node;
+    }
+
+    public ImageIcon getIcon() { return ICON; }
+    public ImageIcon getOpenIcon() { return OPEN_ICON; }
+
+    protected static class TreeNode extends DefaultMutableTreeNode implements ContainerEntryGettable, UriGettable, TreeNodeExpandable {
+        Container.Entry entry;
+        boolean initialized;
+
+        public TreeNode(Container.Entry entry, Object userObject) {
+            super(userObject);
+            this.entry = entry;
+            this.initialized = false;
+        }
+
+        // --- ContainerEntryGettable --- //
+        @Override public Container.Entry getEntry() { return entry; }
+
+        // --- UriGettable --- //
+        @Override public URI getUri() { return entry.getUri(); }
+
+        // --- TreeNodeExpandable --- //
+        @Override
+        public void populateTreeNode(API api) {
+            if (!initialized) {
+                removeAllChildren();
+
+                Collection<Container.Entry> entries = getChildren();
+
+                while (entries.size() == 1) {
+                    Entry child = entries.iterator().next();
+                    if (!child.isDirectory() || api.getTreeNodeFactory(child) != this) {
+                        break;
+                    }
+                    entries = child.getChildren();
+                }
+
+                for (Entry entry : entries) {
+                    TreeNodeFactory factory = api.getTreeNodeFactory(entry);
+                    if (factory != null) {
+                        add(factory.make(api, entry));
+                    }
+                }
+
+                initialized = true;
+            }
+        }
+
+        public Collection<Container.Entry> getChildren() { return entry.getChildren(); }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/DtdFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/DtdFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..db19551
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/DtdFileTreeNodeFactoryProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class DtdFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(DtdFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/dtd_obj.gif"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.dtd"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new TextFileTreeNodeFactoryProvider.Page(entry) {
+                @Override public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_DTD; }
+            };
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/EarFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/EarFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..a1c0f90
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/EarFileTreeNodeFactoryProvider.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class EarFileTreeNodeFactoryProvider extends ZipFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(JarFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/ear_obj.gif"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.ear"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        T node = (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+        // Add dummy node
+        node.add(new DefaultMutableTreeNode());
+        return node;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/EjbJarXmlFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/EjbJarXmlFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..c4cee6c
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/EjbJarXmlFileTreeNodeFactoryProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.PageCreator;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.component.EjbJarXmlFilePage;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class EjbJarXmlFileTreeNodeFactoryProvider extends FileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(ManifestFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/xml_obj.gif"));
+
+    @Override public String[] getSelectors() { return appendSelectors("jar:file:META-INF/ejb-jar.xml"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean("ejb-jar.xml", "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends FileTreeNodeFactoryProvider.TreeNode implements PageCreator {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new EjbJarXmlFilePage(api, entry);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/FileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/FileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..dc8ae87
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/FileTreeNodeFactoryProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+import java.net.URI;
+
+public class FileTreeNodeFactoryProvider extends AbstractTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(FileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/file_plain_obj.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf('/');
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends DefaultMutableTreeNode implements ContainerEntryGettable, UriGettable {
+        protected Container.Entry entry;
+
+        public TreeNode(Container.Entry entry, Object userObject) {
+            super(userObject);
+            this.entry = entry;
+        }
+
+        // --- ContainerEntryGettable --- //
+        @Override public Container.Entry getEntry() { return entry; }
+
+        // --- UriGettable --- //
+        @Override public URI getUri() { return entry.getUri(); }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/HtmlFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/HtmlFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..9196269
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/HtmlFileTreeNodeFactoryProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class HtmlFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(HtmlFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/html_obj.gif"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.html", "*:file:*.xhtml"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new TextFileTreeNodeFactoryProvider.Page(entry) {
+                @Override public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_HTML; }
+            };
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/ImageFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/ImageFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..fe0ffdb
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/ImageFileTreeNodeFactoryProvider.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.*;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+
+public class ImageFileTreeNodeFactoryProvider extends FileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(ImageFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/file-image.gif"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.gif", "*:file:*.jpg", "*:file:*.png"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends FileTreeNodeFactoryProvider.TreeNode implements PageCreator {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new ImagePage(entry);
+        }
+    }
+
+    protected static class ImagePage extends JPanel implements UriGettable {
+        protected Container.Entry entry;
+
+        public ImagePage(Container.Entry entry) {
+            super(new BorderLayout());
+
+            this.entry = entry;
+
+            try (InputStream is = entry.getInputStream()) {
+                JScrollPane scrollPane = new JScrollPane(new JLabel(new ImageIcon(ImageIO.read(is))));
+
+                scrollPane.getHorizontalScrollBar().setUnitIncrement(16);
+                scrollPane.getVerticalScrollBar().setUnitIncrement(16);
+
+                add(scrollPane, BorderLayout.CENTER);
+            } catch (IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+
+        // --- UriGettable --- //
+        @Override public URI getUri() { return entry.getUri(); }
+    }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/jd/gui/service/treenode/JarFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/JarFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..027810d
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/JarFileTreeNodeFactoryProvider.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.util.container.JarContainerEntryUtil;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+import java.util.Collection;
+
+public class JarFileTreeNodeFactoryProvider extends ZipFileTreeNodeFactoryProvider {
+    protected static final ImageIcon JAR_FILE_ICON = new ImageIcon(JarFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/jar_obj.png"));
+    protected static final ImageIcon EJB_FILE_ICON = new ImageIcon(JarFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/ejbmodule_obj.gif"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.jar"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        ImageIcon icon = isAEjbModule(entry) ? EJB_FILE_ICON : JAR_FILE_ICON;
+        T node = (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, icon));
+        // Add dummy node
+        node.add(new DefaultMutableTreeNode());
+        return node;
+    }
+
+    protected static boolean isAEjbModule(Container.Entry entry) {
+        Collection<Container.Entry> children = entry.getChildren();
+
+        if (children != null) {
+            Container.Entry metaInf = null;
+
+            for (Container.Entry child : children) {
+                if (child.getPath().equals("META-INF")) {
+                    metaInf = child;
+                    break;
+                }
+            }
+
+            if (metaInf != null) {
+                children = metaInf.getChildren();
+
+                for (Container.Entry child : children) {
+                    if (child.getPath().equals("META-INF/ejb-jar.xml")) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    protected static class TreeNode extends ZipFileTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) {
+            super(entry, userObject);
+        }
+
+        @Override
+        public Collection<Container.Entry> getChildren() {
+            return JarContainerEntryUtil.removeInnerTypeEntries(entry.getChildren());
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/JavaFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/JavaFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..cd733e7
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/JavaFileTreeNodeFactoryProvider.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.component.JavaFilePage;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class JavaFileTreeNodeFactoryProvider extends AbstractTypeFileTreeNodeFactoryProvider {
+    protected static final ImageIcon JAVA_FILE_ICON = new ImageIcon(JavaFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/jcu_obj.png"));
+    protected static final Factory FACTORY = new Factory();
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.java"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf('/');
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new FileTreeNode(entry, new TreeNodeBean(label, "Location: " + location, JAVA_FILE_ICON), FACTORY);
+    }
+
+    protected static class Factory implements AbstractTypeFileTreeNodeFactoryProvider.PageAndTipFactory {
+        // --- PageAndTipFactory --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T makePage(API a, Container.Entry e) {
+            return (T)new JavaFilePage(a, e);
+        }
+
+        @Override
+        public String makeTip(API api, Container.Entry entry) {
+            String location = new File(entry.getUri()).getPath();
+            StringBuilder tip = new StringBuilder("<html>Location: ");
+
+            tip.append(location);
+            tip.append("</html>");
+
+            return tip.toString();
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/JavaModuleFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/JavaModuleFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..1359e41
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/JavaModuleFileTreeNodeFactoryProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class JavaModuleFileTreeNodeFactoryProvider extends ZipFileTreeNodeFactoryProvider {
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.jmod"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        T node = (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+        // Add dummy node
+        node.add(new DefaultMutableTreeNode());
+        return node;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/JavaModulePackageTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/JavaModulePackageTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..7706c06
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/JavaModulePackageTreeNodeFactoryProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import java.util.regex.Pattern;
+
+public class JavaModulePackageTreeNodeFactoryProvider extends PackageTreeNodeFactoryProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("jmod:dir:*"); }
+
+    @Override
+    public Pattern getPathPattern() {
+        if (externalPathPattern == null) {
+            return Pattern.compile("classes\\/(?!META-INF)..*");
+        } else {
+            return externalPathPattern;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/JavascriptFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/JavascriptFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..b4065e2
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/JavascriptFileTreeNodeFactoryProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class JavascriptFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(JavascriptFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/js_obj.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.js"); }
+
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new TextFileTreeNodeFactoryProvider.Page(entry) {
+                @Override public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT; }
+            };
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/JsonFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/JsonFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..8b41c47
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/JsonFileTreeNodeFactoryProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class JsonFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(JsonFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/ascii_obj.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.json"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new TextFileTreeNodeFactoryProvider.Page(entry) {
+                @Override public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_JSON; }
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/jd/gui/service/treenode/JspFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/JspFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..1ff9686
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/JspFileTreeNodeFactoryProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class JspFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(HtmlFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/html_obj.gif"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.jsp", "*:file:*.jspf"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new TextFileTreeNodeFactoryProvider.Page(entry) {
+                @Override public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_JSP; }
+            };
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/KarFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/KarFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..7a8f4f9
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/KarFileTreeNodeFactoryProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class KarFileTreeNodeFactoryProvider extends ZipFileTreeNodeFactoryProvider {
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.kar"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        T node = (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+        // Add dummy node
+        node.add(new DefaultMutableTreeNode());
+        return node;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/ManifestFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/ManifestFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..118f387
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/ManifestFileTreeNodeFactoryProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.PageCreator;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.component.ManifestFilePage;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class ManifestFileTreeNodeFactoryProvider extends FileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(ManifestFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/manifest_obj.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:META-INF/MANIFEST.MF"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean("MANIFEST.MF", "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends FileTreeNodeFactoryProvider.TreeNode implements PageCreator {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new ManifestFilePage(api, entry);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/MetainfDirectoryTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/MetainfDirectoryTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..acd120a
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/MetainfDirectoryTreeNodeFactoryProvider.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import javax.swing.*;
+import java.util.regex.Pattern;
+
+public class MetainfDirectoryTreeNodeFactoryProvider extends DirectoryTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(MetainfDirectoryTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/inf_obj.png"));
+
+    @Override public String[] getSelectors() {
+        return appendSelectors(
+                "jar:dir:META-INF",
+                "war:dir:WEB-INF",
+                "war:dir:WEB-INF/classes/META-INF",
+                "ear:dir:META-INF",
+                "jmod:dir:classes/META-INF");
+    }
+
+    @Override public ImageIcon getIcon() { return ICON; }
+    @Override public ImageIcon getOpenIcon() { return null; }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/MetainfServiceFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/MetainfServiceFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..629b97d
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/MetainfServiceFileTreeNodeFactoryProvider.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.PageCreator;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.component.OneTypeReferencePerLinePage;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+import java.util.regex.Pattern;
+
+public class MetainfServiceFileTreeNodeFactoryProvider extends FileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(TextFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/ascii_obj.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*"); }
+
+    @Override
+    public Pattern getPathPattern() {
+        if (externalPathPattern == null) {
+            return Pattern.compile("META-INF\\/services\\/[^\\/]+");
+        } else {
+            return externalPathPattern;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends FileTreeNodeFactoryProvider.TreeNode implements PageCreator {
+        public TreeNode(Container.Entry entry, Object userObject) {
+            super(entry, userObject);
+        }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new OneTypeReferencePerLinePage(api, entry);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/ModuleInfoFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/ModuleInfoFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..099d1a6
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/ModuleInfoFileTreeNodeFactoryProvider.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Type;
+import org.jd.gui.spi.TypeFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.view.component.ModuleInfoFilePage;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+import java.util.Collection;
+import java.util.regex.Pattern;
+
+public class ModuleInfoFileTreeNodeFactoryProvider extends ClassFileTreeNodeFactoryProvider {
+    protected static final ImageIcon MODULE_FILE_ICON = new ImageIcon(ClassFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/module_obj.png"));
+    protected static final Factory FACTORY = new Factory();
+
+    static {
+        // Early class loading
+        try {
+            Class.forName(ModuleInfoFilePage.class.getName());
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*/module-info.class"); }
+
+    @Override public Pattern getPathPattern() { return externalPathPattern; }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf('/');
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        return (T)new ModuleInfoFileTreeNode(entry, new TreeNodeBean(label, CLASS_FILE_ICON), FACTORY);
+    }
+
+    protected static class ModuleInfoFileTreeNode extends FileTreeNode {
+        public ModuleInfoFileTreeNode(Container.Entry entry, Object userObject, PageAndTipFactory pageAndTipFactory) {
+            super(entry, null, userObject, pageAndTipFactory);
+        }
+
+        // --- TreeNodeExpandable --- //
+        @Override
+        public void populateTreeNode(API api) {
+            if (!initialized) {
+                removeAllChildren();
+                // Create type node
+                TypeFactory typeFactory = api.getTypeFactory(entry);
+
+                if (typeFactory != null) {
+                    Collection<Type> types = typeFactory.make(api, entry);
+
+                    for (Type type : types) {
+                        add(new BaseTreeNode(entry, type.getName(), new TreeNodeBean(type.getDisplayTypeName(), MODULE_FILE_ICON), factory));
+                    }
+                }
+
+                initialized = true;
+            }
+        }
+    }
+
+    protected static class Factory implements AbstractTypeFileTreeNodeFactoryProvider.PageAndTipFactory {
+        // --- PageAndTipFactory --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T makePage(API a, Container.Entry e) {
+            return (T)new ModuleInfoFilePage(a, e);
+        }
+
+        @Override
+        public String makeTip(API api, Container.Entry entry) {
+            String location = new File(entry.getUri()).getPath();
+            StringBuilder tip = new StringBuilder("<html>Location: ");
+
+            tip.append(location);
+            tip.append("</html>");
+
+            return tip.toString();
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/PackageTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/PackageTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..3db5d65
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/PackageTreeNodeFactoryProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.util.container.JarContainerEntryUtil;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+import java.util.Collection;
+import java.util.regex.Pattern;
+
+public class PackageTreeNodeFactoryProvider extends DirectoryTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(PackageTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/package_obj.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("jar:dir:*"); }
+
+    @Override
+    public Pattern getPathPattern() {
+        if (externalPathPattern == null) {
+            return Pattern.compile("(META-INF\\/versions\\/.*)|(?!META-INF)..*");
+        } else {
+            return externalPathPattern;
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        Collection<Container.Entry> entries = entry.getChildren();
+
+        // Aggregate directory names
+        while (entries.size() == 1) {
+            Container.Entry child = entries.iterator().next();
+            if (!child.isDirectory() || (api.getTreeNodeFactory(child) != this) || (entry.getContainer() != child.getContainer())) break;
+            entry = child;
+            entries = entry.getChildren();
+        }
+
+        String label = entry.getPath().substring(lastSlashIndex+1).replace("/", ".");
+        String location = new File(entry.getUri()).getPath();
+        T node = (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, getIcon(), getOpenIcon()));
+
+        if (entries.size() > 0) {
+            // Add dummy node
+            node.add(new DefaultMutableTreeNode());
+        }
+
+        return node;
+    }
+
+    @Override public ImageIcon getIcon() { return ICON; }
+    @Override public ImageIcon getOpenIcon() { return null; }
+
+    protected static class TreeNode extends DirectoryTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) {
+            super(entry, userObject);
+        }
+
+        @Override
+        public Collection<Container.Entry> getChildren() {
+            return JarContainerEntryUtil.removeInnerTypeEntries(entry.getChildren());
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/PropertiesFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/PropertiesFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..68c2fa7
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/PropertiesFileTreeNodeFactoryProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class PropertiesFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(PropertiesFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/ascii_obj.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.properties"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new TextFileTreeNodeFactoryProvider.Page(entry) {
+                @Override public String getSyntaxStyle() {
+                    return SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE;
+                }
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/jd/gui/service/treenode/SpiFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/SpiFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..48f2bab
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/SpiFileTreeNodeFactoryProvider.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import java.util.regex.Pattern;
+
+public class SpiFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider {
+    @Override public String[] getSelectors() {
+        return appendSelectors("*:file:*");
+    }
+
+    @Override
+    public Pattern getPathPattern() {
+        if (externalPathPattern == null) {
+            return Pattern.compile("(.*\\/)?META-INF\\/services\\/.*");
+        } else {
+            return externalPathPattern;
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/jd/gui/service/treenode/SqlFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/SqlFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..a5c3471
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/SqlFileTreeNodeFactoryProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class SqlFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(SqlFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/sql_obj.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.sql"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new TextFileTreeNodeFactoryProvider.Page(entry) {
+                @Override public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_SQL; }
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/jd/gui/service/treenode/TextFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/TextFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..6665c83
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/TextFileTreeNodeFactoryProvider.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
+import org.fife.ui.rsyntaxtextarea.Theme;
+import org.fife.ui.rtextarea.Gutter;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.PageCreator;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.io.TextReader;
+import org.jd.gui.view.component.TextPage;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+
+public class TextFileTreeNodeFactoryProvider extends FileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(TextFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/ascii_obj.png"));
+
+    static {
+        // Early class loading
+        new Gutter(new RSyntaxTextArea());
+        try {
+            Theme.load(TextFileTreeNodeFactoryProvider.class.getClassLoader().getResourceAsStream("rsyntaxtextarea/themes/eclipse.xml"));
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    @Override public String[] getSelectors() {
+        return appendSelectors("*:file:*.txt", "*:file:*.md", "*:file:*.SF", "*:file:*.policy", "*:file:*.yaml", "*:file:*.yml", "*:file:*/COPYRIGHT", "*:file:*/LICENSE");
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends FileTreeNodeFactoryProvider.TreeNode implements PageCreator {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new Page(entry);
+        }
+    }
+
+    protected static class Page extends TextPage implements UriGettable {
+        protected Container.Entry entry;
+
+        public Page(Container.Entry entry) {
+            this.entry = entry;
+            setText(TextReader.getText(entry.getInputStream()));
+        }
+
+        // --- UriGettable --- //
+        @Override public URI getUri() { return entry.getUri(); }
+
+        // --- ContentSavable --- //
+        public String getFileName() {
+            String path = entry.getPath();
+            int index = path.lastIndexOf("/");
+            return path.substring(index+1);
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/jd/gui/service/treenode/WarFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/WarFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..57e8094
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/WarFileTreeNodeFactoryProvider.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class WarFileTreeNodeFactoryProvider extends ZipFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(JarFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/war_obj.gif"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.war"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        T node = (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+        // Add dummy node
+        node.add(new DefaultMutableTreeNode());
+        return node;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/WarPackageTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/WarPackageTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..d42fa2c
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/WarPackageTreeNodeFactoryProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import java.util.regex.Pattern;
+
+public class WarPackageTreeNodeFactoryProvider extends PackageTreeNodeFactoryProvider {
+
+    @Override public String[] getSelectors() { return appendSelectors("war:dir:*"); }
+
+    @Override
+    public Pattern getPathPattern() {
+        if (externalPathPattern == null) {
+            return Pattern.compile("WEB-INF\\/classes\\/(?!META-INF)..*");
+        } else {
+            return externalPathPattern;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/WebXmlFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/WebXmlFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..d4e410e
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/WebXmlFileTreeNodeFactoryProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.PageCreator;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.component.WebXmlFilePage;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class WebXmlFileTreeNodeFactoryProvider extends FileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(ManifestFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/xml_obj.gif"));
+
+    @Override public String[] getSelectors() { return appendSelectors("war:file:WEB-INF/web.xml"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean("web.xml", "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends FileTreeNodeFactoryProvider.TreeNode implements PageCreator {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new WebXmlFilePage(api, entry);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/WebinfLibDirectoryTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/WebinfLibDirectoryTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..752680c
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/WebinfLibDirectoryTreeNodeFactoryProvider.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use, 
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import javax.swing.*;
+
+public class WebinfLibDirectoryTreeNodeFactoryProvider extends DirectoryTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(WebinfLibDirectoryTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/archivefolder_obj.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("war:dir:WEB-INF/lib"); }
+    @Override public ImageIcon getIcon() { return ICON; }
+    @Override public ImageIcon getOpenIcon() { return null; }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/XmlBasedFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/XmlBasedFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..66f4b98
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/XmlBasedFileTreeNodeFactoryProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class XmlBasedFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(XmlBasedFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/xml_obj.gif"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.xsl", "*:file:*.xslt", "*:file:*.xsd", "*:file:*.tld", "*:file:*.wsdl"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new TextFileTreeNodeFactoryProvider.Page(entry) {
+                @Override public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_XML; }
+            };
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/XmlFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/XmlFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..9b647ea
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/XmlFileTreeNodeFactoryProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.view.component.XmlFilePage;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class XmlFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(XmlFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/xml_obj.gif"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.xml"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        return (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+    }
+
+    protected static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) { super(entry, userObject); }
+
+        // --- PageCreator --- //
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T extends JComponent & UriGettable> T createPage(API api) {
+            return (T)new XmlFilePage(api, entry);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/treenode/ZipFileTreeNodeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/treenode/ZipFileTreeNodeFactoryProvider.java
new file mode 100644
index 0000000..204e79c
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/treenode/ZipFileTreeNodeFactoryProvider.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.treenode;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContainerEntryGettable;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.spi.TreeNodeFactory;
+import org.jd.gui.view.data.TreeNodeBean;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.File;
+
+public class ZipFileTreeNodeFactoryProvider extends DirectoryTreeNodeFactoryProvider {
+    protected static final ImageIcon ICON = new ImageIcon(ZipFileTreeNodeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/zip_obj.png"));
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.zip", "*:file:*.aar"); }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends DefaultMutableTreeNode & ContainerEntryGettable & UriGettable> T make(API api, Container.Entry entry) {
+        int lastSlashIndex = entry.getPath().lastIndexOf("/");
+        String label = entry.getPath().substring(lastSlashIndex+1);
+        String location = new File(entry.getUri()).getPath();
+        T node = (T)new TreeNode(entry, new TreeNodeBean(label, "Location: " + location, ICON));
+        // Add dummy node
+        node.add(new DefaultMutableTreeNode());
+        return node;
+    }
+
+    protected static class TreeNode extends DirectoryTreeNodeFactoryProvider.TreeNode {
+        public TreeNode(Container.Entry entry, Object userObject) {
+            super(entry, userObject);
+        }
+
+        // --- TreeNodeExpandable --- //
+        public void populateTreeNode(API api) {
+            if (!initialized) {
+                removeAllChildren();
+
+                for (Container.Entry e : getChildren()) {
+                    TreeNodeFactory factory = api.getTreeNodeFactory(e);
+                    if (factory != null) {
+                        add(factory.make(api, e));
+                    }
+                }
+
+                initialized = true;
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/type/AbstractTypeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/type/AbstractTypeFactoryProvider.java
new file mode 100644
index 0000000..a41dbde
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/type/AbstractTypeFactoryProvider.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.type;
+
+import org.jd.gui.api.model.Type;
+import org.jd.gui.spi.TypeFactory;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public abstract class AbstractTypeFactoryProvider implements TypeFactory {
+    protected List<String> externalSelectors;
+    protected Pattern externalPathPattern;
+
+    /**
+     * Initialize "selectors" and "pathPattern" with optional external properties file
+     */
+    public AbstractTypeFactoryProvider() {
+        Properties properties = new Properties();
+        Class clazz = this.getClass();
+
+        try (InputStream is = clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/') + ".properties")) {
+            if (is != null) {
+                properties.load(is);
+            }
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        init(properties);
+    }
+
+    protected void init(Properties properties) {
+        String selectors = properties.getProperty("selectors");
+        externalSelectors = (selectors == null) ? null : Arrays.asList(selectors.split(","));
+
+        String pathRegExp = properties.getProperty("pathRegExp");
+        externalPathPattern = (pathRegExp == null) ? null : Pattern.compile(pathRegExp);
+    }
+
+    protected String[] appendSelectors(String selector) {
+        if (externalSelectors == null) {
+            return new String[] { selector };
+        } else {
+            int size = externalSelectors.size();
+            String[] array = new String[size+1];
+            externalSelectors.toArray(array);
+            array[size] = selector;
+            return array;
+        }
+    }
+
+    protected String[] appendSelectors(String... selectors) {
+        if (externalSelectors == null) {
+            return selectors;
+        } else {
+            int size = externalSelectors.size();
+            String[] array = new String[size+selectors.length];
+            externalSelectors.toArray(array);
+            System.arraycopy(selectors, 0, array, size, selectors.length);
+            return array;
+        }
+    }
+
+    public Pattern getPathPattern() { return externalPathPattern; }
+
+    // Signature writers
+    protected static int writeSignature(StringBuilder sb, String descriptor, int  length, int index, boolean varargsFlag) {
+        while (true) {
+            // Print array : '[[?' ou '[L[?;'
+            int dimensionLength = 0;
+
+            if (descriptor.charAt(index) == '[') {
+                dimensionLength++;
+
+                while (++index < length) {
+                    if ((descriptor.charAt(index) == 'L') && (index+1 < length) && (descriptor.charAt(index+1) == '[')) {
+                        index++;
+                        length--;
+                        dimensionLength++;
+                    } else if (descriptor.charAt(index) == '[') {
+                        dimensionLength++;
+                    } else {
+                        break;
+                    }
+                }
+            }
+
+            switch(descriptor.charAt(index)) {
+                case 'B': sb.append("byte"); index++; break;
+                case 'C': sb.append("char"); index++; break;
+                case 'D': sb.append("double"); index++; break;
+                case 'F': sb.append("float"); index++; break;
+                case 'I': sb.append("int"); index++; break;
+                case 'J': sb.append("long"); index++; break;
+                case 'L': case '.':
+                    int beginIndex = ++index;
+                    char c = '.';
+
+                    // Search ; or de <
+                    while (index < length) {
+                        c = descriptor.charAt(index);
+                        if ((c == ';') || (c == '<'))
+                            break;
+                        index++;
+                    }
+
+                    String internalClassName = descriptor.substring(beginIndex, index);
+                    int lastPackageSeparatorIndex = internalClassName.lastIndexOf('/');
+
+                    if (lastPackageSeparatorIndex >= 0) {
+                        // Cut package name
+                        internalClassName = internalClassName.substring(lastPackageSeparatorIndex + 1);
+                    }
+
+                    sb.append(internalClassName.replace('$', '.'));
+
+                    if (c == '<') {
+                        sb.append('<');
+                        index = writeSignature(sb, descriptor, length, index+1, false);
+
+                        while (descriptor.charAt(index) != '>') {
+                            sb.append(", ");
+                            index = writeSignature(sb, descriptor, length, index, false);
+                        }
+                        sb.append('>');
+
+                        // pass '>'
+                        index++;
+                    }
+
+                    // pass ';'
+                    if (descriptor.charAt(index) == ';')
+                        index++;
+                    break;
+                case 'S': sb.append("short"); index++; break;
+                case 'T':
+                    beginIndex = ++index;
+                    index = descriptor.substring(beginIndex, length).indexOf(';');
+                    sb.append(descriptor.substring(beginIndex, index));
+                    index++;
+                    break;
+                case 'V': sb.append("void"); index++; break;
+                case 'Z': sb.append("boolean"); index++; break;
+                case '-':
+                    sb.append("? super ");
+                    index = writeSignature(sb, descriptor, length, index+1, false);
+                    break;
+                case '+':
+                    sb.append("? extends ");
+                    index = writeSignature(sb, descriptor, length, index+1, false);
+                    break;
+                case '*': sb.append('?'); index++; break;
+                case 'X': case 'Y': sb.append("int"); index++; break;
+                default:
+                    throw new RuntimeException("SignatureWriter.WriteSignature: invalid signature '" + descriptor + "'");
+            }
+
+            if (varargsFlag) {
+                if (dimensionLength > 0) {
+                    while (--dimensionLength > 0)
+                        sb.append("[]");
+                    sb.append("...");
+                }
+            } else {
+                while (dimensionLength-- > 0)
+                    sb.append("[]");
+            }
+
+            if ((index >= length) || (descriptor.charAt(index) != '.'))
+                break;
+
+            sb.append('.');
+        }
+
+        return index;
+    }
+
+    protected static void writeMethodSignature(
+            StringBuilder sb, int typeAccess, int methodAccess, boolean isInnerClass,
+            String constructorName, String methodName, String descriptor) {
+        if (methodName.equals("<clinit>")) {
+            sb.append("{...}");
+        } else {
+            boolean isAConstructor = methodName.equals("<init>");
+
+            if (isAConstructor) {
+                sb.append(constructorName);
+            } else {
+                sb.append(methodName);
+            }
+
+            // Skip generics
+            int length = descriptor.length();
+            int index = 0;
+
+            while ((index < length) && (descriptor.charAt(index) != '('))
+                index++;
+
+            if (descriptor.charAt(index) != '(') {
+                throw new RuntimeException("Signature format exception: '" + descriptor + "'");
+            }
+
+            sb.append('(');
+
+            // pass '('
+            index++;
+
+            if (descriptor.charAt(index) != ')') {
+                if (isAConstructor && isInnerClass && ((typeAccess & Type.FLAG_STATIC) == 0)) {
+                    // Skip first parameter
+                    int lengthBackup = sb.length();
+                    index = writeSignature(sb, descriptor, length, index, false);
+                    sb.setLength(lengthBackup);
+                }
+
+                if (descriptor.charAt(index) != ')') {
+                    int varargsParameterIndex;
+
+                    if ((methodAccess & Type.FLAG_VARARGS) == 0) {
+                        varargsParameterIndex = Integer.MAX_VALUE;
+                    } else {
+                        // Count parameters
+                        int indexBackup = index;
+                        int lengthBackup = sb.length();
+
+                        varargsParameterIndex = -1;
+
+                        while (descriptor.charAt(index) != ')') {
+                            index = writeSignature(sb, descriptor, length, index, false);
+                            varargsParameterIndex++;
+                        }
+
+                        index = indexBackup;
+                        sb.setLength(lengthBackup);
+                    }
+
+                    // Write parameters
+                    index = writeSignature(sb, descriptor, length, index, false);
+
+                    int parameterIndex = 1;
+
+                    while (descriptor.charAt(index) != ')') {
+                        sb.append(", ");
+                        index = writeSignature(sb, descriptor, length, index, (parameterIndex == varargsParameterIndex));
+                        parameterIndex++;
+                    }
+                }
+            }
+
+            if (isAConstructor) {
+                sb.append(')');
+            } else {
+                sb.append(") : ");
+                writeSignature(sb, descriptor, length, ++index, false);
+            }
+        }
+    }
+
+    // Icon getters
+    protected static ImageIcon getTypeIcon(int access) {
+        if ((access & Type.FLAG_ANNOTATION) != 0)
+            return ANNOTATION_ICON;
+        else if ((access & Type.FLAG_INTERFACE) != 0)
+            return INTERFACE_ICONS[accessToIndex(access)];
+        else if ((access & Type.FLAG_ENUM) != 0)
+            return ENUM_ICON;
+        else
+            return CLASS_ICONS[accessToIndex(access)];
+    }
+
+    protected static ImageIcon getFieldIcon(int access) {
+        return FIELD_ICONS[accessToIndex(access)];
+    }
+
+    protected static ImageIcon getMethodIcon(int access) {
+        return METHOD_ICONS[accessToIndex(access)];
+    }
+
+    protected static int accessToIndex(int access) {
+        int index = 0;
+
+        if ((access & Type.FLAG_STATIC) != 0)
+            index += 4;
+
+        if ((access & Type.FLAG_FINAL) != 0)
+            index += 8;
+
+        if ((access & Type.FLAG_ABSTRACT) != 0)
+            index += 16;
+
+        if ((access & Type.FLAG_PUBLIC) != 0)
+            return index + 1;
+        else if ((access & Type.FLAG_PROTECTED) != 0)
+            return index + 2;
+        else if ((access & Type.FLAG_PRIVATE) != 0)
+            return index + 3;
+        else
+            return index;
+    }
+
+    // Internal graphic stuff ...
+    protected static ImageIcon mergeIcons(ImageIcon background, ImageIcon overlay, int x, int y) {
+        int w = background.getIconWidth();
+        int h = background.getIconHeight();
+        BufferedImage image = new BufferedImage(w, h,  BufferedImage.TYPE_INT_ARGB);
+
+        if (x + overlay.getIconWidth() > w)
+            x = w - overlay.getIconWidth();
+        if (y + overlay.getIconHeight() > h)
+            y = h - overlay.getIconHeight();
+
+        Graphics2D g2 = image.createGraphics();
+        g2.drawImage(background.getImage(), 0, 0, null);
+        g2.drawImage(overlay.getImage(), x, y, null);
+        g2.dispose();
+
+        return new ImageIcon(image);
+    }
+
+    protected static ImageIcon[] mergeIcons(ImageIcon[] backgrounds, ImageIcon overlay, int x, int y) {
+        int length = backgrounds.length;
+        ImageIcon[] result = new ImageIcon[length*2];
+
+        // Copy source icons
+        System.arraycopy(backgrounds, 0, result, 0, length);
+
+        // Add overlays
+        for (int i=0; i<length; i++) {
+            result[length+i] = mergeIcons(backgrounds[i], overlay, x, y);
+        }
+
+        return result;
+    }
+
+    protected static final ImageIcon ABSTRACT_OVERLAY_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/abstract_ovr.png"));
+    protected static final ImageIcon FINAL_OVERLAY_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/final_ovr.png"));
+    protected static final ImageIcon STATIC_OVERLAY_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/static_ovr.png"));
+
+    protected static final ImageIcon CLASS_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/class_default_obj.png"));
+    protected static final ImageIcon PUBLIC_CLASS_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/class_obj.png"));
+    protected static final ImageIcon PROTECTED_CLASS_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/class_protected_obj.png"));
+    protected static final ImageIcon PRIVATE_CLASS_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/class_private_obj.png"));
+
+    protected static final ImageIcon INTERFACE_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/int_default_obj.png"));
+    protected static final ImageIcon PUBLIC_INTERFACE_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/int_obj.png"));
+    protected static final ImageIcon PROTECTED_INTERFACE_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/int_protected_obj.png"));
+    protected static final ImageIcon PRIVATE_INTERFACE_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/int_private_obj.png"));
+
+    protected static final ImageIcon ANNOTATION_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/annotation_obj.png"));
+    protected static final ImageIcon ENUM_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/enum_obj.png"));
+
+    protected static final ImageIcon FIELD_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/field_default_obj.png"));
+    protected static final ImageIcon PUBLIC_FIELD_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/field_public_obj.png"));
+    protected static final ImageIcon PROTECTED_FIELD_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/field_protected_obj.png"));
+    protected static final ImageIcon PRIVATE_FIELD_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/field_private_obj.png"));
+
+    protected static final ImageIcon METHOD_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/methdef_obj.png"));
+    protected static final ImageIcon PUBLIC_METHOD_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/methpub_obj.png"));
+    protected static final ImageIcon PROTECTED_METHOD_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/methpro_obj.png"));
+    protected static final ImageIcon PRIVATE_METHOD_ICON = new ImageIcon(ClassFileTypeFactoryProvider.class.getClassLoader().getResource("org/jd/gui/images/methpri_obj.png"));
+
+    // Default icon set
+    protected static final ImageIcon[] DEFAULT_CLASS_ICONS = {
+            CLASS_ICON,
+            PUBLIC_CLASS_ICON,
+            PROTECTED_CLASS_ICON,
+            PRIVATE_CLASS_ICON
+    };
+
+    protected static final ImageIcon[] DEFAULT_INTERFACE_ICONS = {
+            INTERFACE_ICON,
+            PUBLIC_INTERFACE_ICON,
+            PROTECTED_INTERFACE_ICON,
+            PRIVATE_INTERFACE_ICON
+    };
+
+    protected static final ImageIcon[] DEFAULT_FIELD_ICONS = {
+            FIELD_ICON,
+            PUBLIC_FIELD_ICON,
+            PROTECTED_FIELD_ICON,
+            PRIVATE_FIELD_ICON
+    };
+
+    protected static final ImageIcon[] DEFAULT_METHOD_ICONS = {
+            METHOD_ICON,
+            PUBLIC_METHOD_ICON,
+            PROTECTED_METHOD_ICON,
+            PRIVATE_METHOD_ICON
+    };
+
+    // Add static icon set
+    protected static final ImageIcon[] STATIC_CLASS_ICONS = mergeIcons(DEFAULT_CLASS_ICONS, STATIC_OVERLAY_ICON, 100, 0);
+    protected static final ImageIcon[] STATIC_INTERFACE_ICONS = mergeIcons(DEFAULT_INTERFACE_ICONS, STATIC_OVERLAY_ICON, 100, 0);
+    protected static final ImageIcon[] STATIC_FIELD_ICONS = mergeIcons(DEFAULT_FIELD_ICONS, STATIC_OVERLAY_ICON, 100, 0);
+    protected static final ImageIcon[] STATIC_METHOD_ICONS = mergeIcons(DEFAULT_METHOD_ICONS, STATIC_OVERLAY_ICON, 100, 0);
+
+    // Add final icon set
+    protected static final ImageIcon[] FINAL_STATIC_CLASS_ICONS = mergeIcons(STATIC_CLASS_ICONS, FINAL_OVERLAY_ICON, 0, 0);
+    protected static final ImageIcon[] FINAL_STATIC_INTERFACE_ICONS = mergeIcons(STATIC_INTERFACE_ICONS, FINAL_OVERLAY_ICON, 0, 0);
+    protected static final ImageIcon[] FINAL_STATIC_FIELD_ICONS = mergeIcons(STATIC_FIELD_ICONS, FINAL_OVERLAY_ICON, 0, 0);
+    protected static final ImageIcon[] FINAL_STATIC_METHOD_ICONS = mergeIcons(STATIC_METHOD_ICONS, FINAL_OVERLAY_ICON, 0, 0);
+
+    // Add abstract icon set
+    protected static final ImageIcon[] CLASS_ICONS = mergeIcons(FINAL_STATIC_CLASS_ICONS, ABSTRACT_OVERLAY_ICON, 0, 100);
+    protected static final ImageIcon[] INTERFACE_ICONS = mergeIcons(FINAL_STATIC_INTERFACE_ICONS, ABSTRACT_OVERLAY_ICON, 0, 100);
+    protected static final ImageIcon[] FIELD_ICONS = mergeIcons(FINAL_STATIC_FIELD_ICONS, ABSTRACT_OVERLAY_ICON, 0, 100);
+    protected static final ImageIcon[] METHOD_ICONS = mergeIcons(FINAL_STATIC_METHOD_ICONS, ABSTRACT_OVERLAY_ICON, 0, 100);
+
+    // Cache
+    protected static class Cache<K, V> extends LinkedHashMap<K, V> {
+        public static final int CACHE_MAX_ENTRIES = 100;
+
+        public Cache() {
+            super(CACHE_MAX_ENTRIES*3/2, 0.7f, true);
+        }
+
+        @Override
+        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+            return size() > CACHE_MAX_ENTRIES;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/type/ClassFileTypeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/type/ClassFileTypeFactoryProvider.java
new file mode 100644
index 0000000..b232cb9
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/type/ClassFileTypeFactoryProvider.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.type;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Type;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.objectweb.asm.*;
+
+import javax.swing.*;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class ClassFileTypeFactoryProvider extends AbstractTypeFactoryProvider {
+
+    static {
+        // Early class loading
+        try {
+            Class.forName(JavaType.class.getName());
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    // Create cache
+    protected Cache<URI, JavaType> cache = new Cache<>();
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.class"); }
+
+    @Override
+    public Collection<Type> make(API api, Container.Entry entry) {
+        return Collections.singletonList(make(api, entry, null));
+    }
+
+    @Override
+    public Type make(API api, Container.Entry entry, String fragment) {
+        URI key = entry.getUri();
+
+        if (cache.containsKey(key)) {
+            return cache.get(key);
+        } else {
+            JavaType type;
+
+            try (InputStream is = entry.getInputStream()) {
+                ClassReader classReader = new ClassReader(is);
+
+                if ((fragment != null) && (fragment.length() > 0)) {
+                    // Search type name in fragment. URI format : see jd.gui.api.feature.UriOpener
+                    int index = fragment.indexOf('-');
+                    if (index != -1) {
+                        // Keep type name only
+                        fragment = fragment.substring(0, index);
+                    }
+
+                    if (!classReader.getClassName().equals(fragment)) {
+                        // Search entry for type name
+                        String entryTypePath = classReader.getClassName() + ".class";
+                        String fragmentTypePath = fragment + ".class";
+
+                        while (true) {
+                            if (entry.getPath().endsWith(entryTypePath)) {
+                                // Entry path ends with the internal class name
+                                String pathToFound = entry.getPath().substring(0, entry.getPath().length() - entryTypePath.length()) + fragmentTypePath;
+                                Container.Entry entryFound = null;
+
+                                for (Container.Entry e : entry.getParent().getChildren()) {
+                                    if (e.getPath().equals(pathToFound)) {
+                                        entryFound = e;
+                                        break;
+                                    }
+                                }
+
+                                if (entryFound == null)
+                                    return null;
+
+                                entry = entryFound;
+
+                                try (InputStream is2 = entry.getInputStream()) {
+                                    classReader = new ClassReader(is2);
+                                } catch (IOException e) {
+                                    assert ExceptionUtil.printStackTrace(e);
+                                    return null;
+                                }
+                                break;
+                            }
+
+                            // Truncated path ? Cut first package name and retry
+                            int firstPackageSeparatorIndex = entryTypePath.indexOf('/');
+                            if (firstPackageSeparatorIndex == -1) {
+                                // Nothing to cut -> Stop
+                                return null;
+                            }
+
+                            entryTypePath = entryTypePath.substring(firstPackageSeparatorIndex + 1);
+                            fragmentTypePath = fragmentTypePath.substring(fragmentTypePath.indexOf('/') + 1);
+                        }
+                    }
+                }
+
+                type = new JavaType(entry, classReader, -1);
+            } catch (IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+                type = null;
+            }
+
+            cache.put(key, type);
+            return type;
+        }
+    }
+
+    static class JavaType implements Type {
+        protected Container.Entry entry;
+        protected int access;
+        protected String name;
+        protected String superName;
+        protected String outerName;
+
+        protected String displayTypeName;
+        protected String displayInnerTypeName;
+        protected String displayPackageName;
+
+        protected List<Type> innerTypes;
+        protected List<Type.Field> fields = new ArrayList<>();
+        protected List<Type.Method> methods = new ArrayList<>();
+
+        @SuppressWarnings("unchecked")
+        protected JavaType(Container.Entry entry, ClassReader classReader, final int outerAccess) {
+            this.entry = entry;
+
+            ClassVisitor classAndInnerClassesVisitor = new ClassVisitor(Opcodes.ASM7) {
+                @Override
+                public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
+                    JavaType.this.access = (outerAccess == -1) ? access : outerAccess;
+                    JavaType.this.name = name;
+                    JavaType.this.superName = ((access & Opcodes.ACC_INTERFACE) != 0) && "java/lang/Object".equals(superName) ? null : superName;
+                }
+
+                @Override
+                public void visitInnerClass(String name, String outerName, String innerName, int access) {
+                    if (JavaType.this.name.equals(name)) {
+                        // Inner class path found
+                        JavaType.this.outerName = outerName;
+                        JavaType.this.displayInnerTypeName = innerName;
+                    } else if (((access & (Opcodes.ACC_SYNTHETIC|Opcodes.ACC_BRIDGE)) == 0) && JavaType.this.name.equals(outerName)) {
+                        Container.Entry innerEntry = getEntry(name);
+
+                        if (innerEntry != null) {
+                            try (InputStream is = innerEntry.getInputStream()) {
+                                ClassReader classReader = new ClassReader(is);
+                                if (innerTypes == null) {
+                                    innerTypes = new ArrayList<>();
+                                }
+                                innerTypes.add(new JavaType(innerEntry, classReader, access));
+                            } catch (IOException e) {
+                                assert ExceptionUtil.printStackTrace(e);
+                            }
+                        }
+                    }
+                }
+            };
+
+            classReader.accept(classAndInnerClassesVisitor, ClassReader.SKIP_CODE|ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES);
+
+            int lastPackageSeparatorIndex = name.lastIndexOf('/');
+
+            if (lastPackageSeparatorIndex == -1) {
+                displayPackageName = "";
+
+                if (outerName == null) {
+                    displayTypeName = name;
+                } else {
+                    displayTypeName = getDisplayTypeName(outerName, 0) + '.' + displayInnerTypeName;
+                }
+            } else {
+                displayPackageName = name.substring(0, lastPackageSeparatorIndex).replace('/', '.');
+
+                if (outerName == null) {
+                    displayTypeName = name;
+                } else {
+                    displayTypeName = getDisplayTypeName(outerName, lastPackageSeparatorIndex) + '.' + displayInnerTypeName;
+                }
+
+                displayTypeName = displayTypeName.substring(lastPackageSeparatorIndex+1);
+            }
+
+            ClassVisitor fieldsAndMethodsVisitor = new ClassVisitor(Opcodes.ASM7) {
+                @Override
+                public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
+                    if ((access & (Opcodes.ACC_SYNTHETIC|Opcodes.ACC_ENUM)) == 0) {
+                        fields.add(new Type.Field() {
+                            public int getFlags() { return access; }
+                            public String getName() { return name; }
+                            public String getDescriptor() { return descriptor; }
+                            public Icon getIcon() { return getFieldIcon(access); }
+
+                            public String getDisplayName() {
+                                StringBuilder sb = new StringBuilder();
+                                sb.append(name).append(" : ");
+                                writeSignature(sb, descriptor, descriptor.length(), 0, false);
+                                return sb.toString();
+                            }
+                        });
+                    }
+                    return null;
+                }
+
+                @Override
+                public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+                    if ((access & (Opcodes.ACC_SYNTHETIC|Opcodes.ACC_ENUM|Opcodes.ACC_BRIDGE)) == 0) {
+                        methods.add(new Type.Method() {
+                            public int getFlags() { return access; }
+                            public String getName() { return name; }
+                            public String getDescriptor() { return descriptor; }
+                            public Icon getIcon() { return getMethodIcon(access); }
+
+                            public String getDisplayName() {
+                                boolean isInnerClass = (JavaType.this.displayInnerTypeName != null);
+                                String constructorName = isInnerClass ? JavaType.this.displayInnerTypeName : JavaType.this.displayTypeName;
+                                StringBuilder sb = new StringBuilder();
+                                writeMethodSignature(sb, JavaType.this.access, access, isInnerClass, constructorName, name, descriptor);
+                                return sb.toString();
+                            }
+                        });
+                    }
+                    return null;
+                }
+            };
+
+            classReader.accept(fieldsAndMethodsVisitor, ClassReader.SKIP_CODE|ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES);
+        }
+
+        @SuppressWarnings("unchecked")
+        protected String getDisplayTypeName(String name, int packageLength) {
+            int indexDollar = name.lastIndexOf('$');
+
+            if (indexDollar > packageLength) {
+                Container.Entry entry = getEntry(name);
+
+                if (entry != null) {
+                    try (InputStream is = entry.getInputStream()) {
+                        ClassReader classReader = new ClassReader(is);
+                        InnerClassVisitor classVisitor = new InnerClassVisitor(name);
+
+                        classReader.accept(classVisitor, ClassReader.SKIP_CODE|ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES);
+
+                        String outerName = classVisitor.getOuterName();
+
+                        if (outerName != null) {
+                            // Inner class path found => Recursive call
+                            return getDisplayTypeName(outerName, packageLength) + '.' + classVisitor.getInnerName();
+                        }
+                    } catch (IOException e) {
+                        assert ExceptionUtil.printStackTrace(e);
+                    }
+                }
+            }
+
+            return name;
+        }
+
+        protected Container.Entry getEntry(String typeName) {
+            String pathToFound = typeName + ".class";
+
+            for (Container.Entry entry : entry.getParent().getChildren()) {
+                if (entry.getPath().equals(pathToFound)) {
+                    return entry;
+                }
+            }
+
+            return null;
+        }
+
+        @Override public int getFlags() { return access; }
+        @Override public String getName() { return name; }
+        @Override public String getSuperName() { return superName; }
+        @Override public String getOuterName() { return outerName; }
+        @Override public String getDisplayPackageName() { return displayPackageName; }
+        @Override public String getDisplayTypeName() { return displayTypeName; }
+        @Override public String getDisplayInnerTypeName() { return displayInnerTypeName; }
+        @Override public Icon getIcon() { return getTypeIcon(access); }
+        @Override public List<Type> getInnerTypes() { return innerTypes; }
+        @Override public List<Type.Field> getFields() { return fields; }
+        @Override public List<Type.Method> getMethods() { return methods; }
+    }
+
+    protected static class InnerClassVisitor extends ClassVisitor {
+        protected String name;
+        protected String outerName;
+        protected String innerName;
+
+        public InnerClassVisitor(String name) {
+            super(Opcodes.ASM7);
+            this.name = name;
+        }
+
+        @Override
+        public void visitInnerClass(String name, String outerName, String innerName, int access) {
+            if (this.name.equals(name)) {
+                // Inner class path found
+                this.outerName = outerName;
+                this.innerName = innerName;
+            }
+        }
+
+        public String getOuterName() {
+            return outerName;
+        }
+
+        public String getInnerName() {
+            return innerName;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/type/JavaFileTypeFactoryProvider.java b/services/src/main/java/org/jd/gui/service/type/JavaFileTypeFactoryProvider.java
new file mode 100644
index 0000000..a0fac9f
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/type/JavaFileTypeFactoryProvider.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.type;
+
+import org.antlr.v4.runtime.ANTLRInputStream;
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.TerminalNode;
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Type;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.parser.antlr.ANTLRJavaParser;
+import org.jd.gui.util.parser.antlr.AbstractJavaListener;
+import org.jd.gui.util.parser.antlr.JavaParser;
+
+import javax.swing.*;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.*;
+
+public class JavaFileTypeFactoryProvider extends AbstractTypeFactoryProvider {
+
+    static {
+        // Early class loading
+        ANTLRJavaParser.parse(new ANTLRInputStream("class EarlyLoading{}"), new Listener(null));
+    }
+
+    // Create cache
+    protected Cache<URI, Listener> cache = new Cache<>();
+
+    @Override public String[] getSelectors() { return appendSelectors("*:file:*.java"); }
+
+    @Override
+    public Collection<Type> make(API api, Container.Entry entry) {
+        Listener listener = getListener(entry);
+
+        if (listener == null) {
+            return Collections.emptyList();
+        } else {
+            return listener.getRootTypes();
+        }
+    }
+
+    @Override
+    public Type make(API api, Container.Entry entry, String fragment) {
+        Listener listener = getListener(entry);
+
+        if (listener == null) {
+            return null;
+        } else {
+            if ((fragment != null) && (fragment.length() > 0)) {
+                // Search type name in fragment. URI format : see jd.gui.api.feature.UriOpener
+                int index = fragment.indexOf('-');
+
+                if (index != -1) {
+                    // Keep type name only
+                    fragment = fragment.substring(0, index);
+                }
+
+                return listener.getType(fragment);
+            } else {
+                return listener.getMainType();
+            }
+        }
+    }
+
+    protected Listener getListener(Container.Entry entry) {
+        URI key = entry.getUri();
+
+        if (cache.containsKey(key)) {
+            return cache.get(key);
+        } else {
+            Listener listener;
+
+            try (InputStream inputStream = entry.getInputStream()) {
+                ANTLRJavaParser.parse(new ANTLRInputStream(inputStream), listener = new Listener(entry));
+            } catch (IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+                listener = null;
+            }
+
+            cache.put(key, listener);
+            return listener;
+        }
+    }
+
+    protected static class JavaType implements Type {
+        protected int access;
+        protected String name;
+        protected String superName;
+        protected String outerName;
+
+        protected String displayTypeName;
+        protected String displayInnerTypeName;
+        protected String displayPackageName;
+
+        protected List<Type> innerTypes = new ArrayList<>();
+        protected List<Field> fields = new ArrayList<>();
+        protected List<Method> methods = new ArrayList<>();
+
+        protected JavaType outerType;
+
+        public JavaType(
+                int access, String name, String superName, String outerName,
+                String displayTypeName, String displayInnerTypeName, String displayPackageName,
+                JavaType outerType) {
+
+            this.access = access;
+            this.name = name;
+            this.superName = superName;
+            this.outerName = outerName;
+            this.displayTypeName = displayTypeName;
+            this.displayInnerTypeName = displayInnerTypeName;
+            this.displayPackageName = displayPackageName;
+            this.outerType = outerType;
+        }
+
+        public int getFlags() { return access; }
+        public String getName() { return name; }
+        public String getSuperName() { return superName; }
+        public String getOuterName() { return outerName; }
+        public String getDisplayTypeName() { return displayTypeName; }
+        public String getDisplayInnerTypeName() { return displayInnerTypeName; }
+        public String getDisplayPackageName() { return displayPackageName; }
+        public Icon getIcon() { return getTypeIcon(access); }
+        public JavaType getOuterType() { return outerType; }
+        public Collection<Type> getInnerTypes() { return innerTypes; }
+        public Collection<Field> getFields() { return fields; }
+        public Collection<Method> getMethods() { return methods; }
+    }
+
+    protected static class JavaField implements Type.Field {
+        protected int access;
+        protected String name;
+        protected String descriptor;
+
+        public JavaField(int access, String name, String descriptor) {
+            this.access = access;
+            this.name = name;
+            this.descriptor = descriptor;
+        }
+
+        public int getFlags() { return access; }
+        public String getName() { return name; }
+        public String getDescriptor() { return descriptor; }
+        public Icon getIcon() { return getFieldIcon(access); }
+
+        public String getDisplayName() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(name).append(" : ");
+            writeSignature(sb, descriptor, descriptor.length(), 0, false);
+            return sb.toString();
+        }
+    }
+
+    protected static class JavaMethod implements Type.Method {
+        protected JavaType type;
+        protected int access;
+        protected String name;
+        protected String descriptor;
+
+        public JavaMethod(JavaType type, int access, String name, String descriptor) {
+            this.type = type;
+            this.access = access;
+            this.name = name;
+            this.descriptor = descriptor;
+        }
+
+        public int getFlags() { return access; }
+        public String getName() { return name; }
+        public String getDescriptor() { return descriptor; }
+        public Icon getIcon() { return getMethodIcon(access); }
+
+        public String getDisplayName() {
+            String constructorName = type.getDisplayInnerTypeName();
+            boolean isInnerClass = (constructorName != null);
+
+            if (constructorName == null)
+                constructorName = type.getDisplayTypeName();
+
+            StringBuilder sb = new StringBuilder();
+            writeMethodSignature(sb, access, access, isInnerClass, constructorName, name, descriptor);
+            return sb.toString();
+        }
+    }
+
+    protected static class Listener extends AbstractJavaListener {
+
+        protected String displayPackageName = "";
+
+        protected JavaType mainType = null;
+        protected JavaType currentType = null;
+        protected ArrayList<Type> rootTypes = new ArrayList<>();
+        protected HashMap<String, Type> types = new HashMap<>();
+
+        public Listener(Container.Entry entry) {
+            super(entry);
+        }
+
+        public Type getMainType() {
+            return mainType;
+        }
+        public Type getType(String typeName) {
+            return types.get(typeName);
+        }
+        public ArrayList<Type> getRootTypes() {
+            return rootTypes;
+        }
+
+        // --- ANTLR Listener --- //
+
+        public void enterPackageDeclaration(JavaParser.PackageDeclarationContext ctx) {
+            super.enterPackageDeclaration(ctx);
+            displayPackageName = packageName.replace('/', '.');
+        }
+
+        public void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx) { enterTypeDeclaration(ctx, 0); }
+        public void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterEnumDeclaration(JavaParser.EnumDeclarationContext ctx) { enterTypeDeclaration(ctx, JavaType.FLAG_ENUM); }
+        public void exitEnumDeclaration(JavaParser.EnumDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterInterfaceDeclaration(JavaParser.InterfaceDeclarationContext ctx) { enterTypeDeclaration(ctx, JavaType.FLAG_INTERFACE); }
+        public void exitInterfaceDeclaration(JavaParser.InterfaceDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterAnnotationTypeDeclaration(JavaParser.AnnotationTypeDeclarationContext ctx) { enterTypeDeclaration(ctx, JavaType.FLAG_ANNOTATION); }
+        public void exitAnnotationTypeDeclaration(JavaParser.AnnotationTypeDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        protected void enterTypeDeclaration(ParserRuleContext ctx, int access) {
+            String name = ctx.getToken(JavaParser.Identifier, 0).getText();
+
+            JavaParser.TypeContext superType = ctx.getRuleContext(JavaParser.TypeContext.class, 0);
+            String superQualifiedTypeName;
+
+            if (superType == null) {
+                superQualifiedTypeName = ((access & JavaType.FLAG_INTERFACE) == 0) ? "java/lang/Object" : "";
+            } else {
+                superQualifiedTypeName = resolveInternalTypeName(superType.classOrInterfaceType().Identifier());
+            }
+
+            ParserRuleContext parent = ctx.getParent();
+
+            if (parent instanceof JavaParser.TypeDeclarationContext)
+                access += getTypeDeclarationContextAccessFlag(parent);
+            else if (parent instanceof JavaParser.MemberDeclarationContext)
+                access += getMemberDeclarationContextAccessFlag(parent.getParent());
+
+            if (currentType == null) {
+                String internalTypeName = packageName.isEmpty() ? name : packageName + "/" + name;
+                String outerName = null;
+                String displayTypeName = name;
+                String displayInnerTypeName = null;
+
+                currentType = new JavaType(access, internalTypeName, superQualifiedTypeName, outerName, displayTypeName, displayInnerTypeName, displayPackageName, null);
+                types.put(internalTypeName, currentType);
+                rootTypes.add(currentType);
+                nameToInternalTypeName.put(name, internalTypeName);
+
+                if (mainType == null) {
+                    mainType = currentType;
+                } else {
+                    // Multi class definitions in the same file
+                    String path = entry.getPath();
+                    int index = path.lastIndexOf('/') + 1;
+
+                    if (path.substring(index).startsWith(name + '.')) {
+                        // Select the correct root type
+                        mainType = currentType;
+                    }
+                }
+            } else {
+                String internalTypeName = currentType.getName() + '$' + name;
+                String outerName = currentType.getName();
+                String displayTypeName = currentType.getDisplayTypeName() + '.' + name;
+                String displayInnerTypeName = name;
+                JavaType subType = new JavaType(access, internalTypeName, superQualifiedTypeName, outerName, displayTypeName, displayInnerTypeName, displayPackageName, currentType);
+
+                currentType.getInnerTypes().add(subType);
+                currentType = subType;
+                types.put(internalTypeName, currentType);
+                nameToInternalTypeName.put(name, internalTypeName);
+            }
+        }
+
+        protected void exitTypeDeclaration() {
+            currentType = currentType.getOuterType();
+        }
+
+        public void enterClassBodyDeclaration(JavaParser.ClassBodyDeclarationContext ctx) {
+            if (ctx.getChildCount() == 2) {
+                ParseTree first = ctx.getChild(0);
+
+                if (first instanceof TerminalNode) {
+                    if (((TerminalNode)first).getSymbol().getType() == JavaParser.STATIC) {
+                        currentType.getMethods().add(new JavaMethod(currentType, JavaType.FLAG_STATIC, "<clinit>", "()V"));
+                    }
+                }
+            }
+        }
+
+        public void enterConstDeclaration(JavaParser.ConstDeclarationContext ctx) {
+            JavaParser.TypeContext typeContext = ctx.type();
+            int access = getClassBodyDeclarationAccessFlag(ctx.getParent().getParent());
+
+            for (JavaParser.ConstantDeclaratorContext constantDeclaratorContext : ctx.constantDeclarator()) {
+                TerminalNode identifier = constantDeclaratorContext.Identifier();
+                String name = identifier.getText();
+                int dimensionOnVariable = countDimension(constantDeclaratorContext.children);
+                String descriptor = createDescriptor(typeContext, dimensionOnVariable);
+
+                currentType.getFields().add(new JavaField(access, name, descriptor));
+            }
+        }
+
+        public void enterFieldDeclaration(JavaParser.FieldDeclarationContext ctx) {
+            JavaParser.TypeContext typeContext = ctx.type();
+            int access = getClassBodyDeclarationAccessFlag(ctx.getParent().getParent());
+
+            for (JavaParser.VariableDeclaratorContext declaration : ctx.variableDeclarators().variableDeclarator()) {
+                JavaParser.VariableDeclaratorIdContext variableDeclaratorId = declaration.variableDeclaratorId();
+                TerminalNode identifier = variableDeclaratorId.Identifier();
+                String name = identifier.getText();
+                int dimensionOnVariable = countDimension(variableDeclaratorId.children);
+                String descriptor = createDescriptor(typeContext, dimensionOnVariable);
+
+                currentType.getFields().add(new JavaField(access, name, descriptor));
+            }
+        }
+
+        public void enterMethodDeclaration(JavaParser.MethodDeclarationContext ctx) {
+            enterMethodDeclaration(ctx, ctx.Identifier(), ctx.formalParameters(), ctx.type());
+        }
+
+        public void enterInterfaceMethodDeclaration(JavaParser.InterfaceMethodDeclarationContext ctx) {
+            enterMethodDeclaration(ctx, ctx.Identifier(), ctx.formalParameters(), ctx.type());
+        }
+
+        public void enterMethodDeclaration(
+                ParserRuleContext ctx, TerminalNode identifier,
+                JavaParser.FormalParametersContext formalParameters, JavaParser.TypeContext returnType) {
+
+            int access = getClassBodyDeclarationAccessFlag(ctx.getParent().getParent());
+            String name = identifier.getText();
+            String paramDescriptors = createParamDescriptors(formalParameters.formalParameterList());
+            String returnDescriptor = createDescriptor(returnType, 0);
+            String descriptor = paramDescriptors + returnDescriptor;
+
+            currentType.getMethods().add(new JavaMethod(currentType, access, name, descriptor));
+        }
+
+        public void enterConstructorDeclaration(JavaParser.ConstructorDeclarationContext ctx) {
+            int access = getClassBodyDeclarationAccessFlag(ctx.getParent().getParent());
+            String paramDescriptors = createParamDescriptors(ctx.formalParameters().formalParameterList());
+            String descriptor = paramDescriptors + "V";
+
+            currentType.getMethods().add(new JavaMethod(currentType, access, "<init>", descriptor));
+        }
+
+        protected String createParamDescriptors(JavaParser.FormalParameterListContext formalParameterList) {
+            StringBuilder paramDescriptors = null;
+
+            if (formalParameterList != null) {
+                List<JavaParser.FormalParameterContext> formalParameters = formalParameterList.formalParameter();
+                paramDescriptors = new StringBuilder("(");
+
+                for (JavaParser.FormalParameterContext formalParameter : formalParameters) {
+                    int dimensionOnParameter = countDimension(formalParameter.variableDeclaratorId().children);
+                    paramDescriptors.append(createDescriptor(formalParameter.type(), dimensionOnParameter));
+                }
+            }
+
+            return (paramDescriptors == null) ? "()" : paramDescriptors.append(')').toString();
+        }
+
+        protected int getTypeDeclarationContextAccessFlag(ParserRuleContext ctx) {
+            int access = 0;
+
+            for (JavaParser.ClassOrInterfaceModifierContext coiModifierContext : ctx.getRuleContexts(JavaParser.ClassOrInterfaceModifierContext.class)) {
+                access += getAccessFlag(coiModifierContext);
+            }
+
+            return access;
+        }
+
+        protected int getMemberDeclarationContextAccessFlag(ParserRuleContext ctx) {
+            int access = 0;
+
+            for (JavaParser.ModifierContext modifierContext : ctx.getRuleContexts(JavaParser.ModifierContext.class)) {
+                JavaParser.ClassOrInterfaceModifierContext coiModifierContext = modifierContext.classOrInterfaceModifier();
+                if (coiModifierContext != null) {
+                    access += getAccessFlag(coiModifierContext);
+                }
+            }
+
+            return access;
+        }
+
+        protected int getClassBodyDeclarationAccessFlag(ParserRuleContext ctx) {
+            if ((currentType.access & JavaType.FLAG_INTERFACE) == 0) {
+                int access = 0;
+
+                for (JavaParser.ModifierContext modifierContext : ctx.getRuleContexts(JavaParser.ModifierContext.class)) {
+                    JavaParser.ClassOrInterfaceModifierContext coimc = modifierContext.classOrInterfaceModifier();
+
+                    if (coimc != null) {
+                        access += getAccessFlag(coimc);
+                    }
+                }
+
+                return access;
+            } else {
+                return JavaType.FLAG_PUBLIC;
+            }
+        }
+
+        protected int getAccessFlag(JavaParser.ClassOrInterfaceModifierContext ctx) {
+            if (ctx.getChildCount() == 1) {
+                ParseTree first = ctx.getChild(0);
+
+                if (first instanceof TerminalNode) {
+                    switch (((TerminalNode)first).getSymbol().getType()) {
+                        case JavaParser.STATIC:    return JavaType.FLAG_STATIC;
+                        case JavaParser.FINAL:     return JavaType.FLAG_FINAL;
+                        case JavaParser.ABSTRACT:  return JavaType.FLAG_ABSTRACT;
+                        case JavaParser.PUBLIC:    return JavaType.FLAG_PUBLIC;
+                        case JavaParser.PROTECTED: return JavaType.FLAG_PROTECTED;
+                        case JavaParser.PRIVATE:   return JavaType.FLAG_PRIVATE;
+                    }
+                }
+            }
+
+            return 0;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/service/uriloader/FileUriLoaderProvider.java b/services/src/main/java/org/jd/gui/service/uriloader/FileUriLoaderProvider.java
new file mode 100644
index 0000000..211972a
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/service/uriloader/FileUriLoaderProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.service.uriloader;
+
+import org.jd.gui.api.API;
+import org.jd.gui.spi.FileLoader;
+import org.jd.gui.spi.UriLoader;
+
+import java.io.File;
+import java.net.URI;
+
+public class FileUriLoaderProvider implements UriLoader {
+    protected static final String[] SCHEMES = { "file" };
+
+    public String[] getSchemes() { return SCHEMES; }
+
+    public boolean accept(API api, URI uri) { return "file".equals(uri.getScheme()); }
+
+    public boolean load(API api, URI uri) {
+        File file = new File(uri.getPath());
+        FileLoader fileLoader = api.getFileLoader(file);
+
+        return (fileLoader != null) && fileLoader.load(api, file);
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/container/JarContainerEntryUtil.java b/services/src/main/java/org/jd/gui/util/container/JarContainerEntryUtil.java
new file mode 100644
index 0000000..d710060
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/container/JarContainerEntryUtil.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.container;
+
+import org.jd.gui.api.model.Container;
+import org.jd.gui.model.container.ContainerEntryComparator;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+
+public class JarContainerEntryUtil {
+    public static Collection<Container.Entry> removeInnerTypeEntries(Collection<Container.Entry> entries) {
+        HashSet<String> potentialOuterTypePaths = new HashSet<>();
+        Collection<Container.Entry> filtredSubEntries;
+
+        for (Container.Entry e : entries) {
+            if (!e.isDirectory()) {
+                String p = e.getPath();
+
+                if (p.toLowerCase().endsWith(".class")) {
+                    int lastSeparatorIndex = p.lastIndexOf('/');
+                    int dollarIndex = p.substring(lastSeparatorIndex+1).indexOf('$');
+
+                    if (dollarIndex != -1) {
+                        potentialOuterTypePaths.add(p.substring(0, lastSeparatorIndex+1+dollarIndex) + ".class");
+                    }
+                }
+            }
+        }
+
+        if (potentialOuterTypePaths.size() == 0) {
+            filtredSubEntries = entries;
+        } else {
+            HashSet<String> innerTypePaths = new HashSet<>();
+
+            for (Container.Entry e : entries) {
+                if (!e.isDirectory() && potentialOuterTypePaths.contains(e.getPath())) {
+                    populateInnerTypePaths(innerTypePaths, e);
+                }
+            }
+
+            filtredSubEntries = new ArrayList<>();
+
+            for (Container.Entry e : entries) {
+                if (!e.isDirectory()) {
+                    String p = e.getPath();
+
+                    if (p.toLowerCase().endsWith(".class")) {
+                        int indexDollar = p.lastIndexOf('$');
+
+                        if (indexDollar != -1) {
+                            int indexSeparator = p.lastIndexOf('/');
+
+                            if (indexDollar > indexSeparator) {
+                                if (innerTypePaths.contains(p)) {
+                                    // Inner class found -> Skip
+                                    continue;
+                                } else {
+                                    populateInnerTypePaths(innerTypePaths, e);
+
+                                    if (innerTypePaths.contains(p)) {
+                                        // Inner class found -> Skip
+                                        continue;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                // Valid path
+                filtredSubEntries.add(e);
+            }
+        }
+
+        List<Container.Entry> list = new ArrayList<>(filtredSubEntries);
+        list.sort(ContainerEntryComparator.COMPARATOR);
+
+        return list;
+    }
+
+    protected static void populateInnerTypePaths(final HashSet<String> innerTypePaths, Container.Entry entry) {
+        try (InputStream is = entry.getInputStream()) {
+            ClassReader classReader = new ClassReader(is);
+            String p = entry.getPath();
+            final String prefixPath = p.substring(0, p.length() - classReader.getClassName().length() - 6);
+
+            ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM7) {
+                public void visitInnerClass(final String name, final String outerName, final String innerName, final int access) {
+                    innerTypePaths.add(prefixPath + name + ".class");
+                }
+            };
+
+            classReader.accept(classVisitor, ClassReader.SKIP_CODE|ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES);
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/decompiler/ClassPathLoader.java b/services/src/main/java/org/jd/gui/util/decompiler/ClassPathLoader.java
new file mode 100644
index 0000000..9880279
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/decompiler/ClassPathLoader.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.decompiler;
+
+import org.jd.core.v1.api.loader.Loader;
+import org.jd.core.v1.api.loader.LoaderException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ClassPathLoader implements Loader {
+    protected byte[] buffer = new byte[1024 * 4];
+
+    @Override
+    public boolean canLoad(String internalName) {
+        return this.getClass().getResource("/" + internalName + ".class") != null;
+    }
+
+    @Override
+    public byte[] load(String internalName) throws LoaderException {
+        InputStream is = this.getClass().getResourceAsStream("/" + internalName + ".class");
+
+        if (is == null) {
+            return null;
+        } else {
+            try (InputStream input=is; ByteArrayOutputStream output=new ByteArrayOutputStream()) {
+                int len = input.read(buffer);
+
+                while (len > 0) {
+                    output.write(buffer, 0, len);
+                    len = input.read(buffer);
+                }
+
+                return output.toByteArray();
+            } catch (IOException e) {
+                throw new LoaderException(e);
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/decompiler/ContainerLoader.java b/services/src/main/java/org/jd/gui/util/decompiler/ContainerLoader.java
new file mode 100644
index 0000000..9d491bf
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/decompiler/ContainerLoader.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.decompiler;
+
+import org.jd.core.v1.api.loader.Loader;
+import org.jd.core.v1.api.loader.LoaderException;
+import org.jd.gui.api.model.Container;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ContainerLoader implements Loader {
+    protected byte[] buffer = new byte[1024 * 4];
+    protected Container.Entry entry;
+
+    public ContainerLoader() { this.entry = null; }
+    public ContainerLoader(Container.Entry entry) {
+        this.entry = entry;
+    }
+
+    public void setEntry(Container.Entry e) { this.entry = e; }
+
+    protected Container.Entry getEntry(String internalPath) {
+        String path = internalPath + ".class";
+
+        if (entry.getPath().equals(path)) {
+            return entry;
+        }
+        for (Container.Entry e : entry.getParent().getChildren()) {
+            if (e.getPath().equals(path)) {
+                return e;
+            }
+        }
+        return null;
+    }
+
+    // --- Loader --- //
+    @Override
+    public boolean canLoad(String internalName) {
+        return getEntry(internalName) != null;
+    }
+
+    @Override
+    public byte[] load(String internalName) throws LoaderException {
+        Container.Entry entry = getEntry(internalName);
+
+        if (entry == null) {
+            return null;
+        } else {
+            try (InputStream input=entry.getInputStream(); ByteArrayOutputStream output=new ByteArrayOutputStream()) {
+                int len = input.read(buffer);
+
+                while (len > 0) {
+                    output.write(buffer, 0, len);
+                    len = input.read(buffer);
+                }
+
+                return output.toByteArray();
+            } catch (IOException e) {
+                throw new LoaderException(e);
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/decompiler/LineNumberStringBuilderPrinter.java b/services/src/main/java/org/jd/gui/util/decompiler/LineNumberStringBuilderPrinter.java
new file mode 100644
index 0000000..018384a
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/decompiler/LineNumberStringBuilderPrinter.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.decompiler;
+
+public class LineNumberStringBuilderPrinter extends StringBuilderPrinter {
+    protected boolean showLineNumbers = false;
+
+    protected int maxLineNumber = 0;
+    protected int digitCount = 0;
+
+    protected String lineNumberBeginPrefix;
+    protected String lineNumberEndPrefix;
+    protected String unknownLineNumberPrefix;
+
+    public void setShowLineNumbers(boolean showLineNumbers) { this.showLineNumbers = showLineNumbers; }
+
+    protected int printDigit(int dcv, int lineNumber, int divisor, int left) {
+        if (digitCount >= dcv) {
+            if (lineNumber < divisor) {
+                stringBuffer.append(' ');
+            } else {
+                int e = (lineNumber-left) / divisor;
+                stringBuffer.append((char)('0' + e));
+                left += e*divisor;
+            }
+        }
+
+        return left;
+    }
+
+    // --- Printer --- //
+    @Override
+    public void start(int maxLineNumber, int majorVersion, int minorVersion) {
+        super.start(maxLineNumber, majorVersion, minorVersion);
+
+        if (showLineNumbers) {
+            this.maxLineNumber = maxLineNumber;
+
+            if (maxLineNumber > 0) {
+                digitCount = 1;
+                unknownLineNumberPrefix = " ";
+                int maximum = 9;
+
+                while (maximum < maxLineNumber) {
+                    digitCount++;
+                    unknownLineNumberPrefix += ' ';
+                    maximum = maximum*10 + 9;
+                }
+
+                lineNumberBeginPrefix = "/* ";
+                lineNumberEndPrefix = " */ ";
+            } else {
+                unknownLineNumberPrefix = "";
+                lineNumberBeginPrefix = "";
+                lineNumberEndPrefix = "";
+            }
+        } else {
+            this.maxLineNumber = 0;
+            unknownLineNumberPrefix = "";
+            lineNumberBeginPrefix = "";
+            lineNumberEndPrefix = "";
+        }
+    }
+
+    @Override public void startLine(int lineNumber) {
+        if (maxLineNumber > 0) {
+            stringBuffer.append(lineNumberBeginPrefix);
+
+            if (lineNumber == UNKNOWN_LINE_NUMBER) {
+                stringBuffer.append(unknownLineNumberPrefix);
+            } else {
+                int left = 0;
+
+                left = printDigit(5, lineNumber, 10000, left);
+                left = printDigit(4, lineNumber,  1000, left);
+                left = printDigit(3, lineNumber,   100, left);
+                left = printDigit(2, lineNumber,    10, left);
+                stringBuffer.append((char)('0' + (lineNumber-left)));
+            }
+
+            stringBuffer.append(lineNumberEndPrefix);
+        }
+
+        for (int i=0; i<indentationCount; i++) {
+            stringBuffer.append(TAB);
+        }
+    }
+    @Override public void extraLine(int count) {
+        if (realignmentLineNumber) {
+            while (count-- > 0) {
+                if (maxLineNumber > 0) {
+                    stringBuffer.append(lineNumberBeginPrefix);
+                    stringBuffer.append(unknownLineNumberPrefix);
+                    stringBuffer.append(lineNumberEndPrefix);
+                }
+
+                stringBuffer.append(NEWLINE);
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/decompiler/NopPrinter.java b/services/src/main/java/org/jd/gui/util/decompiler/NopPrinter.java
new file mode 100644
index 0000000..363b8d1
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/decompiler/NopPrinter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.decompiler;
+
+import org.jd.core.v1.api.printer.Printer;
+
+public class NopPrinter implements Printer {
+	@Override public void start(int maxLineNumber, int majorVersion, int minorVersion) {}
+	@Override public void end() {}
+
+	@Override public void printText(String text) {}
+	@Override public void printNumericConstant(String constant) {}
+	@Override public void printStringConstant(String constant, String ownerInternalName) {}
+	@Override public void printKeyword(String keyword) {}
+
+	@Override public void printDeclaration(int flags, String internalTypeName, String name, String descriptor) {}
+	@Override public void printReference(int flags, String internalTypeName, String name, String descriptor, String ownerInternalName) {}
+
+	@Override public void indent() {}
+	@Override public void unindent() {}
+
+	@Override public void startLine(int lineNumber) {}
+	@Override public void endLine() {}
+	@Override public void extraLine(int count) {}
+
+	@Override public void startMarker(int type) {}
+	@Override public void endMarker(int type) {}
+}
diff --git a/services/src/main/java/org/jd/gui/util/decompiler/StringBuilderPrinter.java b/services/src/main/java/org/jd/gui/util/decompiler/StringBuilderPrinter.java
new file mode 100644
index 0000000..345a615
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/decompiler/StringBuilderPrinter.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.decompiler;
+
+import org.jd.core.v1.api.printer.Printer;
+
+public class StringBuilderPrinter implements Printer {
+    protected static final String TAB = "  ";
+    protected static final String NEWLINE = "\n";
+
+    protected StringBuilder stringBuffer = new StringBuilder(10*1024);
+
+    protected boolean unicodeEscape = true;
+    protected boolean realignmentLineNumber = false;
+
+    protected int majorVersion = 0;
+    protected int minorVersion = 0;
+    protected int indentationCount;
+
+    public void setUnicodeEscape(boolean unicodeEscape) { this.unicodeEscape = unicodeEscape; }
+    public void setRealignmentLineNumber(boolean realignmentLineNumber) { this.realignmentLineNumber = realignmentLineNumber; }
+
+    public int getMajorVersion() { return majorVersion; }
+    public int getMinorVersion() { return minorVersion; }
+    public StringBuilder getStringBuffer() { return stringBuffer; }
+
+    protected void escape(String s) {
+        if (unicodeEscape && (s != null)) {
+            int length = s.length();
+
+            for (int i=0; i<length; i++) {
+                char c = s.charAt(i);
+
+                if (c == '\t') {
+                    stringBuffer.append(c);
+                } else if (c < 32) {
+                    // Write octal format
+                    stringBuffer.append("\\0");
+                    stringBuffer.append((char) ('0' + (c >> 3)));
+                    stringBuffer.append((char) ('0' + (c & 0x7)));
+                } else if (c > 127) {
+                    // Write octal format
+                    stringBuffer.append("\\u");
+
+                    int z = (c >> 12);
+                    stringBuffer.append((char) ((z <= 9) ? ('0' + z) : (('A' - 10) + z)));
+                    z = ((c >> 8) & 0xF);
+                    stringBuffer.append((char) ((z <= 9) ? ('0' + z) : (('A' - 10) + z)));
+                    z = ((c >> 4) & 0xF);
+                    stringBuffer.append((char) ((z <= 9) ? ('0' + z) : (('A' - 10) + z)));
+                    z = (c & 0xF);
+                    stringBuffer.append((char) ((z <= 9) ? ('0' + z) : (('A' - 10) + z)));
+                } else {
+                    stringBuffer.append(c);
+                }
+            }
+        } else {
+            stringBuffer.append(s);
+        }
+    }
+
+    // --- Printer --- //
+    @Override
+    public void start(int maxLineNumber, int majorVersion, int minorVersion) {
+        this.stringBuffer.setLength(0);
+        this.majorVersion = majorVersion;
+        this.minorVersion = minorVersion;
+        this.indentationCount = 0;
+    }
+
+    @Override public void end() {}
+
+    @Override public void printText(String text) { escape(text); }
+    @Override public void printNumericConstant(String constant) { escape(constant); }
+    @Override public void printStringConstant(String constant, String ownerInternalName) { escape(constant); }
+    @Override public void printKeyword(String keyword) { stringBuffer.append(keyword); }
+
+    @Override public void printDeclaration(int type, String internalTypeName, String name, String descriptor) { escape(name); }
+    @Override public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) { escape(name); }
+
+    @Override public void indent() { indentationCount++; }
+    @Override public void unindent() { if (indentationCount > 0) indentationCount--; }
+
+    @Override public void startLine(int lineNumber) { for (int i=0; i<indentationCount; i++) stringBuffer.append(TAB); }
+    @Override public void endLine() { stringBuffer.append(NEWLINE); }
+    @Override public void extraLine(int count) { if (realignmentLineNumber) while (count-- > 0) stringBuffer.append(NEWLINE); }
+
+    @Override public void startMarker(int type) {}
+    @Override public void endMarker(int type) {}
+}
diff --git a/services/src/main/java/org/jd/gui/util/exception/ExceptionUtil.java b/services/src/main/java/org/jd/gui/util/exception/ExceptionUtil.java
new file mode 100644
index 0000000..d931686
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/exception/ExceptionUtil.java
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.exception;
+
+public class ExceptionUtil {
+    public static boolean printStackTrace(Throwable throwable) {
+        throwable.printStackTrace();
+        return true;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/index/IndexesUtil.java b/services/src/main/java/org/jd/gui/util/index/IndexesUtil.java
new file mode 100644
index 0000000..6897297
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/index/IndexesUtil.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.index;
+
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Future;
+
+public class IndexesUtil {
+    public static boolean containsInternalTypeName(Collection<Future<Indexes>> collectionOfFutureIndexes, String internalTypeName) {
+        return contains(collectionOfFutureIndexes, "typeDeclarations", internalTypeName);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static List<Container.Entry> findInternalTypeName(Collection<Future<Indexes>> collectionOfFutureIndexes, String internalTypeName) {
+        return find(collectionOfFutureIndexes, "typeDeclarations", internalTypeName);
+    }
+
+    public static boolean contains(Collection<Future<Indexes>> collectionOfFutureIndexes, String indexName, String key) {
+        try {
+            for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
+                if (futureIndexes.isDone()) {
+                    Map<String, Collection> index = futureIndexes.get().getIndex(indexName);
+                    if ((index != null) && (index.get(key) != null)) {
+                        return true;
+                    }
+                }
+            }
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        return false;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static List<Container.Entry> find(Collection<Future<Indexes>> collectionOfFutureIndexes, String indexName, String key) {
+        ArrayList<Container.Entry> entries = new ArrayList<>();
+
+        try {
+            for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
+                if (futureIndexes.isDone()) {
+                    Map<String, Collection> index = futureIndexes.get().getIndex(indexName);
+                    if (index != null) {
+                        Collection<Container.Entry> collection = index.get(key);
+                        if (collection != null) {
+                            entries.addAll(collection);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        return entries;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/io/NewlineOutputStream.java b/services/src/main/java/org/jd/gui/util/io/NewlineOutputStream.java
new file mode 100644
index 0000000..9407ff4
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/io/NewlineOutputStream.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.io;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+public class NewlineOutputStream extends FilterOutputStream {
+    private static byte[] lineSeparator;
+
+    public NewlineOutputStream(OutputStream os) {
+        super(os);
+
+        if (lineSeparator == null) {
+            String s = System.getProperty("line.separator");
+
+            if ((s == null) || (s.length() <= 0))
+                s = "\n";
+
+            lineSeparator = s.getBytes(Charset.forName("UTF-8"));
+        }
+    }
+
+    public void write(int b) throws IOException {
+        if (b == '\n') {
+            out.write(lineSeparator);
+        } else {
+            out.write(b);
+        }
+    }
+
+    public void write(byte b[]) throws IOException {
+        write(b, 0, b.length);
+    }
+
+    public void write(byte b[], int off, int len) throws IOException {
+        int i;
+
+        for (i=off; i<len; i++) {
+            if (b[i] == '\n') {
+                out.write(b, off, i-off);
+                out.write(lineSeparator);
+                off = i+1;
+            }
+        }
+
+        out.write(b, off, i-off);
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/io/TextReader.java b/services/src/main/java/org/jd/gui/util/io/TextReader.java
new file mode 100644
index 0000000..6dc8e0c
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/io/TextReader.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.io;
+
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.io.*;
+
+public class TextReader {
+
+    public static String getText(File file) {
+        try {
+            return getText(new FileInputStream(file));
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+            return "";
+        }
+    }
+
+    public static String getText(InputStream is) {
+        StringBuilder sb = new StringBuilder();
+        char[] charBuffer = new char[8192];
+        int nbCharRead;
+
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
+            while ((nbCharRead = reader.read(charBuffer)) != -1) {
+                // appends buffer
+                sb.append(charBuffer, 0, nbCharRead);
+            }
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        return sb.toString();
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/matcher/DescriptorMatcher.java b/services/src/main/java/org/jd/gui/util/matcher/DescriptorMatcher.java
new file mode 100644
index 0000000..c6fd6bd
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/matcher/DescriptorMatcher.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.matcher;
+
+/*
+ * Descriptor format : @see jd.gui.api.feature.UriOpenable
+ */
+public class DescriptorMatcher {
+
+    public static boolean matchFieldDescriptors(String d1, String d2) {
+        return matchDescriptors(new CharBuffer(d1), new CharBuffer(d2));
+    }
+
+    protected static boolean matchDescriptors(CharBuffer cb1, CharBuffer cb2) {
+        if (cb1.read() == '?') {
+            if (cb2.read() == '?') {
+                return true;
+            } else {
+                cb2.unread();
+                return cb2.skipType();
+            }
+        } else {
+            cb1.unread();
+
+            if (cb2.read() == '?') {
+                return cb1.skipType();
+            } else {
+                cb2.unread();
+                return cb1.compareTypeWith(cb2);
+            }
+        }
+    }
+
+    public static boolean matchMethodDescriptors(String d1, String d2) {
+        CharBuffer cb1 = new CharBuffer(d1);
+        CharBuffer cb2 = new CharBuffer(d2);
+
+        if ((cb1.read() != '(') || (cb2.read() != '('))
+            return false;
+
+        if (cb1.read() == '*') {
+            return true;
+        }
+        if (cb2.read() == '*') {
+            return true;
+        }
+
+        cb1.unread();
+        cb2.unread();
+
+        // Check parameter descriptors
+        while (cb2.get() != ')') {
+            if (!matchDescriptors(cb1, cb2))
+                return false;
+        }
+
+        if ((cb1.read() != ')') || (cb2.read() != ')'))
+            return false;
+
+        // Check return descriptor
+        return matchDescriptors(cb1, cb2);
+    }
+
+    protected static class CharBuffer {
+        protected char[] buffer;
+        protected int length;
+        protected int offset;
+
+        public CharBuffer(String s) {
+            this.buffer = s.toCharArray();
+            this.length = buffer.length;
+            this.offset = 0;
+        }
+
+        public char read() {
+            if (offset < length)
+                return buffer[offset++];
+            else
+                return (char)0;
+        }
+
+        public boolean unread() {
+            if (offset > 0) {
+                offset--;
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        public char get() {
+            if (offset < length)
+                return buffer[offset];
+            else
+                return (char)0;
+        }
+
+        public boolean skipType() {
+            if (offset < length) {
+                char c = buffer[offset++];
+
+                while ((c == '[') && (offset < length)) {
+                    c = buffer[offset++];
+                }
+
+                if (c == 'L') {
+                    while (offset < length) {
+                        if (buffer[offset++] == ';')
+                            return true;
+                    }
+                } else if (c != '[') {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        public boolean compareTypeWith(CharBuffer other) {
+            if (offset >= length)
+                return false;
+
+            char c = buffer[offset++];
+
+            if (c != other.read())
+                return false;
+
+            if (c == 'L') {
+                if ((offset >= length) || (other.offset >= other.length))
+                    return false;
+
+                char[] otherBuffer = other.buffer;
+
+                if ((buffer[offset] == '*') || (otherBuffer[other.offset] == '*')) {
+                    int start = offset;
+                    int otherStart = other.offset;
+
+                    // Search ';'
+                    if (!searchEndOfType() || !other.searchEndOfType())
+                        return false;
+
+                    int current = offset - 1;
+                    int otherCurrent = other.offset - 1;
+
+                    // Backward comparison
+                    while ((start < current) && (otherStart < otherCurrent)) {
+                        c = buffer[--current];
+                        if (c == '*')
+                            return true;
+
+                        char otherC = otherBuffer[--otherCurrent];
+                        if (otherC == '*')
+                            return true;
+                        if (c != otherC)
+                            return false;
+                    }
+                } else {
+                    // Forward comparison
+                    while (offset < length) {
+                        c = buffer[offset++];
+                        if (c != other.read())
+                            return false;
+                        if (c == ';')
+                            return true;
+                    }
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        protected boolean searchEndOfType() {
+            while (offset < length) {
+                if (buffer[offset++] == ';')
+                    return true;
+            }
+            return false;
+        }
+
+        public String toString() {
+            return new String(buffer, offset, length-offset);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/parser/antlr/ANTLRJavaParser.java b/services/src/main/java/org/jd/gui/util/parser/antlr/ANTLRJavaParser.java
new file mode 100644
index 0000000..13bb4d5
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/parser/antlr/ANTLRJavaParser.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.parser.antlr;
+
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.ParseTreeWalker;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+public class ANTLRJavaParser {
+    public static void parse(CharStream input, JavaListener listener) {
+        try {
+            JavaLexer lexer = new JavaLexer(input);
+
+            lexer.removeErrorListeners();
+
+            CommonTokenStream tokens = new CommonTokenStream(lexer);
+            JavaParser parser = new JavaParser(tokens);
+
+            parser.removeErrorListeners();
+
+            ParseTree tree = parser.compilationUnit();
+
+            ParseTreeWalker.DEFAULT.walk(listener, tree);
+        } catch (StackOverflowError e) {
+            // Too complex source file, probably not written by a human.
+            // This error may happen on Java file generated by ANTLR for example.
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/parser/antlr/AbstractJavaListener.java b/services/src/main/java/org/jd/gui/util/parser/antlr/AbstractJavaListener.java
new file mode 100644
index 0000000..ab312e4
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/parser/antlr/AbstractJavaListener.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.parser.antlr;
+
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.TerminalNode;
+import org.antlr.v4.runtime.tree.TerminalNodeImpl;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.util.HashMap;
+import java.util.List;
+
+public abstract class AbstractJavaListener extends JavaBaseListener {
+    protected Container.Entry entry;
+    protected String packageName = "";
+    protected HashMap<String, String> nameToInternalTypeName = new HashMap<>();
+    protected StringBuilder sb = new StringBuilder();
+    protected HashMap<String, String> typeNameCache = new HashMap<>();
+
+    public AbstractJavaListener(Container.Entry entry) {
+        this.entry = entry;
+    }
+
+    public void enterPackageDeclaration(JavaParser.PackageDeclarationContext ctx) {
+        packageName = concatIdentifiers(ctx.qualifiedName().Identifier());
+    }
+
+    public void enterImportDeclaration(JavaParser.ImportDeclarationContext ctx) {
+        List<TerminalNode> identifiers = ctx.qualifiedName().Identifier();
+        int size = identifiers.size();
+
+        if (size > 1) {
+            nameToInternalTypeName.put(identifiers.get(size - 1).getText(), concatIdentifiers(identifiers));
+        }
+    }
+
+    protected String concatIdentifiers(List<TerminalNode> identifiers) {
+        switch (identifiers.size()) {
+            case 0:
+                return "";
+            case 1:
+                return identifiers.get(0).getText();
+            default:
+                sb.setLength(0);
+
+                for (TerminalNode identifier : identifiers) {
+                    sb.append(identifier.getText()).append('/');
+                }
+
+                // Remove last separator
+                sb.setLength(sb.length() - 1);
+
+                return sb.toString();
+        }
+    }
+
+    protected String resolveInternalTypeName(List<TerminalNode> identifiers) {
+        switch (identifiers.size()) {
+            case 0:
+                return null;
+
+            case 1:
+                // Search in cache
+                String name = identifiers.get(0).getText();
+                String qualifiedName = typeNameCache.get(name);
+
+                if (qualifiedName != null) {
+                    return qualifiedName;
+                }
+
+                // Search in imports
+                String imp = nameToInternalTypeName.get(name);
+
+                if (imp != null) {
+                    // Import found
+                    return imp;
+                }
+
+                // Search type in same package
+                String prefix = name + '.';
+
+                if (entry.getPath().indexOf('/') != -1) {
+                    // Not in root package
+                    Container.Entry parent = entry.getParent();
+                    int packageLength = parent.getPath().length() + 1;
+
+                    for (Container.Entry child : parent.getChildren()) {
+                        if (!child.isDirectory() && child.getPath().substring(packageLength).startsWith(prefix)) {
+                            qualifiedName = packageName + '/' + name;
+                            typeNameCache.put(name, qualifiedName);
+                            return qualifiedName;
+                        }
+                    }
+                }
+
+                // Search type in root package
+                for (Container.Entry child : entry.getContainer().getRoot().getChildren()) {
+                    if (!child.isDirectory() && child.getPath().startsWith(prefix)) {
+                        typeNameCache.put(name, name);
+                        return name;
+                    }
+                }
+
+                // Search type in 'java.lang'
+                try {
+                    if (Class.forName("java.lang." + name) != null) {
+                        qualifiedName = "java/lang/" + name;
+                        typeNameCache.put(name, qualifiedName);
+                        return qualifiedName;
+                    }
+                } catch (ClassNotFoundException ignore) {
+                    // Ignore class loading error
+                }
+
+                // Type not found
+                qualifiedName = "*/" + name;
+                typeNameCache.put(name, qualifiedName);
+                return qualifiedName;
+
+            default:
+                // Qualified type name -> Nothing to do
+                return concatIdentifiers(identifiers);
+        }
+    }
+
+    protected String createDescriptor(JavaParser.TypeContext typeContext, int dimension) {
+        if (typeContext == null) {
+            return "V";
+        } else {
+            dimension += countDimension(typeContext.children);
+            JavaParser.PrimitiveTypeContext primitive = typeContext.primitiveType();
+            String name;
+
+            if (primitive == null) {
+                JavaParser.ClassOrInterfaceTypeContext type = typeContext.classOrInterfaceType();
+                List<JavaParser.TypeArgumentsContext> typeArgumentsContexts = type.typeArguments();
+
+                if (typeArgumentsContexts.size() == 1) {
+                    JavaParser.TypeArgumentsContext typeArgumentsContext = typeArgumentsContexts.get(0);
+                    List<JavaParser.TypeArgumentContext> typeArguments = typeArgumentsContext.typeArgument();
+                } else if (typeArgumentsContexts.size() > 1) {
+                    throw new RuntimeException("UNEXPECTED");
+                }
+
+                name = "L" + resolveInternalTypeName(type.Identifier()) + ";";
+            } else {
+                // Search primitive
+                switch (primitive.getText()) {
+                    case "boolean": name = "Z"; break;
+                    case "byte":    name = "B"; break;
+                    case "char":    name = "C"; break;
+                    case "double":  name = "D"; break;
+                    case "float":   name = "F"; break;
+                    case "int":     name = "I"; break;
+                    case "long":    name = "J"; break;
+                    case "short":   name = "S"; break;
+                    case "void":    name = "V"; break;
+                    default:
+                        throw new RuntimeException("UNEXPECTED PRIMITIVE");
+                }
+            }
+
+            switch (dimension) {
+                case 0:  return name;
+                case 1:  return "[" + name;
+                case 2:  return "[[" + name;
+                default: return new String(new char[dimension]).replace('\0', '[') + name;
+            }
+        }
+    }
+
+    protected int countDimension(List<ParseTree> children) {
+        int dimension = 0;
+
+        for (ParseTree child : children) {
+            if (child instanceof TerminalNodeImpl) {
+                if (((TerminalNodeImpl)child).getSymbol().getType() == JavaParser.LBRACK)
+                    dimension++;
+            }
+        }
+
+        return dimension;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/util/xml/AbstractXmlPathFinder.java b/services/src/main/java/org/jd/gui/util/xml/AbstractXmlPathFinder.java
new file mode 100644
index 0000000..7156410
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/util/xml/AbstractXmlPathFinder.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.util.xml;
+
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.io.StringReader;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+
+public abstract class AbstractXmlPathFinder {
+    protected HashMap<String, HashSet<String>> tagNameToPaths = new HashMap<>();
+    protected StringBuilder sb = new StringBuilder(200);
+
+    public AbstractXmlPathFinder(Collection<String> paths) {
+        for (String path : paths) {
+            if ((path != null) && (path.length() > 0)) {
+                // Normalize path
+                path = '/' + path;
+                int lastIndex = path.lastIndexOf('/');
+                String lastTagName = path.substring(lastIndex+1);
+
+                // Add tag names to map
+                HashSet<String> setOfPaths = tagNameToPaths.get(lastTagName);
+                if (setOfPaths == null) {
+                    tagNameToPaths.put(lastTagName, setOfPaths = new HashSet<>());
+                }
+                setOfPaths.add(path);
+            }
+        }
+    }
+
+    public void find(String text) {
+        sb.setLength(0);
+
+        try {
+            XMLInputFactory factory = XMLInputFactory.newInstance();
+            XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(text));
+
+            String tagName = "";
+            int offset = 0;
+
+            while (reader.hasNext()) {
+                reader.next();
+
+                switch (reader.getEventType())
+                {
+                case XMLStreamReader.START_ELEMENT:
+                    sb.append('/').append(tagName = reader.getLocalName());
+                    offset = reader.getLocation().getCharacterOffset();
+                    break;
+                case XMLStreamReader.END_ELEMENT:
+                    sb.setLength(sb.length() - reader.getLocalName().length() - 1);
+                    break;
+                case XMLStreamReader.CHARACTERS:
+                    HashSet<String> setOfPaths = tagNameToPaths.get(tagName);
+
+                    if (setOfPaths != null) {
+                        String path = sb.toString();
+
+                        if (setOfPaths.contains(path)) {
+                            // Search start offset
+                            while (offset > 0) {
+                                if (text.charAt(offset) == '>') {
+                                    break;
+                                } else {
+                                    offset--;
+                                }
+                            }
+
+                            handle(path.substring(1), reader.getText(), offset+1);
+                        }
+                    }
+                    break;
+                }
+            }
+        } catch (XMLStreamException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    public abstract void handle(String path, String text, int position);
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/AbstractTextPage.java b/services/src/main/java/org/jd/gui/view/component/AbstractTextPage.java
new file mode 100644
index 0000000..88cc749
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/AbstractTextPage.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.fife.ui.rsyntaxtextarea.*;
+import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
+import org.fife.ui.rtextarea.*;
+import org.jd.gui.api.feature.ContentSearchable;
+import org.jd.gui.api.feature.LineNumberNavigable;
+import org.jd.gui.api.feature.PreferencesChangeListener;
+import org.jd.gui.api.feature.UriOpenable;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import javax.swing.*;
+import javax.swing.text.BadLocationException;
+import java.awt.*;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseWheelListener;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.util.HashMap;
+import java.util.Map;
+
+public class AbstractTextPage extends JPanel implements LineNumberNavigable, ContentSearchable, UriOpenable, PreferencesChangeListener {
+    protected static final String FONT_SIZE_KEY = "ViewerPreferences.fontSize";
+
+    protected static final ImageIcon COLLAPSED_ICON = new ImageIcon(AbstractTextPage.class.getClassLoader().getResource("org/jd/gui/images/plus.png"));
+    protected static final ImageIcon EXPANDED_ICON = new ImageIcon(AbstractTextPage.class.getClassLoader().getResource("org/jd/gui/images/minus.png"));
+
+    protected static final Color DOUBLE_CLICK_HIGHLIGHT_COLOR = new Color(0x66ff66);
+    protected static final Color SEARCH_HIGHLIGHT_COLOR = new Color(0xffff66);
+    protected static final Color SELECT_HIGHLIGHT_COLOR = new Color(0xF49810);
+
+    protected static final RSyntaxTextAreaEditorKit.DecreaseFontSizeAction DECREASE_FONT_SIZE_ACTION = new RSyntaxTextAreaEditorKit.DecreaseFontSizeAction();
+    protected static final RSyntaxTextAreaEditorKit.IncreaseFontSizeAction INCREASE_FONT_SIZE_ACTION = new RSyntaxTextAreaEditorKit.IncreaseFontSizeAction();
+
+    protected RSyntaxTextArea textArea;
+    protected RTextScrollPane scrollPane;
+
+    protected Map<String, String> preferences;
+
+    public AbstractTextPage() {
+        super(new BorderLayout());
+
+        textArea = newSyntaxTextArea();
+        textArea.setSyntaxEditingStyle(getSyntaxStyle());
+        textArea.setCodeFoldingEnabled(true);
+        textArea.setAntiAliasingEnabled(true);
+        textArea.setCaretPosition(0);
+        textArea.setEditable(false);
+        textArea.setDropTarget(null);
+        textArea.setPopupMenu(null);
+        textArea.addMouseListener(new MouseAdapter() {
+            public void mouseClicked(MouseEvent e) {
+                if (e.getClickCount() == 2) {
+                    textArea.setMarkAllHighlightColor(DOUBLE_CLICK_HIGHLIGHT_COLOR);
+                    SearchEngine.markAll(textArea, newSearchContext(textArea.getSelectedText(), true, true, true, false));
+                }
+            }
+        });
+
+        KeyStroke ctrlA = KeyStroke.getKeyStroke(KeyEvent.VK_A, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
+        KeyStroke ctrlC = KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
+        KeyStroke ctrlV = KeyStroke.getKeyStroke(KeyEvent.VK_V, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
+        InputMap inputMap = textArea.getInputMap();
+        inputMap.put(ctrlA, "none");
+        inputMap.put(ctrlC, "none");
+        inputMap.put(ctrlV, "none");
+
+        try {
+            Theme theme = Theme.load(getClass().getClassLoader().getResourceAsStream("rsyntaxtextarea/themes/eclipse.xml"));
+            theme.apply(textArea);
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        scrollPane = new RTextScrollPane(textArea);
+        scrollPane.setFoldIndicatorEnabled(true);
+        scrollPane.setFont(textArea.getFont());
+
+        final MouseWheelListener[] mouseWheelListeners = scrollPane.getMouseWheelListeners();
+
+        // Remove default listeners
+        for (MouseWheelListener listener : mouseWheelListeners) {
+            scrollPane.removeMouseWheelListener(listener);
+        }
+
+        scrollPane.addMouseWheelListener(e -> {
+            if ((e.getModifiers() & (Event.META_MASK|Event.CTRL_MASK)) != 0) {
+                int x = e.getX() + scrollPane.getX() - textArea.getX();
+                int y = e.getY() + scrollPane.getY() - textArea.getY();
+                int offset = textArea.viewToModel(new Point(x, y));
+
+                // Update font size
+                if (e.getWheelRotation() > 0) {
+                    DECREASE_FONT_SIZE_ACTION.actionPerformedImpl(null, textArea);
+                } else {
+                    INCREASE_FONT_SIZE_ACTION.actionPerformedImpl(null, textArea);
+                }
+
+                // Save preferences
+                if (preferences != null) {
+                    preferences.put(FONT_SIZE_KEY, String.valueOf(textArea.getFont().getSize()));
+                }
+
+                try {
+                    Rectangle newRectangle = textArea.modelToView(offset);
+                    int newY = newRectangle.y + (newRectangle.height >> 1);
+
+                    // Scroll
+                    Point viewPosition = scrollPane.getViewport().getViewPosition();
+                    viewPosition.y = Math.max(viewPosition.y +newY - y, 0);
+                    scrollPane.getViewport().setViewPosition(viewPosition);
+                } catch (BadLocationException ee) {
+                    assert ExceptionUtil.printStackTrace(ee);
+                }
+            } else {
+                // Call default listeners
+                for (MouseWheelListener listener : mouseWheelListeners) {
+                    listener.mouseWheelMoved(e);
+                }
+            }
+        });
+
+        Gutter gutter = scrollPane.getGutter();
+        gutter.setFoldIcons(COLLAPSED_ICON, EXPANDED_ICON);
+        gutter.setFoldIndicatorForeground(gutter.getBorderColor());
+
+        add(scrollPane, BorderLayout.CENTER);
+        add(new RoundMarkErrorStrip(textArea), BorderLayout.LINE_END);
+    }
+
+    protected RSyntaxTextArea newSyntaxTextArea() { return new RSyntaxTextArea(); }
+
+    public String getText() { return textArea.getText(); }
+
+    public JScrollPane getScrollPane() {
+        return scrollPane;
+    }
+
+    public void setText(String text) {
+        textArea.setText(text);
+        textArea.setCaretPosition(0);
+    }
+
+    public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_NONE; }
+
+    /**
+     * @see org.fife.ui.rsyntaxtextarea.RSyntaxUtilities#selectAndPossiblyCenter
+     * Force center and do not select
+     */
+    public void setCaretPositionAndCenter(DocumentRange range) {
+        final int start = range.getStartOffset();
+        final int end = range.getEndOffset();
+        boolean foldsExpanded = false;
+        FoldManager fm = textArea.getFoldManager();
+
+        if (fm.isCodeFoldingSupportedAndEnabled()) {
+            foldsExpanded = fm.ensureOffsetNotInClosedFold(start);
+            foldsExpanded |= fm.ensureOffsetNotInClosedFold(end);
+        }
+
+        if (!foldsExpanded) {
+            try {
+                Rectangle rec = textArea.modelToView(start);
+
+                if (rec != null) {
+                    // Visible
+                    setCaretPositionAndCenter(start, end, rec);
+                } else {
+                    // Not visible yet
+                    SwingUtilities.invokeLater(() -> {
+                        try {
+                            Rectangle r = textArea.modelToView(start);
+                            if (r != null) {
+                                setCaretPositionAndCenter(start, end, r);
+                            }
+                        } catch (BadLocationException e) {
+                            assert ExceptionUtil.printStackTrace(e);
+                        }
+                    });
+                }
+            } catch (BadLocationException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+    }
+
+    protected void setCaretPositionAndCenter(int start, int end, Rectangle r) {
+        if (end != start) {
+            try {
+                r = r.union(textArea.modelToView(end));
+            } catch (BadLocationException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+
+        Rectangle visible = textArea.getVisibleRect();
+
+        // visible.x = r.x - (visible.width - r.width) / 2;
+        visible.y = r.y - (visible.height - r.height) / 2;
+
+        Rectangle bounds = textArea.getBounds();
+        Insets i = textArea.getInsets();
+        //bounds.x = i.left;
+        bounds.y = i.top;
+        //bounds.width -= i.left + i.right;
+        bounds.height -= i.top + i.bottom;
+
+        //if (visible.x < bounds.x) {
+        //    visible.x = bounds.x;
+        //}
+        //if (visible.x + visible.width > bounds.x + bounds.width) {
+        //    visible.x = bounds.x + bounds.width - visible.width;
+        //}
+        if (visible.y < bounds.y) {
+            visible.y = bounds.y;
+        }
+        if (visible.y + visible.height > bounds.y + bounds.height) {
+            visible.y = bounds.y + bounds.height - visible.height;
+        }
+
+        textArea.scrollRectToVisible(visible);
+        textArea.setCaretPosition(start);
+    }
+
+    // --- LineNumberNavigable --- //
+    public int getMaximumLineNumber() {
+        try {
+            return textArea.getLineOfOffset(textArea.getDocument().getLength()) + 1;
+        } catch (BadLocationException e) {
+            assert ExceptionUtil.printStackTrace(e);
+            return 0;
+        }
+    }
+
+    public void goToLineNumber(int lineNumber) {
+        try {
+            textArea.setCaretPosition(textArea.getLineStartOffset(lineNumber-1));
+        } catch (BadLocationException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+
+    public boolean checkLineNumber(int lineNumber) { return true; }
+
+    // --- ContentSearchable --- //
+    public boolean highlightText(String text, boolean caseSensitive) {
+        if (text.length() > 1) {
+            textArea.setMarkAllHighlightColor(SEARCH_HIGHLIGHT_COLOR);
+            textArea.setCaretPosition(textArea.getSelectionStart());
+
+            SearchContext context = newSearchContext(text, caseSensitive, false, true, false);
+            SearchResult result = SearchEngine.find(textArea, context);
+
+            if (!result.wasFound()) {
+                textArea.setCaretPosition(0);
+                result = SearchEngine.find(textArea, context);
+            }
+
+            return result.wasFound();
+        } else {
+            return true;
+        }
+    }
+
+    public void findNext(String text, boolean caseSensitive) {
+        if (text.length() > 1) {
+            textArea.setMarkAllHighlightColor(SEARCH_HIGHLIGHT_COLOR);
+
+            SearchContext context = newSearchContext(text, caseSensitive, false, true, false);
+            SearchResult result = SearchEngine.find(textArea, context);
+
+            if (!result.wasFound()) {
+                textArea.setCaretPosition(0);
+                SearchEngine.find(textArea, context);
+            }
+        }
+    }
+
+    public void findPrevious(String text, boolean caseSensitive) {
+        if (text.length() > 1) {
+            textArea.setMarkAllHighlightColor(SEARCH_HIGHLIGHT_COLOR);
+
+            SearchContext context = newSearchContext(text, caseSensitive, false, false, false);
+            SearchResult result = SearchEngine.find(textArea, context);
+
+            if (!result.wasFound()) {
+                textArea.setCaretPosition(textArea.getDocument().getLength());
+                SearchEngine.find(textArea, context);
+            }
+        }
+    }
+
+    protected SearchContext newSearchContext(String searchFor, boolean matchCase, boolean wholeWord, boolean searchForward, boolean regexp) {
+        SearchContext context = new SearchContext(searchFor, matchCase);
+        context.setMarkAll(true);
+        context.setWholeWord(wholeWord);
+        context.setSearchForward(searchForward);
+        context.setRegularExpression(regexp);
+        return context;
+    }
+
+    // --- UriOpenable --- //
+    public boolean openUri(URI uri) {
+        String query = uri.getQuery();
+
+        if (query != null) {
+            Map<String, String> parameters = parseQuery(query);
+
+            if (parameters.containsKey("lineNumber")) {
+                String lineNumber = parameters.get("lineNumber");
+
+                try {
+                    goToLineNumber(Integer.parseInt(lineNumber));
+                    return true;
+                } catch (NumberFormatException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            } else if (parameters.containsKey("position")) {
+                String position = parameters.get("position");
+
+                try {
+                    int pos = Integer.parseInt(position);
+                    if (textArea.getDocument().getLength() > pos) {
+                        setCaretPositionAndCenter(new DocumentRange(pos, pos));
+                        return true;
+                    }
+                } catch (NumberFormatException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            } else if (parameters.containsKey("highlightFlags")) {
+                String highlightFlags = parameters.get("highlightFlags");
+
+                if ((highlightFlags.indexOf('s') != -1) && parameters.containsKey("highlightPattern")) {
+                    textArea.setMarkAllHighlightColor(SELECT_HIGHLIGHT_COLOR);
+                    textArea.setCaretPosition(0);
+
+                    // Highlight all
+                    String searchFor = createRegExp(parameters.get("highlightPattern"));
+                    SearchContext context =  newSearchContext(searchFor, true, false, true, true);
+                    SearchResult result = SearchEngine.find(textArea, context);
+
+                    if (result.getMatchRange() != null) {
+                        textArea.setCaretPosition(result.getMatchRange().getStartOffset());
+                    }
+
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    protected Map<String, String> parseQuery(String query) {
+        HashMap<String, String> parameters = new HashMap<>();
+
+        // Parse parameters
+        try {
+            for (String param : query.split("&")) {
+                int index = param.indexOf('=');
+
+                if (index == -1) {
+                    parameters.put(URLDecoder.decode(param, "UTF-8"), "");
+                } else {
+                    String key = param.substring(0, index);
+                    String value = param.substring(index + 1);
+                    parameters.put(URLDecoder.decode(key, "UTF-8"), URLDecoder.decode(value, "UTF-8"));
+                }
+            }
+        } catch (UnsupportedEncodingException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        return parameters;
+    }
+
+    /**
+     * Create a simple regular expression
+     *
+     * Rules:
+     *  '*'        matchTypeEntries 0 ou N characters
+     *  '?'        matchTypeEntries 1 character
+     */
+    public static String createRegExp(String pattern) {
+        int patternLength = pattern.length();
+        StringBuilder sbPattern = new StringBuilder(patternLength * 2);
+
+        for (int i = 0; i < patternLength; i++) {
+            char c = pattern.charAt(i);
+
+            if (c == '*') {
+                sbPattern.append(".*");
+            } else if (c == '?') {
+                sbPattern.append('.');
+            } else if (c == '.') {
+                sbPattern.append("\\.");
+            } else {
+                sbPattern.append(c);
+            }
+        }
+
+        return sbPattern.toString();
+    }
+
+    // --- PreferencesChangeListener --- //
+    public void preferencesChanged(Map<String, String> preferences) {
+        String fontSize = preferences.get(FONT_SIZE_KEY);
+
+        if (fontSize != null) {
+            try {
+                textArea.setFont(textArea.getFont().deriveFont(Float.parseFloat(fontSize)));
+            } catch (Exception e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+
+        this.preferences = preferences;
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/ClassFilePage.java b/services/src/main/java/org/jd/gui/view/component/ClassFilePage.java
new file mode 100644
index 0000000..f12dafe
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/ClassFilePage.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.fife.ui.rsyntaxtextarea.DocumentRange;
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.core.v1.ClassFileToJavaSourceDecompiler;
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.util.decompiler.*;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.io.NewlineOutputStream;
+
+import javax.swing.text.BadLocationException;
+import javax.swing.text.DefaultCaret;
+import java.awt.*;
+import java.io.*;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ClassFilePage extends TypePage {
+    protected static final String ESCAPE_UNICODE_CHARACTERS   = "ClassFileDecompilerPreferences.escapeUnicodeCharacters";
+    protected static final String REALIGN_LINE_NUMBERS        = "ClassFileDecompilerPreferences.realignLineNumbers";
+    protected static final String WRITE_LINE_NUMBERS          = "ClassFileSaverPreferences.writeLineNumbers";
+    protected static final String WRITE_METADATA              = "ClassFileSaverPreferences.writeMetadata";
+    protected static final String JD_CORE_VERSION             = "JdGuiPreferences.jdCoreVersion";
+
+    protected static final ClassFileToJavaSourceDecompiler DECOMPILER = new ClassFileToJavaSourceDecompiler();
+
+    protected int maximumLineNumber = -1;
+
+    static {
+        // Early class loading
+        try {
+            String internalTypeName = ClassFilePage.class.getName().replace('.', '/');
+            DECOMPILER.decompile(new ClassPathLoader(), new NopPrinter(), internalTypeName);
+        } catch (Throwable t) {
+            assert ExceptionUtil.printStackTrace(t);
+        }
+    }
+
+    public ClassFilePage(API api, Container.Entry entry) {
+        super(api, entry);
+        Map<String, String> preferences = api.getPreferences();
+        // Init view
+        setErrorForeground(Color.decode(preferences.get("JdGuiPreferences.errorBackgroundColor")));
+        // Display source
+        decompile(preferences);
+    }
+
+    public void decompile(Map<String, String> preferences) {
+        try {
+            // Clear ...
+            clearHyperlinks();
+            clearLineNumbers();
+            declarations.clear();
+            typeDeclarations.clear();
+            strings.clear();
+
+            // Init preferences
+            boolean realignmentLineNumbers = getPreferenceValue(preferences, REALIGN_LINE_NUMBERS, false);
+            boolean unicodeEscape = getPreferenceValue(preferences, ESCAPE_UNICODE_CHARACTERS, false);
+
+            Map<String, Object> configuration = new HashMap<>();
+            configuration.put("realignLineNumbers", realignmentLineNumbers);
+
+            setShowMisalignment(realignmentLineNumbers);
+
+            // Init loader
+            ContainerLoader loader = new ContainerLoader(entry);
+
+            // Init printer
+            ClassFilePrinter printer = new ClassFilePrinter();
+            printer.setRealignmentLineNumber(realignmentLineNumbers);
+            printer.setUnicodeEscape(unicodeEscape);
+
+            // Format internal name
+            String entryPath = entry.getPath();
+            assert entryPath.endsWith(".class");
+            String entryInternalName = entryPath.substring(0, entryPath.length() - 6); // 6 = ".class".length()
+
+            // Decompile class file
+            DECOMPILER.decompile(loader, printer, entryInternalName, configuration);
+        } catch (Throwable t) {
+            assert ExceptionUtil.printStackTrace(t);
+            setText("// INTERNAL ERROR //");
+        }
+
+        maximumLineNumber = getMaximumSourceLineNumber();
+    }
+
+    protected static boolean getPreferenceValue(Map<String, String> preferences, String key, boolean defaultValue) {
+        String v = preferences.get(key);
+        return (v == null) ? defaultValue : Boolean.valueOf(v);
+    }
+
+    @Override
+    public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_JAVA; }
+
+    // --- ContentSavable --- //
+    @Override
+    public String getFileName() {
+        String path = entry.getPath();
+        int index = path.lastIndexOf('.');
+        return path.substring(0, index) + ".java";
+    }
+
+    @Override
+    public void save(API api, OutputStream os) {
+        try {
+            // Init preferences
+            Map<String, String> preferences = api.getPreferences();
+            boolean realignmentLineNumbers = getPreferenceValue(preferences, REALIGN_LINE_NUMBERS, false);
+            boolean unicodeEscape = getPreferenceValue(preferences, ESCAPE_UNICODE_CHARACTERS, false);
+            boolean showLineNumbers = getPreferenceValue(preferences, WRITE_LINE_NUMBERS, true);
+
+            Map<String, Object> configuration = new HashMap<>();
+            configuration.put("realignLineNumbers", realignmentLineNumbers);
+
+            // Init loader
+            ContainerLoader loader = new ContainerLoader(entry);
+
+            // Init printer
+            LineNumberStringBuilderPrinter printer = new LineNumberStringBuilderPrinter();
+            printer.setRealignmentLineNumber(realignmentLineNumbers);
+            printer.setUnicodeEscape(unicodeEscape);
+            printer.setShowLineNumbers(showLineNumbers);
+
+            // Format internal name
+            String entryPath = entry.getPath();
+            assert entryPath.endsWith(".class");
+            String entryInternalName = entryPath.substring(0, entryPath.length() - 6); // 6 = ".class".length()
+
+            // Decompile class file
+            DECOMPILER.decompile(loader, printer, entryInternalName, configuration);
+
+            StringBuilder stringBuffer = printer.getStringBuffer();
+
+            // Metadata
+            if (getPreferenceValue(preferences, WRITE_METADATA, true)) {
+                // Add location
+                String location =
+                        new File(entry.getUri()).getPath()
+                                // Escape "\ u" sequence to prevent "Invalid unicode" errors
+                                .replaceAll("(^|[^\\\\])\\\\u", "\\\\\\\\u");
+                stringBuffer.append("\n\n/* Location:              ");
+                stringBuffer.append(location);
+                // Add Java compiler version
+                int majorVersion = printer.getMajorVersion();
+
+                if (majorVersion >= 45) {
+                    stringBuffer.append("\n * Java compiler version: ");
+
+                    if (majorVersion >= 49) {
+                        stringBuffer.append(majorVersion - (49 - 5));
+                    } else {
+                        stringBuffer.append(majorVersion - (45 - 1));
+                    }
+
+                    stringBuffer.append(" (");
+                    stringBuffer.append(majorVersion);
+                    stringBuffer.append('.');
+                    stringBuffer.append(printer.getMinorVersion());
+                    stringBuffer.append(')');
+                }
+                // Add JD-Core version
+                stringBuffer.append("\n * JD-Core Version:       ");
+                stringBuffer.append(preferences.get(JD_CORE_VERSION));
+                stringBuffer.append("\n */");
+            }
+
+            try (PrintStream ps = new PrintStream(new NewlineOutputStream(os), true, "UTF-8")) {
+                ps.print(stringBuffer.toString());
+            } catch (IOException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        } catch (Throwable t) {
+            assert ExceptionUtil.printStackTrace(t);
+
+            try (OutputStreamWriter writer = new OutputStreamWriter(os, Charset.defaultCharset())) {
+                writer.write("// INTERNAL ERROR //");
+            } catch (IOException ee) {
+                assert ExceptionUtil.printStackTrace(ee);
+            }
+        }
+    }
+
+    // --- LineNumberNavigable --- //
+    @Override
+    public int getMaximumLineNumber() { return maximumLineNumber; }
+
+    @Override
+    public void goToLineNumber(int lineNumber) {
+        int textAreaLineNumber = getTextAreaLineNumber(lineNumber);
+        if (textAreaLineNumber > 0) {
+            try {
+                int start = textArea.getLineStartOffset(textAreaLineNumber - 1);
+                int end = textArea.getLineEndOffset(textAreaLineNumber - 1);
+                setCaretPositionAndCenter(new DocumentRange(start, end));
+            } catch (BadLocationException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+    }
+
+    @Override
+    public boolean checkLineNumber(int lineNumber) { return lineNumber <= maximumLineNumber; }
+
+    // --- PreferencesChangeListener --- //
+    @Override
+    public void preferencesChanged(Map<String, String> preferences) {
+        DefaultCaret caret = (DefaultCaret)textArea.getCaret();
+        int updatePolicy = caret.getUpdatePolicy();
+
+        caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
+        decompile(preferences);
+        caret.setUpdatePolicy(updatePolicy);
+
+        super.preferencesChanged(preferences);
+    }
+
+    public class ClassFilePrinter extends StringBuilderPrinter {
+        protected HashMap<String, ReferenceData> referencesCache = new HashMap<>();
+
+        // Manage line number and misalignment
+        int textAreaLineNumber = 1;
+
+        @Override
+        public void start(int maxLineNumber, int majorVersion, int minorVersion) {
+            super.start(maxLineNumber, majorVersion, minorVersion);
+
+            if (maxLineNumber == 0) {
+                scrollPane.setLineNumbersEnabled(false);
+            } else {
+                setMaxLineNumber(maxLineNumber);
+            }
+        }
+
+        @Override
+        public void end() {
+            setText(stringBuffer.toString());
+        }
+
+        // --- Add strings --- //
+        @Override
+        public void printStringConstant(String constant, String ownerInternalName) {
+            if (constant == null) constant = "null";
+            if (ownerInternalName == null) ownerInternalName = "null";
+
+            strings.add(new TypePage.StringData(stringBuffer.length(), constant.length(), constant, ownerInternalName));
+            super.printStringConstant(constant, ownerInternalName);
+        }
+
+        @Override
+        public void printDeclaration(int type, String internalTypeName, String name, String descriptor) {
+            if (internalTypeName == null) internalTypeName = "null";
+            if (name == null) name = "null";
+            if (descriptor == null) descriptor = "null";
+
+            switch (type) {
+                case TYPE:
+                    TypePage.DeclarationData data = new TypePage.DeclarationData(stringBuffer.length(), name.length(), internalTypeName, null, null);
+                    declarations.put(internalTypeName, data);
+                    typeDeclarations.put(stringBuffer.length(), data);
+                    break;
+                case CONSTRUCTOR:
+                    declarations.put(internalTypeName + "-<init>-" + descriptor, new TypePage.DeclarationData(stringBuffer.length(), name.length(), internalTypeName, "<init>", descriptor));
+                    break;
+                default:
+                    declarations.put(internalTypeName + '-' + name + '-' + descriptor, new TypePage.DeclarationData(stringBuffer.length(), name.length(), internalTypeName, name, descriptor));
+                    break;
+            }
+            super.printDeclaration(type, internalTypeName, name, descriptor);
+        }
+
+        @Override
+        public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) {
+            if (internalTypeName == null) internalTypeName = "null";
+            if (name == null) name = "null";
+            if (descriptor == null) descriptor = "null";
+
+            switch (type) {
+                case TYPE:
+                    addHyperlink(new TypePage.HyperlinkReferenceData(stringBuffer.length(), name.length(), newReferenceData(internalTypeName, null, null, ownerInternalName)));
+                    break;
+                case CONSTRUCTOR:
+                    addHyperlink(new TypePage.HyperlinkReferenceData(stringBuffer.length(), name.length(), newReferenceData(internalTypeName, "<init>", descriptor, ownerInternalName)));
+                    break;
+                default:
+                    addHyperlink(new TypePage.HyperlinkReferenceData(stringBuffer.length(), name.length(), newReferenceData(internalTypeName, name, descriptor, ownerInternalName)));
+                    break;
+            }
+            super.printReference(type, internalTypeName, name, descriptor, ownerInternalName);
+        }
+
+        @Override
+        public void startLine(int lineNumber) {
+            super.startLine(lineNumber);
+            setLineNumber(textAreaLineNumber, lineNumber);
+        }
+        @Override
+        public void endLine() {
+            super.endLine();
+            textAreaLineNumber++;
+        }
+        @Override
+        public void extraLine(int count) {
+            super.extraLine(count);
+            if (realignmentLineNumber) {
+                textAreaLineNumber += count;
+            }
+        }
+
+        // --- Add references --- //
+        public TypePage.ReferenceData newReferenceData(String internalName, String name, String descriptor, String scopeInternalName) {
+            String key = internalName + '-' + name + '-'+ descriptor + '-' + scopeInternalName;
+            ReferenceData reference = referencesCache.get(key);
+
+            if (reference == null) {
+                reference = new TypePage.ReferenceData(internalName, name, descriptor, scopeInternalName);
+                referencesCache.put(key, reference);
+                references.add(reference);
+            }
+
+            return reference;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/CustomLineNumbersPage.java b/services/src/main/java/org/jd/gui/view/component/CustomLineNumbersPage.java
new file mode 100644
index 0000000..c02b8e9
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/CustomLineNumbersPage.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
+import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit;
+import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaUI;
+import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities;
+import org.fife.ui.rsyntaxtextarea.folding.Fold;
+import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
+import org.fife.ui.rtextarea.Gutter;
+import org.fife.ui.rtextarea.LineNumberList;
+import org.fife.ui.rtextarea.RTextArea;
+import org.fife.ui.rtextarea.RTextAreaUI;
+
+import javax.swing.*;
+import javax.swing.text.EditorKit;
+import javax.swing.text.Element;
+import javax.swing.text.JTextComponent;
+import javax.swing.text.View;
+import java.awt.*;
+import java.util.Arrays;
+import java.util.Map;
+
+public abstract class CustomLineNumbersPage extends HyperlinkPage {
+    protected Color errorForeground = Color.RED;
+    protected boolean showMisalignment = true;
+
+    public void setErrorForeground(Color color) {
+        errorForeground = color;
+    }
+
+    public void setShowMisalignment(boolean b) {
+        showMisalignment = b;
+    }
+
+    /**
+     * Map[textarea line number] = original line number
+     */
+    protected int[] lineNumberMap = null;
+    protected int maxLineNumber = 0;
+
+    protected void setMaxLineNumber(int maxLineNumber) {
+        if (maxLineNumber > 0) {
+            if (lineNumberMap == null) {
+                lineNumberMap = new int[maxLineNumber+1];
+            } else if (lineNumberMap.length <= maxLineNumber) {
+                int[] tmp = new int[maxLineNumber+1];
+                System.arraycopy(lineNumberMap, 0, tmp, 0, lineNumberMap.length);
+                lineNumberMap = tmp;
+            }
+
+            this.maxLineNumber = maxLineNumber;
+        }
+    }
+
+    protected void initLineNumbers() {
+        String text = getText();
+        int len = text.length();
+
+        if (len == 0) {
+            setMaxLineNumber(0);
+        } else {
+            int mln = len - text.replace("\n", "").length();
+
+            if (text.charAt(len-1) != '\n') {
+                mln++;
+            }
+
+            setMaxLineNumber(mln);
+
+            for (int i=1; i<=maxLineNumber; i++) {
+                lineNumberMap[i] = i;
+            }
+        }
+    }
+
+    protected void setLineNumber(int textAreaLineNumber, int originalLineNumber) {
+        if (originalLineNumber > 0) {
+            setMaxLineNumber(textAreaLineNumber);
+            lineNumberMap[textAreaLineNumber] = originalLineNumber;
+        }
+    }
+
+    protected void clearLineNumbers() {
+        if (lineNumberMap != null) {
+            Arrays.fill(lineNumberMap, 0);
+        }
+    }
+
+    protected int getMaximumSourceLineNumber() { return maxLineNumber; }
+
+    protected int getTextAreaLineNumber(int originalLineNumber) {
+        int textAreaLineNumber = 1;
+        int greatestLowerSourceLineNumber = 0;
+        int i = lineNumberMap.length;
+
+        while (i-- > 0) {
+            int sln = lineNumberMap[i];
+            if (sln <= originalLineNumber) {
+                if (greatestLowerSourceLineNumber < sln) {
+                    greatestLowerSourceLineNumber = sln;
+                    textAreaLineNumber = i;
+                }
+            }
+        }
+
+        return textAreaLineNumber;
+    }
+
+    @Override protected RSyntaxTextArea newSyntaxTextArea() { return new SourceSyntaxTextArea(); }
+
+    public class SourceSyntaxTextArea extends HyperlinkSyntaxTextArea {
+        @Override protected RTextAreaUI createRTextAreaUI() { return new SourceSyntaxTextAreaUI(this); }
+    }
+
+    /**
+     * A lot of code to replace the default LineNumberList...
+     */
+    public class SourceSyntaxTextAreaUI extends RSyntaxTextAreaUI {
+        public SourceSyntaxTextAreaUI(JComponent rSyntaxTextArea) { super(rSyntaxTextArea); }
+        @Override public EditorKit getEditorKit(JTextComponent tc) { return new SourceSyntaxTextAreaEditorKit(); }
+        @Override public Rectangle getVisibleEditorRect() { return super.getVisibleEditorRect(); }
+    }
+
+    public class SourceSyntaxTextAreaEditorKit extends RSyntaxTextAreaEditorKit {
+        @Override public LineNumberList createLineNumberList(RTextArea textArea) { return new SourceLineNumberList(textArea); }
+    }
+
+    /**
+     * Why 'LineNumberList' is so unexpandable ? Too many private fields & methods and too many package scope.
+     */
+    public class SourceLineNumberList extends LineNumberList {
+        protected RTextArea rTextArea;
+        protected Map<?,?> aaHints;
+        protected Rectangle visibleRect;
+        protected Insets textAreaInsets;
+        protected Dimension preferredSize;
+
+        public SourceLineNumberList(RTextArea textArea) {
+            super(textArea, null);
+            this.rTextArea = textArea;
+        }
+
+        @Override
+        protected void init() {
+            super.init();
+            visibleRect = new Rectangle();
+            aaHints = RSyntaxUtilities.getDesktopAntiAliasHints();
+            textAreaInsets = null;
+        }
+
+        /**
+         * @see org.fife.ui.rtextarea.LineNumberList#paintComponent(java.awt.Graphics)
+         */
+        @Override
+        protected void paintComponent(Graphics g) {
+            visibleRect = g.getClipBounds(visibleRect);
+
+            if (visibleRect == null) {
+                visibleRect = getVisibleRect();
+            }
+            if (visibleRect == null) {
+                return;
+            }
+
+            int cellWidth = getPreferredSize().width;
+            int cellHeight = rTextArea.getLineHeight();
+            int ascent = rTextArea.getMaxAscent();
+            FoldManager fm = ((RSyntaxTextArea)rTextArea).getFoldManager();
+            int RHS_BORDER_WIDTH = getRhsBorderWidth();
+            FontMetrics metrics = g.getFontMetrics();
+            int rhs = getWidth() - RHS_BORDER_WIDTH;
+
+            if (getParent() instanceof Gutter) { // Should always be true
+                g.setColor(getParent().getBackground());
+            } else {
+                g.setColor(getBackground());
+            }
+
+            g.fillRect(0, visibleRect.y, cellWidth, visibleRect.height);
+            g.setFont(getFont());
+
+            if (aaHints != null) {
+                ((Graphics2D)g).addRenderingHints(aaHints);
+            }
+
+            if (rTextArea.getLineWrap()) {
+                SourceSyntaxTextAreaUI ui = (SourceSyntaxTextAreaUI)rTextArea.getUI();
+                View v = ui.getRootView(rTextArea).getView(0);
+                Element root = rTextArea.getDocument().getDefaultRootElement();
+                int lineCount = root.getElementCount();
+                int topPosition = rTextArea.viewToModel(visibleRect.getLocation());
+                int topLine = root.getElementIndex(topPosition);
+                Rectangle visibleEditorRect = ui.getVisibleEditorRect();
+                Rectangle r = LineNumberList.getChildViewBounds(v, topLine, visibleEditorRect);
+                int y = r.y;
+
+                int visibleBottom =  visibleRect.y + visibleRect.height;
+
+                // Keep painting lines until our y-coordinate is past the visible
+                // end of the text area.
+
+                while (y < visibleBottom) {
+                    r = getChildViewBounds(v, topLine, visibleEditorRect);
+
+                    // Paint the line number.
+                    paintLineNumber(g, metrics, rhs, y+ascent, topLine + 1);
+
+                    // The next possible y-coordinate is just after the last line
+                    // painted.
+                    y += r.height;
+
+                    // Update topLine (we're actually using it for our "current line"
+                    // variable now).
+                    if (fm != null) {
+                        Fold fold = fm.getFoldForLine(topLine);
+                        if ((fold != null) && fold.isCollapsed()) {
+                            topLine += fold.getCollapsedLineCount();
+                        }
+                    }
+
+                    if (++topLine >= lineCount) {
+                        break;
+                    }
+                }
+            } else {
+                textAreaInsets = rTextArea.getInsets(textAreaInsets);
+
+                if (visibleRect.y < textAreaInsets.top) {
+                    visibleRect.height -= (textAreaInsets.top - visibleRect.y);
+                    visibleRect.y = textAreaInsets.top;
+                }
+
+                int topLine = (visibleRect.y - textAreaInsets.top) / cellHeight;
+                int actualTopY = topLine * cellHeight + textAreaInsets.top;
+                int y = actualTopY + ascent;
+
+                // Get the actual first line to paint, taking into account folding.
+                topLine += fm.getHiddenLineCountAbove(topLine, true);
+
+                // Paint line numbers
+                g.setColor(getForeground());
+
+                int line = topLine + 1;
+
+                while ((y < visibleRect.y + visibleRect.height + ascent) && (line <= rTextArea.getLineCount())) {
+                    paintLineNumber(g, metrics, rhs, y, line);
+
+                    y += cellHeight;
+
+                    if (fm != null) {
+                        Fold fold = fm.getFoldForLine(line - 1);
+                        // Skip to next line to paint, taking extra care for lines with
+                        // block ends and begins together, e.g. "} else {"
+                        while ((fold != null) && fold.isCollapsed()) {
+                            int hiddenLineCount = fold.getLineCount();
+                            if (hiddenLineCount == 0) {
+                                // Fold parser identified a 0-line fold region... This
+                                // is really a bug, but we'll handle it gracefully.
+                                break;
+                            }
+                            line += hiddenLineCount;
+                            fold = fm.getFoldForLine(line - 1);
+                        }
+                    }
+
+                    line++;
+                }
+            }
+        }
+
+        protected void paintLineNumber(Graphics g, FontMetrics metrics, int x, int y, int lineNumber) {
+            int originalLineNumber;
+
+            if (lineNumberMap != null) {
+                originalLineNumber = (lineNumber < lineNumberMap.length) ? lineNumberMap[lineNumber] : 0;
+            } else {
+                originalLineNumber = lineNumber;
+            }
+
+            if (originalLineNumber != 0) {
+                String number = Integer.toString(originalLineNumber);
+                int strWidth = metrics.stringWidth(number);
+                g.setColor(showMisalignment && (lineNumber != originalLineNumber) ? errorForeground : getForeground());
+                g.drawString(number, x-strWidth, y);
+            }
+        }
+
+        public int getRhsBorderWidth() { return ((RSyntaxTextArea)rTextArea).isCodeFoldingEnabled() ? 0 : 4; }
+
+        @Override
+        public Dimension getPreferredSize() {
+            if (preferredSize == null) {
+                int lineCount = getMaximumSourceLineNumber();
+
+                if (lineCount > 0) {
+                    Font font = getFont();
+                    FontMetrics fontMetrics = getFontMetrics(font);
+                    int count = 1;
+
+                    while (lineCount >= 10) {
+                        lineCount = lineCount / 10;
+                        count++;
+                    }
+
+                    int preferredWidth = fontMetrics.charWidth('9') * count + 10;
+                    preferredSize = new Dimension(preferredWidth, 0);
+                } else {
+                    preferredSize = new Dimension(0, 0);
+                }
+            }
+
+            return preferredSize;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/DynamicPage.java b/services/src/main/java/org/jd/gui/view/component/DynamicPage.java
new file mode 100644
index 0000000..307bfe2
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/DynamicPage.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.*;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.Future;
+
+public class DynamicPage
+        extends JPanel
+        implements ContentCopyable, ContentSavable, ContentSearchable, ContentSelectable, FocusedTypeGettable,
+                   IndexesChangeListener, LineNumberNavigable, PreferencesChangeListener, UriGettable, UriOpenable,
+                   API.LoadSourceListener
+{
+    protected API api;
+    protected Container.Entry entry;
+    protected TypePage page;
+    protected URI lastOpenedUri;
+    protected Collection<Future<Indexes>> lastCollectionOfFutureIndexes;
+
+    public DynamicPage(API api, Container.Entry entry) {
+        super(new BorderLayout());
+        this.api = api;
+        this.entry = entry;
+
+        String source = api.getSource(entry);
+
+        if (source == null) {
+            // Display the decompiled source code
+            add(page = new ClassFilePage(api, entry));
+            // Try to load source in background
+            api.loadSource(entry, this);
+        } else {
+            // Display original source code
+            add(page = new JavaFilePage(api, new DelegatedEntry(entry, source)));
+        }
+    }
+
+    // --- ContentCopyable --- //
+    @Override public void copy() { page.copy(); }
+
+    // --- ContentSavable --- //
+    @Override public String getFileName() { return page.getFileName(); }
+    @Override public void save(API api, OutputStream outputStream) { page.save(api, outputStream); }
+
+    // --- ContentSearchable --- //
+    @Override public boolean highlightText(String text, boolean caseSensitive) { return page.highlightText(text, caseSensitive); }
+    @Override public void findNext(String text, boolean caseSensitive) { page.findNext(text, caseSensitive); }
+    @Override public void findPrevious(String text, boolean caseSensitive) { page.findPrevious(text, caseSensitive); }
+
+    // --- ContentSelectable --- //
+    @Override public void selectAll() { page.selectAll(); }
+
+    // --- FocusedTypeGettable --- //
+    @Override public String getFocusedTypeName() { return page.getFocusedTypeName(); }
+
+    // --- ContainerEntryGettable --- //
+    @Override public Container.Entry getEntry() { return entry; }
+
+    // --- IndexesChangeListener --- //
+    @Override public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        page.indexesChanged(lastCollectionOfFutureIndexes = collectionOfFutureIndexes);
+    }
+
+    // --- LineNumberNavigable --- //
+    @Override public int getMaximumLineNumber() { return page.getMaximumLineNumber(); }
+    @Override public void goToLineNumber(int lineNumber) { page.goToLineNumber(lineNumber); }
+    @Override public boolean checkLineNumber(int lineNumber) { return page.checkLineNumber(lineNumber); }
+
+    // --- PreferencesChangeListener --- //
+    @Override public void preferencesChanged(Map<String, String> preferences) { page.preferencesChanged(preferences); }
+
+    // --- UriGettable --- //
+    @Override public URI getUri() { return entry.getUri(); }
+
+    // --- UriOpenable --- //
+    @Override public boolean openUri(URI uri) { return page.openUri(lastOpenedUri = uri); }
+
+    // --- LoadSourceListener --- //
+    @Override public void sourceLoaded(String source) {
+        SwingUtilities.invokeLater(() -> {
+            // Replace the decompiled source code by the original
+            Point viewPosition = page.getScrollPane().getViewport().getViewPosition();
+
+            removeAll();
+            add(page = new JavaFilePage(api, new DelegatedEntry(entry, source)));
+            page.getScrollPane().getViewport().setViewPosition(viewPosition);
+
+            if (lastOpenedUri != null) {
+                page.openUri(lastOpenedUri);
+            }
+
+            if (lastCollectionOfFutureIndexes != null) {
+                page.indexesChanged(lastCollectionOfFutureIndexes);
+            }
+        });
+    }
+
+    protected static class DelegatedEntry implements Container.Entry {
+        protected Container.Entry entry;
+        protected String source;
+
+        DelegatedEntry(Container.Entry entry, String source) {
+            this.entry = entry;
+            this.source = source;
+        }
+
+        @Override public Container getContainer() { return entry.getContainer(); }
+        @Override public Container.Entry getParent() { return entry.getParent(); }
+        @Override public URI getUri() { return entry.getUri(); }
+        @Override public String getPath() { return entry.getPath(); }
+        @Override public boolean isDirectory() { return entry.isDirectory(); }
+        @Override public long length() { return entry.length(); }
+        @Override public InputStream getInputStream() { return new ByteArrayInputStream(source.getBytes()); }
+        @Override public Collection<Container.Entry> getChildren() { return entry.getChildren(); }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/EjbJarXmlFilePage.java b/services/src/main/java/org/jd/gui/view/component/EjbJarXmlFilePage.java
new file mode 100644
index 0000000..9024083
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/EjbJarXmlFilePage.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.IndexesChangeListener;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.index.IndexesUtil;
+import org.jd.gui.util.io.TextReader;
+import org.jd.gui.util.xml.AbstractXmlPathFinder;
+
+import java.awt.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.Future;
+
+public class EjbJarXmlFilePage extends TypeReferencePage implements UriGettable, IndexesChangeListener {
+    protected API api;
+    protected Container.Entry entry;
+    protected Collection<Future<Indexes>> collectionOfFutureIndexes = Collections.emptyList();
+
+    public EjbJarXmlFilePage(API api, Container.Entry entry) {
+        this.api = api;
+        this.entry = entry;
+        // Load content file
+        String text = TextReader.getText(entry.getInputStream());
+        // Create hyperlinks
+        new PathFinder().find(text);
+        // Display
+        setText(text);
+    }
+
+    public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_XML; }
+
+    protected boolean isHyperlinkEnabled(HyperlinkData hyperlinkData) { return ((TypeHyperlinkData)hyperlinkData).enabled; }
+
+    protected void openHyperlink(int x, int y, HyperlinkData hyperlinkData) {
+        TypeHyperlinkData data = (TypeHyperlinkData)hyperlinkData;
+
+        if (data.enabled) {
+            try {
+                // Save current position in history
+                Point location = textArea.getLocationOnScreen();
+                int offset = textArea.viewToModel(new Point(x - location.x, y - location.y));
+                URI uri = entry.getUri();
+                api.addURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), "position=" + offset, null));
+
+                // Open link
+                String internalTypeName = data.internalTypeName;
+                List<Container.Entry> entries = IndexesUtil.findInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+                String rootUri = entry.getContainer().getRoot().getUri().toString();
+                ArrayList<Container.Entry> sameContainerEntries = new ArrayList<>();
+
+                for (Container.Entry entry : entries) {
+                    if (entry.getUri().toString().startsWith(rootUri)) {
+                        sameContainerEntries.add(entry);
+                    }
+                }
+
+                if (sameContainerEntries.size() > 0) {
+                    api.openURI(x, y, sameContainerEntries, null, data.internalTypeName);
+                } else if (entries.size() > 0) {
+                    api.openURI(x, y, entries, null, data.internalTypeName);
+                }
+            } catch (URISyntaxException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+    }
+
+    // --- UriGettable --- //
+    public URI getUri() { return entry.getUri(); }
+
+    // --- ContentSavable --- //
+    public String getFileName() {
+        String path = entry.getPath();
+        int index = path.lastIndexOf('/');
+        return path.substring(index+1);
+    }
+
+    // --- IndexesChangeListener --- //
+    public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        // Update the list of containers
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        // Refresh links
+        boolean refresh = false;
+
+        for (Map.Entry<Integer, HyperlinkData> entry : hyperlinks.entrySet()) {
+            TypeHyperlinkData entryData = (TypeHyperlinkData)entry.getValue();
+            String internalTypeName = entryData.internalTypeName;
+            boolean enabled = IndexesUtil.containsInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+
+            if (entryData.enabled != enabled) {
+                entryData.enabled = enabled;
+                refresh = true;
+            }
+        }
+
+        if (refresh) {
+            textArea.repaint();
+        }
+    }
+
+    public static final List<String> typeHyperlinkPaths = Arrays.asList(
+        "ejb-jar/assembly-descriptor/application-exception/exception-class",
+        "ejb-jar/assembly-descriptor/interceptor-binding/interceptor-class",
+
+        "ejb-jar/enterprise-beans/entity/home",
+        "ejb-jar/enterprise-beans/entity/remote",
+        "ejb-jar/enterprise-beans/entity/ejb-class",
+        "ejb-jar/enterprise-beans/entity/prim-key-class",
+
+        "ejb-jar/enterprise-beans/message-driven/ejb-class",
+        "ejb-jar/enterprise-beans/message-driven/messaging-type",
+        "ejb-jar/enterprise-beans/message-driven/resource-ref/injection-target/injection-target-class",
+        "ejb-jar/enterprise-beans/message-driven/resource-env-ref/injection-target/injection-target-class",
+
+        "ejb-jar/enterprise-beans/session/home",
+        "ejb-jar/enterprise-beans/session/local",
+        "ejb-jar/enterprise-beans/session/remote",
+        "ejb-jar/enterprise-beans/session/business-local",
+        "ejb-jar/enterprise-beans/session/business-remote",
+        "ejb-jar/enterprise-beans/session/service-endpoint",
+        "ejb-jar/enterprise-beans/session/ejb-class",
+        "ejb-jar/enterprise-beans/session/ejb-ref/home",
+        "ejb-jar/enterprise-beans/session/ejb-ref/remote",
+
+        "ejb-jar/interceptors/interceptor/around-invoke/class",
+        "ejb-jar/interceptors/interceptor/ejb-ref/home",
+        "ejb-jar/interceptors/interceptor/ejb-ref/remote",
+        "ejb-jar/interceptors/interceptor/interceptor-class"
+    );
+
+    public class PathFinder extends AbstractXmlPathFinder {
+        public PathFinder() {
+            super(typeHyperlinkPaths);
+        }
+
+        public void handle(String path, String text, int position) {
+            String trim = text.trim();
+            if (trim != null) {
+                int startIndex = position + text.indexOf(trim);
+                int endIndex = startIndex + trim.length();
+                String internalTypeName = trim.replace(".", "/");
+                addHyperlink(new TypeHyperlinkData(startIndex, endIndex, internalTypeName));
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/HyperlinkPage.java b/services/src/main/java/org/jd/gui/view/component/HyperlinkPage.java
new file mode 100644
index 0000000..23aad10
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/HyperlinkPage.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
+import org.fife.ui.rsyntaxtextarea.Token;
+
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Map;
+import java.util.TreeMap;
+
+public abstract class HyperlinkPage extends TextPage {
+    protected static final Cursor DEFAULT_CURSOR = Cursor.getDefaultCursor();
+    protected static final Cursor HAND_CURSOR = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
+
+    protected TreeMap<Integer, HyperlinkData> hyperlinks = new TreeMap<>();
+
+    public HyperlinkPage() {
+        MouseAdapter listener = new MouseAdapter() {
+            int lastX = -1;
+            int lastY = -1;
+            int lastModifiers = -1;
+
+            public void mouseClicked(MouseEvent e) {
+                if ((e.getClickCount() == 1) && ((e.getModifiers() & (Event.ALT_MASK|Event.META_MASK|Event.SHIFT_MASK)) == 0)) {
+                    int offset = textArea.viewToModel(new Point(e.getX(), e.getY()));
+                    if (offset != -1) {
+                        Map.Entry<Integer, HyperlinkData> entry = hyperlinks.floorEntry(offset);
+                        if (entry != null) {
+                            HyperlinkData entryData = entry.getValue();
+                            if ((entryData != null) && (offset < entryData.endPosition) && (offset >= entryData.startPosition) && isHyperlinkEnabled(entryData)) {
+                                openHyperlink(e.getXOnScreen(), e.getYOnScreen(), entryData);
+                            }
+                        }
+                    }
+                }
+            }
+
+            public void mouseMoved(MouseEvent e) {
+                if ((e.getX() != lastX) || (e.getY() != lastY) || (lastModifiers != e.getModifiers())) {
+                    lastX = e.getX();
+                    lastY = e.getY();
+                    lastModifiers = e.getModifiers();
+
+                    if ((e.getModifiers() & (Event.ALT_MASK|Event.META_MASK|Event.SHIFT_MASK)) == 0) {
+                        int offset = textArea.viewToModel(new Point(e.getX(), e.getY()));
+                        if (offset != -1) {
+                            Map.Entry<Integer, HyperlinkData> entry = hyperlinks.floorEntry(offset);
+                            if (entry != null) {
+                                HyperlinkData entryData = entry.getValue();
+                                if ((entryData != null) && (offset < entryData.endPosition) && (offset >= entryData.startPosition) && isHyperlinkEnabled(entryData)) {
+                                    if (textArea.getCursor() != HAND_CURSOR) {
+                                        textArea.setCursor(HAND_CURSOR);
+                                    }
+                                    return;
+                                }
+                            }
+                        }
+                    }
+
+                    if (textArea.getCursor() != DEFAULT_CURSOR) {
+                        textArea.setCursor(DEFAULT_CURSOR);
+                    }
+                }
+            }
+        };
+
+        textArea.addMouseListener(listener);
+        textArea.addMouseMotionListener(listener);
+    }
+
+    protected RSyntaxTextArea newSyntaxTextArea() { return new HyperlinkSyntaxTextArea(); }
+
+    public void addHyperlink(HyperlinkData hyperlinkData) {
+        hyperlinks.put(hyperlinkData.startPosition, hyperlinkData);
+    }
+
+    public void clearHyperlinks() {
+        hyperlinks.clear();
+    }
+
+    protected abstract boolean isHyperlinkEnabled(HyperlinkData hyperlinkData);
+
+    protected abstract void openHyperlink(int x, int y, HyperlinkData hyperlinkData);
+
+    public static class HyperlinkData {
+        public int startPosition;
+        public int endPosition;
+
+        public HyperlinkData(int startPosition, int endPosition) {
+            this.startPosition = startPosition;
+            this.endPosition = endPosition;
+        }
+    }
+
+    public class HyperlinkSyntaxTextArea extends RSyntaxTextArea {
+        /**
+         * @see HyperlinkPage.HyperlinkSyntaxTextArea#getUnderlineForToken(org.fife.ui.rsyntaxtextarea.Token)
+         */
+        @Override
+        public boolean getUnderlineForToken(Token t) {
+            Map.Entry<Integer, HyperlinkData> entry = hyperlinks.floorEntry(t.getOffset());
+            if (entry != null) {
+                HyperlinkData entryData = entry.getValue();
+                if ((entryData != null) && (t.getOffset() < entryData.endPosition) && (t.getOffset() >= entryData.startPosition) && isHyperlinkEnabled(entryData)) {
+                    return true;
+                }
+            }
+            return super.getUnderlineForToken(t);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/JavaFilePage.java b/services/src/main/java/org/jd/gui/view/component/JavaFilePage.java
new file mode 100644
index 0000000..27551ff
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/JavaFilePage.java
@@ -0,0 +1,745 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.antlr.v4.runtime.ANTLRInputStream;
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.RuleContext;
+import org.antlr.v4.runtime.Token;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.TerminalNode;
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.util.io.TextReader;
+import org.jd.gui.util.parser.antlr.ANTLRJavaParser;
+import org.jd.gui.util.parser.antlr.AbstractJavaListener;
+import org.jd.gui.util.parser.antlr.JavaParser;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JavaFilePage extends TypePage {
+
+    public JavaFilePage(API api, Container.Entry entry) {
+        super(api, entry);
+        // Load content file
+        String text = TextReader.getText(entry.getInputStream()).replace("\r\n", "\n").replace('\r', '\n');
+        // Parse
+        DeclarationListener declarationListener = new DeclarationListener(entry);
+        ReferenceListener referenceListener = new ReferenceListener(entry);
+
+        ANTLRJavaParser.parse(new ANTLRInputStream(text), declarationListener);
+        referenceListener.init(declarationListener);
+        ANTLRJavaParser.parse(new ANTLRInputStream(text), referenceListener);
+        // Display
+        setText(text);
+        initLineNumbers();
+    }
+
+    public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_JAVA; }
+
+    // --- ContentSavable --- //
+    public String getFileName() {
+        String path = entry.getPath();
+        int index = path.lastIndexOf('/');
+        return path.substring(index+1);
+    }
+
+    public class DeclarationListener extends AbstractJavaListener {
+        protected StringBuilder sbTypeDeclaration = new StringBuilder();
+        protected String currentInternalTypeName;
+
+        public DeclarationListener(Container.Entry entry) { super(entry); }
+
+        public HashMap<String, String> getNameToInternalTypeName() { return super.nameToInternalTypeName; }
+
+        // --- Add declarations --- //
+        public void enterPackageDeclaration(JavaParser.PackageDeclarationContext ctx) {
+            super.enterPackageDeclaration(ctx);
+
+            if (! packageName.isEmpty()) {
+                sbTypeDeclaration.append(packageName).append('/');
+            }
+        }
+
+        public void enterImportDeclaration(JavaParser.ImportDeclarationContext ctx) {
+            List<TerminalNode> identifiers = ctx.qualifiedName().Identifier();
+            String internalTypeName = concatIdentifiers(identifiers);
+            String typeName = identifiers.get(identifiers.size()-1).getSymbol().getText();
+
+            nameToInternalTypeName.put(typeName, internalTypeName);
+        }
+
+        public void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterEnumDeclaration(JavaParser.EnumDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitEnumDeclaration(JavaParser.EnumDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterInterfaceDeclaration(JavaParser.InterfaceDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitInterfaceDeclaration(JavaParser.InterfaceDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterAnnotationTypeDeclaration(JavaParser.AnnotationTypeDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitAnnotationTypeDeclaration(JavaParser.AnnotationTypeDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterTypeDeclaration(ParserRuleContext ctx) {
+            // Type declaration
+            TerminalNode identifier = ctx.getToken(JavaParser.Identifier, 0);
+            String typeName = identifier.getText();
+            int position = identifier.getSymbol().getStartIndex();
+            int length = sbTypeDeclaration.length();
+
+            if ((length == 0) || (sbTypeDeclaration.charAt(length-1) == '/')) {
+                sbTypeDeclaration.append(typeName);
+            } else {
+                sbTypeDeclaration.append('$').append(typeName);
+            }
+
+            currentInternalTypeName = sbTypeDeclaration.toString();
+            nameToInternalTypeName.put(typeName, currentInternalTypeName);
+
+            // Super type reference
+            JavaParser.TypeContext superType = ctx.getRuleContext(JavaParser.TypeContext.class, 0);
+            String superInternalTypeName = (superType != null) ? resolveInternalTypeName(superType.classOrInterfaceType().Identifier()) : null;
+            TypeDeclarationData data = new TypeDeclarationData(position, typeName.length(), currentInternalTypeName, null, null, superInternalTypeName);
+
+            declarations.put(currentInternalTypeName, data);
+            typeDeclarations.put(position, data);
+        }
+
+        public void exitTypeDeclaration() {
+            int index = sbTypeDeclaration.lastIndexOf("$");
+
+            if (index == -1) {
+                index = sbTypeDeclaration.lastIndexOf("/") + 1;
+            }
+
+            if (index == -1) {
+                sbTypeDeclaration.setLength(0);
+            } else {
+                sbTypeDeclaration.setLength(index);
+            }
+
+            currentInternalTypeName = sbTypeDeclaration.toString();
+        }
+
+        public void enterClassBodyDeclaration(JavaParser.ClassBodyDeclarationContext ctx) {
+            if (ctx.getChildCount() == 2) {
+                ParseTree first = ctx.getChild(0);
+
+                if (first instanceof TerminalNode) {
+                    TerminalNode f = (TerminalNode)first;
+
+                    if (f.getSymbol().getType() == JavaParser.STATIC) {
+                        String name = f.getText();
+                        int position = f.getSymbol().getStartIndex();
+                        declarations.put(currentInternalTypeName + "-<clinit>-()V", new TypePage.DeclarationData(position, 6, currentInternalTypeName, name, "()V"));
+                    }
+                }
+            }
+        }
+
+        public void enterConstDeclaration(JavaParser.ConstDeclarationContext ctx) {
+            JavaParser.TypeContext typeContext = ctx.type();
+
+            for (JavaParser.ConstantDeclaratorContext constantDeclaratorContext : ctx.constantDeclarator()) {
+                TerminalNode identifier = constantDeclaratorContext.Identifier();
+                String name = identifier.getText();
+                int dimensionOnVariable = countDimension(constantDeclaratorContext.children);
+                String descriptor = createDescriptor(typeContext, dimensionOnVariable);
+                int position = identifier.getSymbol().getStartIndex();
+
+                declarations.put(currentInternalTypeName + '-' + name + '-' + descriptor, new TypePage.DeclarationData(position, name.length(), currentInternalTypeName, name, descriptor));
+            }
+        }
+
+        public void enterFieldDeclaration(JavaParser.FieldDeclarationContext ctx) {
+            JavaParser.TypeContext typeContext = ctx.type();
+
+            for (JavaParser.VariableDeclaratorContext declaration : ctx.variableDeclarators().variableDeclarator()) {
+                JavaParser.VariableDeclaratorIdContext variableDeclaratorId = declaration.variableDeclaratorId();
+                TerminalNode identifier = variableDeclaratorId.Identifier();
+                String name = identifier.getText();
+                int dimensionOnVariable = countDimension(variableDeclaratorId.children);
+                String descriptor = createDescriptor(typeContext, dimensionOnVariable);
+                int position = identifier.getSymbol().getStartIndex();
+                TypePage.DeclarationData data = new TypePage.DeclarationData(position, name.length(), currentInternalTypeName, name, descriptor);
+
+                declarations.put(currentInternalTypeName + '-' + name + '-' + descriptor, data);
+            }
+        }
+
+        public void enterMethodDeclaration(JavaParser.MethodDeclarationContext ctx) {
+            enterMethodDeclaration(ctx, ctx.Identifier(), ctx.formalParameters(), ctx.type());
+        }
+
+        public void enterInterfaceMethodDeclaration(JavaParser.InterfaceMethodDeclarationContext ctx) {
+            enterMethodDeclaration(ctx, ctx.Identifier(), ctx.formalParameters(), ctx.type());
+        }
+
+        public void enterMethodDeclaration(
+                ParserRuleContext ctx, TerminalNode identifier,
+                JavaParser.FormalParametersContext formalParameters, JavaParser.TypeContext returnType) {
+
+            String name = identifier.getText();
+            String paramDescriptors = createParamDescriptors(formalParameters.formalParameterList());
+            String returnDescriptor = createDescriptor(returnType, 0);
+            String descriptor = paramDescriptors + returnDescriptor;
+            int position = identifier.getSymbol().getStartIndex();
+
+            declarations.put(currentInternalTypeName + '-' + name + '-' + descriptor, new TypePage.DeclarationData(position, name.length(), currentInternalTypeName, name, descriptor));
+        }
+
+        public void enterConstructorDeclaration(JavaParser.ConstructorDeclarationContext ctx) {
+            TerminalNode identifier = ctx.Identifier();
+            String name = identifier.getText();
+            String paramDescriptors = createParamDescriptors(ctx.formalParameters().formalParameterList());
+            String descriptor = paramDescriptors + "V";
+            int position = identifier.getSymbol().getStartIndex();
+
+            declarations.put(currentInternalTypeName + "-<init>-" + descriptor, new TypePage.DeclarationData(position, name.length(), currentInternalTypeName, name, descriptor));
+        }
+
+        public String createParamDescriptors(JavaParser.FormalParameterListContext formalParameterList) {
+            StringBuilder paramDescriptors = null;
+
+            if (formalParameterList != null) {
+                List<JavaParser.FormalParameterContext> formalParameters = formalParameterList.formalParameter();
+                paramDescriptors = new StringBuilder("(");
+
+                for (JavaParser.FormalParameterContext formalParameter : formalParameters) {
+                    int dimensionOnParameter = countDimension(formalParameter.variableDeclaratorId().children);
+                    String descriptor = createDescriptor(formalParameter.type(), dimensionOnParameter);
+
+                    paramDescriptors.append(descriptor);
+                }
+            }
+
+            return (paramDescriptors == null) ? "()" : paramDescriptors.append(')').toString();
+        }
+    }
+
+    public class ReferenceListener extends AbstractJavaListener {
+        protected StringBuilder sbTypeDeclaration = new StringBuilder();
+        protected HashMap<String, TypePage.ReferenceData> referencesCache = new HashMap<>();
+        protected String currentInternalTypeName;
+        protected Context currentContext = null;
+
+        public ReferenceListener(Container.Entry entry) { super(entry); }
+
+        public void init(DeclarationListener declarationListener) {
+            this.nameToInternalTypeName.putAll(declarationListener.getNameToInternalTypeName());
+        }
+
+        // --- Add declarations --- //
+        public void enterPackageDeclaration(JavaParser.PackageDeclarationContext ctx) {
+            super.enterPackageDeclaration(ctx);
+
+            if (! packageName.isEmpty()) {
+                sbTypeDeclaration.append(packageName).append('/');
+            }
+        }
+
+        public void enterImportDeclaration(JavaParser.ImportDeclarationContext ctx) {
+            List<TerminalNode> identifiers = ctx.qualifiedName().Identifier();
+            int position = identifiers.get(0).getSymbol().getStartIndex();
+            String internalTypeName = concatIdentifiers(identifiers);
+
+            addHyperlink(new TypePage.HyperlinkReferenceData(position, internalTypeName.length(), newReferenceData(internalTypeName, null, null, null)));
+        }
+
+        public void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterEnumDeclaration(JavaParser.EnumDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitEnumDeclaration(JavaParser.EnumDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterInterfaceDeclaration(JavaParser.InterfaceDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitInterfaceDeclaration(JavaParser.InterfaceDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterAnnotationTypeDeclaration(JavaParser.AnnotationTypeDeclarationContext ctx) { enterTypeDeclaration(ctx); }
+        public void exitAnnotationTypeDeclaration(JavaParser.AnnotationTypeDeclarationContext ctx) { exitTypeDeclaration(); }
+
+        public void enterTypeDeclaration(ParserRuleContext ctx) {
+            // Type declaration
+            TerminalNode identifier = ctx.getToken(JavaParser.Identifier, 0);
+            String typeName = identifier.getText();
+            int length = sbTypeDeclaration.length();
+
+            if ((length == 0) || (sbTypeDeclaration.charAt(length-1) == '/')) {
+                sbTypeDeclaration.append(typeName);
+            } else {
+                sbTypeDeclaration.append('$').append(typeName);
+            }
+
+            currentInternalTypeName = sbTypeDeclaration.toString();
+            currentContext = new Context(currentContext);
+        }
+
+        public void exitTypeDeclaration() {
+            int index = sbTypeDeclaration.lastIndexOf("$");
+
+            if (index == -1) {
+                index = sbTypeDeclaration.lastIndexOf("/") + 1;
+            }
+
+            if (index == -1) {
+                sbTypeDeclaration.setLength(0);
+            } else {
+                sbTypeDeclaration.setLength(index);
+            }
+
+            currentInternalTypeName = sbTypeDeclaration.toString();
+        }
+
+        public void enterFormalParameters(JavaParser.FormalParametersContext ctx) {
+            JavaParser.FormalParameterListContext formalParameterList = ctx.formalParameterList();
+
+            if (formalParameterList != null) {
+                List<JavaParser.FormalParameterContext> formalParameters = formalParameterList.formalParameter();
+
+                for (JavaParser.FormalParameterContext formalParameter : formalParameters) {
+                    int dimensionOnParameter = countDimension(formalParameter.variableDeclaratorId().children);
+                    String descriptor = createDescriptor(formalParameter.type(), dimensionOnParameter);
+                    String name = formalParameter.variableDeclaratorId().Identifier().getSymbol().getText();
+
+                    currentContext.nameToDescriptor.put(name, descriptor);
+                }
+            }
+        }
+
+        // --- Add references --- //
+        public void enterType(JavaParser.TypeContext ctx) {
+            // Add type reference
+            JavaParser.ClassOrInterfaceTypeContext classOrInterfaceType = ctx.classOrInterfaceType();
+
+            if (classOrInterfaceType != null) {
+                List<TerminalNode> identifiers = classOrInterfaceType.Identifier();
+                String name = concatIdentifiers(identifiers);
+                String internalTypeName = resolveInternalTypeName(identifiers);
+                int position = identifiers.get(0).getSymbol().getStartIndex();
+
+                addHyperlink(new TypePage.HyperlinkReferenceData(position, name.length(), newReferenceData(internalTypeName, null, null, currentInternalTypeName)));
+            }
+        }
+
+        public void enterLocalVariableDeclaration(JavaParser.LocalVariableDeclarationContext ctx) {
+            JavaParser.TypeContext typeContext = ctx.type();
+
+            for (JavaParser.VariableDeclaratorContext variableDeclarator : ctx.variableDeclarators().variableDeclarator()) {
+                JavaParser.VariableDeclaratorIdContext variableDeclaratorId = variableDeclarator.variableDeclaratorId();
+                int dimensionOnVariable = countDimension(variableDeclaratorId.children);
+                String descriptor = createDescriptor(typeContext, dimensionOnVariable);
+                String name = variableDeclarator.variableDeclaratorId().Identifier().getSymbol().getText();
+
+                currentContext.nameToDescriptor.put(name, descriptor);
+            }
+        }
+
+        public void enterCreator(JavaParser.CreatorContext ctx) {
+            enterNewExpression(ctx.createdName().Identifier(), ctx.classCreatorRest());
+        }
+
+        public void enterInnerCreator(JavaParser.InnerCreatorContext ctx) {
+            enterNewExpression(Collections.singletonList(ctx.Identifier()), ctx.classCreatorRest());
+        }
+
+        public void enterNewExpression(List<TerminalNode> identifiers, JavaParser.ClassCreatorRestContext classCreatorRest) {
+            if (identifiers.size() > 0) {
+                String name = concatIdentifiers(identifiers);
+                String internalTypeName = resolveInternalTypeName(identifiers);
+                int position = identifiers.get(0).getSymbol().getStartIndex();
+
+                if (classCreatorRest != null) {
+                    // Constructor call -> Add a link to the constructor declaration
+                    JavaParser.ExpressionListContext expressionList = classCreatorRest.arguments().expressionList();
+                    String descriptor = (expressionList != null) ? getParametersDescriptor(expressionList).append('V').toString() : "()V";
+
+                    addHyperlink(new TypePage.HyperlinkReferenceData(position, name.length(), newReferenceData(internalTypeName, "<init>", descriptor, currentInternalTypeName)));
+                } else {
+                    // New type array -> Add a link to the type declaration
+                    addHyperlink(new TypePage.HyperlinkReferenceData(position, name.length(), newReferenceData(internalTypeName, null, null, currentInternalTypeName)));
+                }
+            }
+        }
+
+        public void enterExpression(JavaParser.ExpressionContext ctx) {
+            switch (ctx.getChildCount()) {
+                case 1:
+                    TerminalNode identifier0 = getToken(ctx.children, JavaParser.Identifier, 0);
+
+                    if (identifier0 != null) {
+                        if (isAField(ctx)) {
+                            JavaParser.PrimaryContext primaryContext = ctx.primary();
+
+                            if (primaryContext != null) {
+                                String fieldName = primaryContext.literal().StringLiteral().getText();
+
+                                if (currentContext.getDescriptor(fieldName) == null) {
+                                    // Not a local variable or a method parameter
+                                    String fieldTypeName = searchInternalTypeNameForThisFieldName(currentInternalTypeName, fieldName);
+                                    int position = ctx.Identifier().getSymbol().getStartIndex();
+
+                                    addHyperlink(new TypePage.HyperlinkReferenceData(position, fieldName.length(), newReferenceData(fieldTypeName, fieldName, "?", currentInternalTypeName)));
+                                }
+                            }
+                        }
+                    } else if (ctx.primary() != null) {
+                        TerminalNode identifier = ctx.primary().Identifier();
+
+                        if (identifier != null) {
+                            Token symbol = identifier.getSymbol();
+                            String name = symbol.getText();
+                            String internalTypeName = nameToInternalTypeName.get(name);
+
+                            if (internalTypeName != null) {
+                                int position = symbol.getStartIndex();
+
+                                addHyperlink(new TypePage.HyperlinkReferenceData(position, name.length(), newReferenceData(internalTypeName, null, null, currentInternalTypeName)));
+                            }
+                        }
+                    }
+                    break;
+                case 3:
+                    if (getToken(ctx.children, JavaParser.DOT, 1) != null) {
+                        // Search "expression '.' Identifier" : field reference
+                        TerminalNode identifier3 = getToken(ctx.children, JavaParser.Identifier, 2);
+
+                        if ((identifier3 != null) && isAField(ctx)) {
+                            String fieldTypeName = getInternalTypeName(ctx.getChild(0));
+
+                            if (fieldTypeName != null) {
+                                int position = identifier3.getSymbol().getStartIndex();
+                                String fieldName = identifier3.getText();
+
+                                addHyperlink(new TypePage.HyperlinkReferenceData(position, fieldName.length(), newReferenceData(fieldTypeName, fieldName, "?", currentInternalTypeName)));
+                            }
+                        }
+                    } else if (getToken(ctx.children, JavaParser.LPAREN, 1) != null) {
+                        // Search "expression '(' ')'" : method reference
+                        if (getToken(ctx.children, JavaParser.RPAREN, 2) != null) {
+                            enterCallMethodExpression(ctx, null);
+                        }
+                    }
+                    break;
+                case 4:
+                    if (getToken(ctx.children, JavaParser.LPAREN, 1) != null) {
+                        // Search "expression '(' expressionList ')'" : method reference
+                        if (getToken(ctx.children, JavaParser.RPAREN, 3) != null) {
+                            JavaParser.ExpressionListContext expressionListContext = ctx.expressionList();
+
+                            if ((expressionListContext != null) && (expressionListContext == ctx.children.get(2))) {
+                                enterCallMethodExpression(ctx, expressionListContext);
+                            }
+                        }
+                    }
+                    break;
+            }
+        }
+
+        public void enterCallMethodExpression(JavaParser.ExpressionContext ctx, JavaParser.ExpressionListContext expressionListContext) {
+            ParseTree first = ctx.children.get(0);
+
+            if (first instanceof JavaParser.ExpressionContext) {
+                JavaParser.ExpressionContext f = (JavaParser.ExpressionContext)first;
+
+                switch (f.getChildCount()) {
+                    case 1:
+                        JavaParser.PrimaryContext primary = f.primary();
+                        TerminalNode identifier = primary.Identifier();
+
+                        if (identifier != null) {
+                            Token symbol = identifier.getSymbol();
+
+                            if (symbol != null) {
+                                String methodName = symbol.getText();
+                                String methodTypeName = searchInternalTypeNameForThisMethodName(currentInternalTypeName, methodName);
+
+                                if (methodTypeName != null) {
+                                    int position = symbol.getStartIndex();
+                                    String methodDescriptor = (expressionListContext != null) ? getParametersDescriptor(expressionListContext).append('?').toString() : "()?";
+
+                                    addHyperlink(new TypePage.HyperlinkReferenceData(position, methodName.length(), newReferenceData(methodTypeName, methodName, methodDescriptor, currentInternalTypeName)));
+                                }
+                            }
+                        } else {
+                            Token symbol = primary.getChild(TerminalNode.class, 0).getSymbol();
+
+                            if (symbol != null) {
+                                switch (symbol.getType()) {
+                                    case JavaParser.THIS:
+                                        int position = symbol.getStartIndex();
+                                        String methodDescriptor = (expressionListContext != null) ? getParametersDescriptor(expressionListContext).append('?').toString() : "()?";
+
+                                        addHyperlink(new TypePage.HyperlinkReferenceData(position, 4, newReferenceData(currentInternalTypeName, "<init>", methodDescriptor, currentInternalTypeName)));
+                                        break;
+                                    case JavaParser.SUPER:
+                                        DeclarationData data = declarations.get(currentInternalTypeName);
+
+                                        if (data instanceof TypeDeclarationData) {
+                                            position = symbol.getStartIndex();
+                                            String methodTypeName = ((TypeDeclarationData) data).superTypeName;
+                                            methodDescriptor = (expressionListContext != null) ? getParametersDescriptor(expressionListContext).append('?').toString() : "()?";
+
+                                            addHyperlink(new TypePage.HyperlinkReferenceData(position, 5, newReferenceData(methodTypeName, "<init>", methodDescriptor, currentInternalTypeName)));
+                                        }
+                                        break;
+                                }
+                            }
+                        }
+                        break;
+                    case 3:
+                        // Search "expression '.' Identifier"
+                        ParseTree dot = first.getChild(1);
+
+                        if ((dot instanceof TerminalNode) && (((TerminalNode)dot).getSymbol().getType() == JavaParser.DOT)) {
+                            ParseTree identifier3 = first.getChild(2);
+
+                            if (identifier3 instanceof TerminalNode) {
+                                TerminalNode i3 = (TerminalNode)identifier3;
+
+                                if (i3.getSymbol().getType() == JavaParser.Identifier) {
+                                    String methodTypeName = getInternalTypeName(first.getChild(0));
+
+                                    if (methodTypeName != null) {
+                                        int position = i3.getSymbol().getStartIndex();
+                                        String methodName = i3.getText();
+                                        String methodDescriptor = (expressionListContext != null) ? getParametersDescriptor(expressionListContext).append('?').toString() : "()?";
+
+                                        addHyperlink(new TypePage.HyperlinkReferenceData(position, methodName.length(), newReferenceData(methodTypeName, methodName, methodDescriptor, currentInternalTypeName)));
+                                    }
+                                }
+                            }
+                        }
+                        break;
+                }
+            }
+        }
+
+        public StringBuilder getParametersDescriptor(JavaParser.ExpressionListContext expressionListContext) {
+            StringBuilder sb = new StringBuilder('(');
+            for (JavaParser.ExpressionContext exp : expressionListContext.expression()) sb.append('?');
+            sb.append(')');
+            return sb;
+        }
+
+        public boolean isAField(JavaParser.ExpressionContext ctx) {
+            RuleContext parent = ctx.parent;
+
+            if (parent instanceof JavaParser.ExpressionContext) {
+                int size = parent.getChildCount();
+
+                if (parent.getChild(size - 1) != ctx) {
+                    for (int i=0; i<size; i++) {
+                        if (parent.getChild(i) == ctx) {
+                            ParseTree next = parent.getChild(i+1);
+
+                            if (next instanceof TerminalNode) {
+                                switch (((TerminalNode)next).getSymbol().getType()) {
+                                    case JavaParser.DOT:
+                                    case JavaParser.LPAREN:
+                                        return false;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            return true;
+        }
+
+        public String getInternalTypeName(ParseTree pt) {
+            if (pt instanceof JavaParser.ExpressionContext) {
+
+                if (pt.getChildCount() == 1) {
+                    JavaParser.PrimaryContext primary = ((JavaParser.ExpressionContext)pt).primary();
+                    TerminalNode identifier = primary.Identifier();
+
+                    if (identifier != null) {
+                        String name = identifier.getSymbol().getText();
+                        String descriptor = (currentContext == null) ? null : currentContext.getDescriptor(name);
+
+                        if (descriptor != null) {
+                            // Is a local variable or a method parameter
+                            if (descriptor.charAt(0) == 'L') {
+                                return descriptor.substring(1, descriptor.length() - 1);
+                            }
+                        } else if (currentInternalTypeName != null) {
+                            String internalTypeName = searchInternalTypeNameForThisFieldName(currentInternalTypeName, name);
+
+                            if (internalTypeName != null) {
+                                // Is a field
+                                return internalTypeName;
+                            } else {
+                                internalTypeName = resolveInternalTypeName(Collections.singletonList(identifier));
+
+                                if (internalTypeName != null) {
+                                    // Is a type
+                                    return internalTypeName;
+                                } else {
+                                    // Not found
+                                    return null;
+                                }
+                            }
+                        }
+                    } else {
+                        TerminalNode tn = primary.getChild(TerminalNode.class, 0);
+                        Token symbol = (tn == null) ? null : tn.getSymbol();
+
+                        if (symbol != null) {
+                            switch (symbol.getType()) {
+                                case JavaParser.THIS:
+                                    return currentInternalTypeName;
+                                case JavaParser.SUPER:
+                                    DeclarationData data = declarations.get(currentInternalTypeName);
+
+                                    if (data instanceof TypeDeclarationData) {
+                                        return ((TypeDeclarationData)data).superTypeName;
+                                    } else {
+                                        return null;
+                                    }
+                            }
+                        }
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        public String searchInternalTypeNameForThisFieldName(String internalTypeName, String name) {
+            String prefix = internalTypeName + '-' + name + '-';
+            int length = prefix.length();
+
+            for (Map.Entry<String, DeclarationData> entry : declarations.entrySet()) {
+                if (entry.getKey().startsWith(prefix) && (entry.getKey().charAt(length) != '(')) {
+                    return entry.getValue().typeName;
+                }
+            }
+
+            // Not found
+            int index = internalTypeName.lastIndexOf('$');
+
+            if (index != -1) {
+                // Search in the outer type
+                internalTypeName = internalTypeName.substring(0, index);
+
+                return searchInternalTypeNameForThisFieldName(internalTypeName, name);
+            }
+
+            // Not found
+            return null;
+        }
+
+        public String searchInternalTypeNameForThisMethodName(String internalTypeName, String name) {
+            String prefix = internalTypeName + '-' + name + "-(";
+
+            for (Map.Entry<String, DeclarationData> entry : declarations.entrySet()) {
+                if (entry.getKey().startsWith(prefix)) {
+                    return entry.getValue().typeName;
+                }
+            }
+
+            // Not found
+            int index = internalTypeName.lastIndexOf('$');
+
+            if (index != -1) {
+                // Search in the outer type
+                internalTypeName = internalTypeName.substring(0, index);
+
+                return searchInternalTypeNameForThisMethodName(internalTypeName, name);
+            }
+
+            // Not found
+            return null;
+        }
+
+        public TerminalNode getToken(List<ParseTree> children, int type, int i) {
+            ParseTree pt = children.get(i);
+
+            if (pt instanceof TerminalNode) {
+                if (((TerminalNode)pt).getSymbol().getType() == type) {
+                    return (TerminalNode)pt;
+                }
+            }
+
+            return null;
+        }
+
+        public void enterBlock(JavaParser.BlockContext ctx) {
+            currentContext = new Context(currentContext);
+        }
+
+        public void exitBlock(JavaParser.BlockContext ctx) {
+            currentContext = currentContext.outerContext;
+        }
+
+        public TypePage.ReferenceData newReferenceData(String internalName, String name, String descriptor, String scopeInternalName) {
+            String key = internalName + '-' + name + '-'+ descriptor + '-' + scopeInternalName;
+            TypePage.ReferenceData reference = referencesCache.get(key);
+
+            if (reference == null) {
+                reference = new TypePage.ReferenceData(internalName, name, descriptor, scopeInternalName);
+                referencesCache.put(key, reference);
+                references.add(reference);
+            }
+
+            return reference;
+        }
+
+        // --- Add strings --- //
+        public void enterLiteral(JavaParser.LiteralContext ctx) {
+            TerminalNode stringLiteral = ctx.StringLiteral();
+
+            if (stringLiteral != null) {
+                String str = stringLiteral.getSymbol().getText();
+                int position = stringLiteral.getSymbol().getStartIndex();
+
+                strings.add(new TypePage.StringData(position, str.length(), str, currentInternalTypeName));
+            }
+        }
+    }
+
+    public static class Context {
+        protected Context outerContext;
+
+        protected HashMap<String, String> nameToDescriptor = new HashMap<>();
+
+        public Context(Context outerContext) {
+            this.outerContext = outerContext;
+        }
+
+        /**
+         * @param name Parameter or variable name
+         * @return Qualified type name
+         */
+        public String getDescriptor(String name) {
+            String descriptor = nameToDescriptor.get(name);
+
+            if ((descriptor == null) && (outerContext != null)) {
+                descriptor = outerContext.getDescriptor(name);
+            }
+
+            return descriptor;
+        }
+    }
+
+    public static class TypeDeclarationData extends TypePage.DeclarationData {
+        protected String superTypeName;
+
+        public TypeDeclarationData(int startPosition, int length, String type, String name, String descriptor, String superTypeName) {
+            super(startPosition, length, type, name, descriptor);
+
+            this.superTypeName = superTypeName;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/LogPage.java b/services/src/main/java/org/jd/gui/view/component/LogPage.java
new file mode 100644
index 0000000..51cf0b5
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/LogPage.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.IndexesChangeListener;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.index.IndexesUtil;
+
+import java.awt.*;
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Future;
+
+public class LogPage extends HyperlinkPage implements UriGettable, IndexesChangeListener {
+    protected API api;
+    protected URI uri;
+    protected Collection<Future<Indexes>> collectionOfFutureIndexes = Collections.emptyList();
+
+    public LogPage(API api, URI uri, String content) {
+        this.api = api;
+        this.uri = uri;
+        // Parse
+        int index = 0;
+        int eol = content.indexOf('\n');
+
+        while (eol != -1) {
+            parseLine(content, index, eol);
+            index = eol + 1;
+            eol = content.indexOf('\n', index);
+        }
+
+        parseLine(content, index, content.length());
+        // Display
+        setText(content);
+    }
+
+    protected void parseLine(String content, int index, int eol) {
+        int start = content.indexOf("at ", index);
+
+        if ((start != -1) && (start < eol)) {
+            int leftParenthesisIndex = content.indexOf('(', start);
+
+            if ((leftParenthesisIndex != -1) && (leftParenthesisIndex < eol)) {
+                addHyperlink(new LogHyperlinkData(start+3, leftParenthesisIndex));
+            }
+        }
+    }
+
+    protected boolean isHyperlinkEnabled(HyperlinkData hyperlinkData) { return ((LogHyperlinkData)hyperlinkData).enabled; }
+
+    protected void openHyperlink(int x, int y, HyperlinkData hyperlinkData) {
+        LogHyperlinkData logHyperlinkData = (LogHyperlinkData)hyperlinkData;
+
+        if (logHyperlinkData.enabled) {
+            try {
+                // Save current position in history
+                Point location = textArea.getLocationOnScreen();
+                int offset = textArea.viewToModel(new Point(x - location.x, y - location.y));
+                api.addURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), "position=" + offset, null));
+
+                // Open link
+                String text = getText();
+                String typeAndMethodNames = text.substring(hyperlinkData.startPosition, hyperlinkData.endPosition);
+                int lastDotIndex = typeAndMethodNames.lastIndexOf('.');
+                String methodName = typeAndMethodNames.substring(lastDotIndex + 1);
+                String internalTypeName = typeAndMethodNames.substring(0, lastDotIndex).replace('.', '/');
+                List<Container.Entry> entries = IndexesUtil.findInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+                int leftParenthesisIndex = hyperlinkData.endPosition + 1;
+                int rightParenthesisIndex = text.indexOf(')', leftParenthesisIndex);
+                String lineNumberOrNativeMethodFlag = text.substring(leftParenthesisIndex, rightParenthesisIndex);
+
+                if (lineNumberOrNativeMethodFlag.equals("Native Method")) {
+                    // Example: at java.security.AccessController.doPrivileged(Native Method)
+                    lastDotIndex = internalTypeName.lastIndexOf('/');
+                    String shortTypeName = internalTypeName.substring(lastDotIndex + 1);
+                    api.openURI(x, y, entries, null, shortTypeName + '-' + methodName + "-(*)?");
+                } else {
+                    // Example: at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:294)
+                    int colonIndex = lineNumberOrNativeMethodFlag.indexOf(':');
+                    String lineNumber = lineNumberOrNativeMethodFlag.substring(colonIndex + 1);
+                    api.openURI(x, y, entries, "lineNumber=" + lineNumber, null);
+                }
+            } catch (Exception e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+    }
+
+    // --- UriGettable --- //
+    public URI getUri() { return uri; }
+
+    // --- ContentSavable --- //
+    public String getFileName() {
+        String path = uri.getPath();
+        int index = path.lastIndexOf('/');
+        return path.substring(index + 1);
+    }
+
+    // --- IndexesChangeListener --- //
+    public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        // Update the list of containers
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        // Refresh links
+        boolean refresh = false;
+        String text = getText();
+
+        for (Map.Entry<Integer, HyperlinkData> entry : hyperlinks.entrySet()) {
+            LogHyperlinkData entryData = (LogHyperlinkData)entry.getValue();
+            String typeAndMethodNames = text.substring(entryData.startPosition, entryData.endPosition);
+            int lastDotIndex = typeAndMethodNames.lastIndexOf('.');
+            String internalTypeName = typeAndMethodNames.substring(0, lastDotIndex).replace('.', '/');
+            boolean enabled = IndexesUtil.containsInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+
+            if (entryData.enabled != enabled) {
+                entryData.enabled = enabled;
+                refresh = true;
+            }
+        }
+
+        if (refresh) {
+            textArea.repaint();
+        }
+    }
+
+    public static class LogHyperlinkData extends HyperlinkData {
+        public boolean enabled = false;
+
+        public LogHyperlinkData(int startPosition, int endPosition) {
+            super(startPosition, endPosition);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/ManifestFilePage.java b/services/src/main/java/org/jd/gui/view/component/ManifestFilePage.java
new file mode 100644
index 0000000..c9c00c1
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/ManifestFilePage.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.IndexesChangeListener;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.index.IndexesUtil;
+import org.jd.gui.util.io.TextReader;
+
+import java.awt.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.Future;
+
+public class ManifestFilePage extends HyperlinkPage implements UriGettable, IndexesChangeListener {
+    protected API api;
+    protected Container.Entry entry;
+    protected Collection<Future<Indexes>> collectionOfFutureIndexes = Collections.emptyList();
+
+    public ManifestFilePage(API api, Container.Entry entry) {
+        this.api = api;
+        this.entry = entry;
+        // Load content file
+        String text = TextReader.getText(entry.getInputStream());
+        // Parse hyperlinks. Docs:
+        // - http://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
+        // - http://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html
+        int startLineIndex = text.indexOf("Main-Class:");
+        if (startLineIndex != -1) {
+            // Example: Main-Class: jd.gui.App
+            int startIndex = skipSeparators(text, startLineIndex + "Main-Class:".length());
+            int endIndex = searchEndIndexOfValue(text, startLineIndex, startIndex);
+            String typeName = text.substring(startIndex, endIndex);
+            String internalTypeName = typeName.replace('.', '/');
+            addHyperlink(new ManifestHyperlinkData(startIndex, endIndex, internalTypeName + "-main-([Ljava/lang/String;)V"));
+        }
+
+        startLineIndex = text.indexOf("Premain-Class:");
+        if (startLineIndex != -1) {
+            // Example: Premain-Class: packge.JavaAgent
+            int startIndex = skipSeparators(text, startLineIndex + "Premain-Class:".length());
+            int endIndex = searchEndIndexOfValue(text, startLineIndex, startIndex);
+            String typeName = text.substring(startIndex, endIndex);
+            String internalTypeName = typeName.replace('.', '/');
+            // Undefined parameters : 2 candidate methods
+            // http://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html
+            addHyperlink(new ManifestHyperlinkData(startIndex, endIndex, internalTypeName + "-premain-(*)?"));
+        }
+        // Display
+        setText(text);
+    }
+
+    public int skipSeparators(String text, int index) {
+        int length = text.length();
+
+        while (index < length) {
+            switch (text.charAt(index)) {
+                case ' ': case '\t': case '\n': case '\r':
+                    index++;
+                    break;
+                default:
+                    return index;
+            }
+        }
+
+        return index;
+    }
+
+    public int searchEndIndexOfValue(String text, int startLineIndex, int startIndex) {
+        int length = text.length();
+        int index = startIndex;
+
+        while (index < length) {
+            // MANIFEST.MF Specification: max line length = 72
+            // http://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
+            switch (text.charAt(index)) {
+                case '\r':
+                    // CR followed by LF ?
+                    if ((index-startLineIndex >= 70) && (index+1 < length) && (text.charAt(index+1) == ' ')) {
+                        // Multiline value
+                        startLineIndex = index+1;
+                    } else if ((index-startLineIndex >= 70) && (index+2 < length) && (text.charAt(index+1) == '\n') && (text.charAt(index+2) == ' ')) {
+                        // Multiline value
+                        index++;
+                        startLineIndex = index+1;
+                    } else {
+                        // (End of file) or (single line value) => return end index
+                        return index;
+                    }
+                    break;
+                case '\n':
+                    if ((index-startLineIndex >= 70) && (index+1 < length) && (text.charAt(index+1) == ' ')) {
+                        // Multiline value
+                        startLineIndex = index+1;
+                    } else {
+                        // (End of file) or (single line value) => return end index
+                        return index;
+                    }
+                    break;
+            }
+            index++;
+        }
+
+        return index;
+    }
+
+    protected boolean isHyperlinkEnabled(HyperlinkData hyperlinkData) { return ((ManifestHyperlinkData)hyperlinkData).enabled; }
+
+    protected void openHyperlink(int x, int y, HyperlinkData hyperlinkData) {
+        ManifestHyperlinkData data = (ManifestHyperlinkData)hyperlinkData;
+
+        if (data.enabled) {
+            try {
+                // Save current position in history
+                Point location = textArea.getLocationOnScreen();
+                int offset = textArea.viewToModel(new Point(x-location.x, y-location.y));
+                URI uri = entry.getUri();
+                api.addURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), "position=" + offset, null));
+                // Open link
+                String text = getText();
+                String textLink = getValue(text, hyperlinkData.startPosition, hyperlinkData.endPosition);
+                String internalTypeName = textLink.replace('.', '/');
+                List<Container.Entry> entries = IndexesUtil.findInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+                String rootUri = entry.getContainer().getRoot().getUri().toString();
+                ArrayList<Container.Entry> sameContainerEntries = new ArrayList<>();
+
+                for (Container.Entry entry : entries) {
+                    if (entry.getUri().toString().startsWith(rootUri)) {
+                        sameContainerEntries.add(entry);
+                    }
+                }
+
+                if (sameContainerEntries.size() > 0) {
+                    api.openURI(x, y, sameContainerEntries, null, data.fragment);
+                } else if (entries.size() > 0) {
+                    api.openURI(x, y, entries, null, data.fragment);
+                }
+            } catch (URISyntaxException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+    }
+
+    // --- UriGettable --- //
+    public URI getUri() { return entry.getUri(); }
+
+    // --- ContentSavable --- //
+    public String getFileName() {
+        String path = entry.getPath();
+        int index = path.lastIndexOf('/');
+        return path.substring(index+1);
+    }
+
+    // --- IndexesChangeListener --- //
+    public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        // Update the list of containers
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        // Refresh links
+        boolean refresh = false;
+        String text = getText();
+
+        for (Map.Entry<Integer, HyperlinkData> entry : hyperlinks.entrySet()) {
+            ManifestHyperlinkData entryData = (ManifestHyperlinkData)entry.getValue();
+            String textLink = getValue(text, entryData.startPosition, entryData.endPosition);
+            String internalTypeName = textLink.replace('.', '/');
+            boolean enabled = IndexesUtil.containsInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+
+            if (entryData.enabled != enabled) {
+                entryData.enabled = enabled;
+                refresh = true;
+            }
+        }
+
+        if (refresh) {
+            textArea.repaint();
+        }
+    }
+
+    public static String getValue(String text, int startPosition, int endPosition) {
+        return text
+            // Extract text of link
+            .substring(startPosition, endPosition)
+            // Convert multiline value
+            .replace("\r\n ", "")
+            .replace("\r ", "")
+            .replace("\n ", "");
+    }
+
+    public static class ManifestHyperlinkData extends HyperlinkData {
+        public boolean enabled;
+        public String fragment;
+
+        ManifestHyperlinkData(int startPosition, int endPosition, String fragment) {
+            super(startPosition, endPosition);
+            this.enabled = false;
+            this.fragment = fragment;
+        }
+    }
+}
+
diff --git a/services/src/main/java/org/jd/gui/view/component/ModuleInfoFilePage.java b/services/src/main/java/org/jd/gui/view/component/ModuleInfoFilePage.java
new file mode 100644
index 0000000..29609c9
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/ModuleInfoFilePage.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.fife.ui.rsyntaxtextarea.*;
+import org.fife.ui.rtextarea.Marker;
+import org.jd.gui.api.API;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.decompiler.ContainerLoader;
+import org.jd.gui.util.decompiler.StringBuilderPrinter;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.index.IndexesUtil;
+
+import javax.swing.text.Segment;
+import java.awt.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.jd.core.v1.api.printer.Printer.MODULE;
+import static org.jd.core.v1.api.printer.Printer.PACKAGE;
+import static org.jd.core.v1.api.printer.Printer.TYPE;
+
+public class ModuleInfoFilePage extends ClassFilePage {
+    public static final String SYNTAX_STYLE_JAVA_MODULE = "text/java-module";
+
+    static {
+        // Add a new token maker for Java 9+ module
+        AbstractTokenMakerFactory atmf = (AbstractTokenMakerFactory)TokenMakerFactory.getDefaultInstance();
+        atmf.putMapping(SYNTAX_STYLE_JAVA_MODULE, ModuleInfoTokenMaker.class.getName());
+    }
+
+    public ModuleInfoFilePage(API api, Container.Entry entry) {
+        super(api, entry);
+    }
+
+    @Override
+    public void decompile(Map<String, String> preferences) {
+        try {
+            // Clear ...
+            clearHyperlinks();
+            clearLineNumbers();
+            typeDeclarations.clear();
+
+            // Init preferences
+            boolean unicodeEscape = getPreferenceValue(preferences, ESCAPE_UNICODE_CHARACTERS, false);
+
+            // Init loader
+            ContainerLoader loader = new ContainerLoader(entry);
+
+            // Init printer
+            ModuleInfoFilePrinter printer = new ModuleInfoFilePrinter();
+            printer.setUnicodeEscape(unicodeEscape);
+
+            // Format internal name
+            String entryPath = entry.getPath();
+            assert entryPath.endsWith(".class");
+            String entryInternalName = entryPath.substring(0, entryPath.length() - 6); // 6 = ".class".length()
+
+            // Decompile class file
+            DECOMPILER.decompile(loader, printer, entryInternalName);
+        } catch (Throwable t) {
+            assert ExceptionUtil.printStackTrace(t);
+            setText("// INTERNAL ERROR //");
+        }
+    }
+
+    @Override
+    public String getSyntaxStyle() { return SYNTAX_STYLE_JAVA_MODULE; }
+
+    @Override
+    protected void openHyperlink(int x, int y, HyperlinkData hyperlinkData) {
+        HyperlinkReferenceData hyperlinkReferenceData = (HyperlinkReferenceData)hyperlinkData;
+
+        if (hyperlinkReferenceData.reference.enabled) {
+            try {
+                // Save current position in history
+                Point location = textArea.getLocationOnScreen();
+                int offset = textArea.viewToModel(new Point(x - location.x, y - location.y));
+                URI uri = entry.getUri();
+                api.addURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), "position=" + offset, null));
+
+                // Open link
+                ModuleInfoReferenceData moduleInfoReferenceData = (ModuleInfoReferenceData)hyperlinkReferenceData.reference;
+                List<Container.Entry> entries;
+                String fragment;
+
+                switch (moduleInfoReferenceData.type) {
+                    case TYPE:
+                        entries = IndexesUtil.findInternalTypeName(collectionOfFutureIndexes, fragment = moduleInfoReferenceData.typeName);
+                        break;
+                    case PACKAGE:
+                        entries = IndexesUtil.find(collectionOfFutureIndexes, "packageDeclarations", moduleInfoReferenceData.typeName);
+                        fragment = null;
+                        break;
+                    default: // MODULE
+                        entries = IndexesUtil.find(collectionOfFutureIndexes, "javaModuleDeclarations", moduleInfoReferenceData.name);
+                        fragment = moduleInfoReferenceData.typeName;
+                        break;
+                }
+
+                if (entries.contains(entry)) {
+                    api.openURI(uri);
+                } else {
+                    String rootUri = entry.getContainer().getRoot().getUri().toString();
+                    ArrayList<Container.Entry> sameContainerEntries = new ArrayList<>();
+
+                    for (Container.Entry entry : entries) {
+                        if (entry.getUri().toString().startsWith(rootUri)) {
+                            sameContainerEntries.add(entry);
+                        }
+                    }
+
+                    if (sameContainerEntries.size() > 0) {
+                        api.openURI(x, y, sameContainerEntries, null, fragment);
+                    } else if (entries.size() > 0) {
+                        api.openURI(x, y, entries, null, fragment);
+                    }
+                }
+            } catch (URISyntaxException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+    }
+
+    // --- UriOpenable --- //
+    @Override
+    public boolean openUri(URI uri) {
+        ArrayList<DocumentRange> ranges = new ArrayList<>();
+        String fragment = uri.getFragment();
+        String query = uri.getQuery();
+
+        Marker.clearMarkAllHighlights(textArea);
+
+        if ((fragment != null) && (declarations.size() == 1)) {
+            DeclarationData declaration = declarations.entrySet().iterator().next().getValue();
+
+            if (fragment.equals(declaration.typeName)) {
+                ranges.add(new DocumentRange(declaration.startPosition, declaration.endPosition));
+            }
+        }
+
+        if (query != null) {
+            Map<String, String> parameters = parseQuery(query);
+
+            String highlightFlags = parameters.get("highlightFlags");
+            String highlightPattern = parameters.get("highlightPattern");
+
+            if ((highlightFlags != null) && (highlightPattern != null)) {
+                String regexp = createRegExp(highlightPattern);
+                Pattern pattern = Pattern.compile(regexp + ".*");
+
+                boolean t = (highlightFlags.indexOf('t') != -1); // Highlight types
+                boolean M = (highlightFlags.indexOf('M') != -1); // Highlight modules
+
+                if (highlightFlags.indexOf('d') != -1) {
+                    // Highlight declarations
+                    for (Map.Entry<String, DeclarationData> entry : declarations.entrySet()) {
+                        DeclarationData declaration = entry.getValue();
+
+                        if (M) {
+                            matchAndAddDocumentRange(pattern, declaration.name, declaration.startPosition, declaration.endPosition, ranges);
+                        }
+                    }
+                }
+
+                if (highlightFlags.indexOf('r') != -1) {
+                    // Highlight references
+                    for (Map.Entry<Integer, HyperlinkData> entry : hyperlinks.entrySet()) {
+                        HyperlinkData hyperlink = entry.getValue();
+                        ReferenceData reference = ((HyperlinkReferenceData)hyperlink).reference;
+                        ModuleInfoReferenceData moduleInfoReferenceData = (ModuleInfoReferenceData)reference;
+
+                        if (t && (moduleInfoReferenceData.type == TYPE)) {
+                            matchAndAddDocumentRange(pattern, getMostInnerTypeName(moduleInfoReferenceData.typeName), hyperlink.startPosition, hyperlink.endPosition, ranges);
+                        }
+                        if (M && (moduleInfoReferenceData.type == MODULE)) {
+                            matchAndAddDocumentRange(pattern, moduleInfoReferenceData.name, hyperlink.startPosition, hyperlink.endPosition, ranges);
+                        }
+                    }
+                }
+            }
+        }
+
+        if ((ranges != null) && !ranges.isEmpty()) {
+            textArea.setMarkAllHighlightColor(SELECT_HIGHLIGHT_COLOR);
+            Marker.markAll(textArea, ranges);
+            ranges.sort(null);
+            setCaretPositionAndCenter(ranges.get(0));
+        }
+
+        return true;
+    }
+
+    // --- IndexesChangeListener --- //
+    @Override
+    public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        // Update the list of containers
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        // Refresh links
+        boolean refresh = false;
+
+        for (ReferenceData reference : references) {
+            ModuleInfoReferenceData moduleInfoReferenceData = (ModuleInfoReferenceData)reference;
+            boolean enabled = false;
+
+            try {
+                for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
+                    if (futureIndexes.isDone()) {
+                        Map<String, Collection> index;
+                        String key;
+
+                        switch (moduleInfoReferenceData.type) {
+                            case TYPE:
+                                index = futureIndexes.get().getIndex("typeDeclarations");
+                                key = reference.typeName;
+                                break;
+                            case PACKAGE:
+                                index = futureIndexes.get().getIndex("packageDeclarations");
+                                key = reference.typeName;
+                                break;
+                            default: // MODULE
+                                index = futureIndexes.get().getIndex("javaModuleDeclarations");
+                                key = reference.name;
+                                break;
+                        }
+
+                        if ((index != null) && (index.get(key) != null)) {
+                            enabled = true;
+                            break;
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+
+            if (reference.enabled != enabled) {
+                reference.enabled = enabled;
+                refresh = true;
+            }
+        }
+
+        if (refresh) {
+            textArea.repaint();
+        }
+    }
+
+    protected static class ModuleInfoReferenceData extends ReferenceData {
+        public int type;
+
+        public ModuleInfoReferenceData(int type, String typeName, String name, String descriptor, String owner) {
+            super(typeName, name, descriptor, owner);
+            this.type = type;
+        }
+    }
+
+    public class ModuleInfoFilePrinter extends StringBuilderPrinter {
+        protected HashMap<String, ReferenceData> referencesCache = new HashMap<>();
+
+        @Override
+        public void start(int maxLineNumber, int majorVersion, int minorVersion) {}
+
+        @Override
+        public void end() {
+            setText(stringBuffer.toString());
+            initLineNumbers();
+        }
+
+        @Override
+        public void printDeclaration(int type, String internalTypeName, String name, String descriptor) {
+            declarations.put(internalTypeName, new TypePage.DeclarationData(stringBuffer.length(), name.length(), internalTypeName, name, descriptor));
+            super.printDeclaration(type, internalTypeName, name, descriptor);
+        }
+
+        @Override
+        public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) {
+            String key = (type == MODULE) ? name : internalTypeName;
+            ReferenceData reference = referencesCache.get(key);
+
+            if (reference == null) {
+                reference = new ModuleInfoReferenceData(type, internalTypeName, name, descriptor, ownerInternalName);
+                referencesCache.put(key, reference);
+                references.add(reference);
+            }
+
+            addHyperlink(new HyperlinkReferenceData(stringBuffer.length(), name.length(), reference));
+            super.printReference(type, internalTypeName, name, descriptor, ownerInternalName);
+        }
+    }
+
+    // https://github.com/bobbylight/RSyntaxTextArea/wiki/Adding-Syntax-Highlighting-for-a-new-Language
+    public static class ModuleInfoTokenMaker extends AbstractTokenMaker {
+        @Override
+        public TokenMap getWordsToHighlight() {
+            TokenMap tokenMap = new TokenMap();
+
+            tokenMap.put("exports", Token.RESERVED_WORD);
+            tokenMap.put("module", Token.RESERVED_WORD);
+            tokenMap.put("open", Token.RESERVED_WORD);
+            tokenMap.put("opens", Token.RESERVED_WORD);
+            tokenMap.put("provides", Token.RESERVED_WORD);
+            tokenMap.put("requires", Token.RESERVED_WORD);
+            tokenMap.put("to", Token.RESERVED_WORD);
+            tokenMap.put("transitive", Token.RESERVED_WORD);
+            tokenMap.put("uses", Token.RESERVED_WORD);
+            tokenMap.put("with", Token.RESERVED_WORD);
+
+            return tokenMap;
+        }
+
+        @Override
+        public void addToken(Segment segment, int start, int end, int tokenType, int startOffset) {
+            // This assumes all keywords, etc. were parsed as "identifiers."
+            if (tokenType==Token.IDENTIFIER) {
+                int value = wordsToHighlight.get(segment, start, end);
+                if (value != -1) {
+                    tokenType = value;
+                }
+            }
+            super.addToken(segment, start, end, tokenType, startOffset);
+        }
+
+        @Override
+        public Token getTokenList(Segment text, int startTokenType, int startOffset) {
+            resetTokenList();
+
+            char[] array = text.array;
+            int offset = text.offset;
+            int end = offset + text.count;
+
+            int newStartOffset = startOffset - offset;
+
+            int currentTokenStart = offset;
+            int currentTokenType  = startTokenType;
+
+            for (int i=offset; i<end; i++) {
+                char c = array[i];
+
+                switch (currentTokenType) {
+                    case Token.NULL:
+                        currentTokenStart = i;   // Starting a new token here.
+                        if (RSyntaxUtilities.isLetter(c) || (c == '_')) {
+                            currentTokenType = Token.IDENTIFIER;
+                        } else {
+                            currentTokenType = Token.WHITESPACE;
+                        }
+                        break;
+                    default: // Should never happen
+                    case Token.WHITESPACE:
+                        if (RSyntaxUtilities.isLetter(c) || (c == '_')) {
+                            addToken(text, currentTokenStart, i-1, Token.WHITESPACE, newStartOffset+currentTokenStart);
+                            currentTokenStart = i;
+                            currentTokenType = Token.IDENTIFIER;
+                        }
+                        break;
+                    case Token.IDENTIFIER:
+                        if (!RSyntaxUtilities.isLetterOrDigit(c) && (c != '_') && (c != '.')) {
+                            addToken(text, currentTokenStart, i-1, Token.IDENTIFIER, newStartOffset+currentTokenStart);
+                            currentTokenStart = i;
+                            currentTokenType = Token.WHITESPACE;
+                        }
+                        break;
+                }
+            }
+
+            if (currentTokenType == Token.NULL) {
+                addNullToken();
+            }else {
+                addToken(text, currentTokenStart,end-1, currentTokenType, newStartOffset+currentTokenStart);
+                addNullToken();
+            }
+
+            return firstToken;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/OneTypeReferencePerLinePage.java b/services/src/main/java/org/jd/gui/view/component/OneTypeReferencePerLinePage.java
new file mode 100644
index 0000000..8b1d60d
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/OneTypeReferencePerLinePage.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.IndexesChangeListener;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.index.IndexesUtil;
+
+import java.awt.*;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.Future;
+
+public class OneTypeReferencePerLinePage extends TypeReferencePage implements UriGettable, IndexesChangeListener {
+    protected API api;
+    protected Container.Entry entry;
+    protected Collection<Future<Indexes>> collectionOfFutureIndexes = Collections.emptyList();
+
+    public OneTypeReferencePerLinePage(API api, Container.Entry entry) {
+        this.api = api;
+        this.entry = entry;
+        // Load content file & Create hyperlinks
+        StringBuilder sb = new StringBuilder();
+        int offset = 0;
+
+        try (BufferedReader br = new BufferedReader(new InputStreamReader(entry.getInputStream()))) {
+            String line;
+
+            while ((line = br.readLine()) != null) {
+                String trim = line.trim();
+
+                if (trim.length() > 0) {
+                    int startIndex = offset + line.indexOf(trim);
+                    int endIndex = startIndex + trim.length();
+                    String internalTypeName = trim.replace('.', '/');
+
+                    addHyperlink(new TypeReferencePage.TypeHyperlinkData(startIndex, endIndex, internalTypeName));
+                }
+
+                offset += line.length() + 1;
+                sb.append(line).append('\n');
+            }
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        // Display
+        setText(sb.toString());
+    }
+
+    protected boolean isHyperlinkEnabled(HyperlinkData hyperlinkData) { return ((TypeHyperlinkData)hyperlinkData).enabled; }
+
+    protected void openHyperlink(int x, int y, HyperlinkData hyperlinkData) {
+        TypeHyperlinkData data = (TypeHyperlinkData)hyperlinkData;
+
+        if (data.enabled) {
+            try {
+                // Save current position in history
+                Point location = textArea.getLocationOnScreen();
+                int offset = textArea.viewToModel(new Point(x-location.x, y-location.y));
+                URI uri = entry.getUri();
+                api.addURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), "position=" + offset, null));
+
+                // Open link
+                String internalTypeName = data.internalTypeName;
+                List<Container.Entry> entries = IndexesUtil.findInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+                String rootUri = entry.getContainer().getRoot().getUri().toString();
+                ArrayList<Container.Entry> sameContainerEntries = new ArrayList<>();
+
+                for (Container.Entry entry : entries) {
+                    if (entry.getUri().toString().startsWith(rootUri)) {
+                        sameContainerEntries.add(entry);
+                    }
+                }
+
+                if (sameContainerEntries.size() > 0) {
+                    api.openURI(x, y, sameContainerEntries, null, data.internalTypeName);
+                } else if (entries.size() > 0) {
+                    api.openURI(x, y, entries, null, data.internalTypeName);
+                }
+            } catch (URISyntaxException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+    }
+
+    // --- UriGettable --- //
+    public URI getUri() { return entry.getUri(); }
+
+    // --- ContentSavable --- //
+    public String getFileName() {
+        String path = entry.getPath();
+        int index = path.lastIndexOf('/');
+        return path.substring(index+1);
+    }
+
+    // --- IndexesChangeListener --- //
+    public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        // Update the list of containers
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        // Refresh links
+        boolean refresh = false;
+
+        for (Map.Entry<Integer, HyperlinkData> entry : hyperlinks.entrySet()) {
+            TypeHyperlinkData entryData = (TypeHyperlinkData)entry.getValue();
+            String internalTypeName = entryData.internalTypeName;
+            boolean enabled = IndexesUtil.containsInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+
+            if (entryData.enabled != enabled) {
+                entryData.enabled = enabled;
+                refresh = true;
+            }
+        }
+
+        if (refresh) {
+            textArea.repaint();
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/RoundMarkErrorStrip.java b/services/src/main/java/org/jd/gui/view/component/RoundMarkErrorStrip.java
new file mode 100644
index 0000000..3050306
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/RoundMarkErrorStrip.java
@@ -0,0 +1,911 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.fife.ui.rsyntaxtextarea.DocumentRange;
+import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
+import org.fife.ui.rsyntaxtextarea.parser.Parser;
+import org.fife.ui.rsyntaxtextarea.parser.ParserNotice;
+import org.fife.ui.rsyntaxtextarea.parser.TaskTagParser.TaskNotice;
+import org.fife.ui.rtextarea.RTextArea;
+
+import javax.swing.*;
+import javax.swing.event.CaretEvent;
+import javax.swing.event.CaretListener;
+import javax.swing.text.BadLocationException;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.List;
+
+/*
+ * 'private' access prohibit all changes ==> Copy "ErrorStrip" to JD-GUI project just to change the marker.
+ *
+ * JD-GUI uses two workarounds for RSyntaxTextArea:
+ * - org.fife.ui.rtextarea.Marker
+ * - org.jd.gui.view.component.RoundMarkErrorStrip
+ */
+
+/*
+ * 08/10/2009
+ *
+ * ErrorStrip.java - A component that can visually show Parser messages (syntax
+ * errors, etc.) in an RSyntaxTextArea.
+ *
+ * This library is distributed under a modified BSD license.  See the included
+ * RSyntaxTextArea.License.txt file for details.
+ */
+
+/**
+ * A component to sit alongside an {@link RSyntaxTextArea} that displays
+ * colored markers for locations of interest (parser errors, marked
+ * occurrences, etc.).<p>
+ *
+ * <code>ErrorStrip</code>s display <code>ParserNotice</code>s from
+ * {@link Parser}s.  Currently, the only way to get lines flagged in this
+ * component is to register a <code>Parser</code> on an RSyntaxTextArea and
+ * return <code>ParserNotice</code>s for each line to display an icon for.
+ * The severity of each notice must be at least the threshold set by
+ * {@link #setLevelThreshold(ParserNotice.Level)}
+ * to be displayed in this error strip.  The default threshold is
+ * {@link ParserNotice.Level#WARNING}.<p>
+ *
+ * An <code>ErrorStrip</code> can be added to a UI like so:
+ * <pre>
+ * textArea = createTextArea();
+ * textArea.addParser(new MyParser(textArea)); // Identifies lines to display
+ * scrollPane = new RTextScrollPane(textArea, true);
+ * ErrorStrip es = new ErrorStrip(textArea);
+ * JPanel temp = new JPanel(new BorderLayout());
+ * temp.add(scrollPane);
+ * temp.add(es, BorderLayout.LINE_END);
+ * </pre>
+ *
+ * @author Robert Futrell
+ * @version 0.5
+ */
+/*
+ * Possible improvements:
+ *    1. Handle marked occurrence changes & "mark all" changes separately from
+ *       parser changes. For each property change, call a method that removes
+ *       the notices being reloaded from the Markers (removing any Markers that
+ *       are now "empty").
+ */
+public class RoundMarkErrorStrip extends JComponent {
+
+    /**
+     * The text area.
+     */
+    private RSyntaxTextArea textArea;
+
+    /**
+     * Listens for events in this component.
+     */
+    private Listener listener;
+
+    /**
+     * Whether "marked occurrences" in the text area should be shown in this
+     * error strip.
+     */
+    private boolean showMarkedOccurrences;
+
+    /**
+     * Whether markers for "mark all" highlights should be shown in this
+     * error strip.
+     */
+    private boolean showMarkAll;
+
+    /**
+     * Mapping of colors to brighter colors.  This is kept to prevent
+     * unnecessary creation of the same Colors over and over.
+     */
+    private Map<Color, Color> brighterColors;
+
+    /**
+     * Added for JD-GUI.
+     *
+     * Mapping of colors to darker colors.  This is kept to prevent
+     * unnecessary creation of the same Colors over and over.
+     */
+    private Map<Color, Color> darkerColors;
+
+    /**
+     * Only notices of this severity (or worse) will be displayed in this
+     * error strip.
+     */
+    private ParserNotice.Level levelThreshold;
+
+    /**
+     * Whether the caret marker's location should be rendered.
+     */
+    private boolean followCaret;
+
+    /**
+     * The color to use for the caret marker.
+     */
+    private Color caretMarkerColor;
+
+    /**
+     * Where we paint the caret marker.
+     */
+    private int caretLineY;
+
+    /**
+     * The last location of the caret marker.
+     */
+    private int lastLineY;
+
+    /**
+     * The preferred width of this component.
+     */
+    private static final int PREFERRED_WIDTH = 14;
+
+    private static final String MSG = "org.fife.ui.rsyntaxtextarea.ErrorStrip";
+    private static final ResourceBundle msg = ResourceBundle.getBundle(MSG);
+
+    /**
+     * Constructor.
+     *
+     * @param textArea The text area we are examining.
+     */
+    public RoundMarkErrorStrip(RSyntaxTextArea textArea) {
+        this.textArea = textArea;
+        listener = new Listener();
+        ToolTipManager.sharedInstance().registerComponent(this);
+        setLayout(null); // Manually layout Markers as they can overlap
+        addMouseListener(listener);
+        setShowMarkedOccurrences(true);
+        setShowMarkAll(true);
+        setLevelThreshold(ParserNotice.Level.WARNING);
+        setFollowCaret(true);
+        setCaretMarkerColor(new Color(0x96c5fe));
+    }
+
+
+    /**
+     * Overridden so we only start listening for parser notices when this
+     * component (and presumably the text area) are visible.
+     */
+    @Override
+    public void addNotify() {
+        super.addNotify();
+        textArea.addCaretListener(listener);
+        textArea.addPropertyChangeListener(
+                RSyntaxTextArea.PARSER_NOTICES_PROPERTY, listener);
+        textArea.addPropertyChangeListener(
+                RSyntaxTextArea.MARK_OCCURRENCES_PROPERTY, listener);
+        textArea.addPropertyChangeListener(
+                RSyntaxTextArea.MARKED_OCCURRENCES_CHANGED_PROPERTY, listener);
+        textArea.addPropertyChangeListener(
+                RSyntaxTextArea.MARK_ALL_OCCURRENCES_CHANGED_PROPERTY, listener);
+        refreshMarkers();
+    }
+
+
+    /**
+     * Manually manages layout since this component uses no layout manager.
+     */
+    @Override
+    public void doLayout() {
+        for (int i=0; i<getComponentCount(); i++) {
+            Marker m = (Marker)getComponent(i);
+            m.updateLocation();
+        }
+        listener.caretUpdate(null); // Force recalculation of caret line pos
+    }
+
+
+    /**
+     * Returns a "brighter" color.
+     *
+     * @param c The color.
+     * @return A brighter color.
+     */
+    private Color getBrighterColor(Color c) {
+        if (brighterColors==null) {
+            brighterColors = new HashMap<Color, Color>(5); // Usually small
+        }
+        Color brighter = brighterColors.get(c);
+        if (brighter==null) {
+            // Don't use c.brighter() as it doesn't work well for blue, and
+            // also doesn't return something brighter "enough."
+            int r = possiblyBrighter(c.getRed());
+            int g = possiblyBrighter(c.getGreen());
+            int b = possiblyBrighter(c.getBlue());
+            brighter = new Color(r, g, b);
+            brighterColors.put(c, brighter);
+        }
+        return brighter;
+    }
+
+
+    /**
+     * Added for JD-GUI.
+     *
+     * Returns a "brighter" color.
+     *
+     * @param c The color.
+     * @return A brighter color.
+     */
+    private Color getDarkerColor(Color c) {
+        if (darkerColors==null) {
+            darkerColors = new HashMap<Color, Color>(5); // Usually small
+        }
+        Color darker = darkerColors.get(c);
+        if (darker==null) {
+            // Don't use c.brighter() as it doesn't work well for blue, and
+            // also doesn't return something brighter "enough."
+            int r = possiblyDarker(c.getRed());
+            int g = possiblyDarker(c.getGreen());
+            int b = possiblyDarker(c.getBlue());
+            darker = new Color(r, g, b);
+            darkerColors.put(c, darker);
+        }
+        return darker;
+    }
+
+
+    /**
+     * returns the color to use when painting the caret marker.
+     *
+     * @return The caret marker color.
+     * @see #setCaretMarkerColor(Color)
+     */
+    public Color getCaretMarkerColor() {
+        return caretMarkerColor;
+    }
+
+
+    /**
+     * Returns whether the caret's position should be drawn.
+     *
+     * @return Whether the caret's position should be drawn.
+     * @see #setFollowCaret(boolean)
+     */
+    public boolean getFollowCaret() {
+        return followCaret;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Dimension getPreferredSize() {
+        int height = textArea.getPreferredScrollableViewportSize().height;
+        return new Dimension(PREFERRED_WIDTH, height);
+    }
+
+
+    /**
+     * Returns the minimum severity a parser notice must be for it to be
+     * displayed in this error strip.  This will be one of the constants
+     * defined in the <code>ParserNotice</code> class.
+     *
+     * @return The minimum severity.
+     * @see #setLevelThreshold(ParserNotice.Level)
+     */
+    public ParserNotice.Level getLevelThreshold() {
+        return levelThreshold;
+    }
+
+
+    /**
+     * Returns whether "mark all" highlights are shown in this error strip.
+     *
+     * @return Whether markers are shown for "mark all" highlights.
+     * @see #setShowMarkAll(boolean)
+     */
+    public boolean getShowMarkAll() {
+        return showMarkAll;
+    }
+
+
+    /**
+     * Returns whether marked occurrences are shown in this error strip.
+     *
+     * @return Whether marked occurrences are shown.
+     * @see #setShowMarkedOccurrences(boolean)
+     */
+    public boolean getShowMarkedOccurrences() {
+        return showMarkedOccurrences;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getToolTipText(MouseEvent e) {
+        String text = null;
+        int line = yToLine(e.getY());
+        if (line>-1) {
+            text = msg.getString("Line");
+            text = MessageFormat.format(text, Integer.valueOf(line+1));
+        }
+        return text;
+    }
+
+
+    /**
+     * Returns the y-offset in this component corresponding to a line in the
+     * text component.
+     *
+     * @param line The line.
+     * @return The y-offset.
+     * @see #yToLine(int)
+     */
+    private int lineToY(int line) {
+        int h = textArea.getVisibleRect().height;
+        float lineCount = textArea.getLineCount();
+        return (int)(((line-1)/(lineCount-1)) * h) - 2;
+    }
+
+
+    /**
+     * Overridden to (possibly) draw the caret's position.
+     *
+     * @param g The graphics context.
+     */
+    @Override
+    protected void paintComponent(Graphics g) {
+        super.paintComponent(g);
+        if (caretLineY>-1) {
+            g.setColor(getCaretMarkerColor());
+            g.fillRect(0, caretLineY, getWidth(), 2);
+        }
+    }
+
+
+    /**
+     * Returns a possibly brighter component for a color.
+     *
+     * @param i An RGB component for a color (0-255).
+     * @return A possibly brighter value for the component.
+     */
+    private static final int possiblyBrighter(int i) {
+        if (i<255) {
+            i += (int)((255-i)*0.6f);
+        }
+        return i;
+    }
+
+
+    /**
+     * Returns a possibly darker component for a color.
+     *
+     * @param i An RGB component for a color (0-255).
+     * @return A possibly brighter value for the component.
+     */
+    private static final int possiblyDarker(int i) {
+        return i -= (int)(i*0.4f);
+    }
+
+
+    /**
+     * Refreshes the markers displayed in this error strip.
+     */
+    private void refreshMarkers() {
+
+        removeAll(); // listener is removed in Marker.removeNotify()
+        Map<Integer, Marker> markerMap = new HashMap<Integer, Marker>();
+
+        List<ParserNotice> notices = textArea.getParserNotices();
+        for (ParserNotice notice : notices) {
+            if (notice.getLevel().isEqualToOrWorseThan(levelThreshold) ||
+                    (notice instanceof TaskNotice)) {
+                Integer key = Integer.valueOf(notice.getLine());
+                Marker m = markerMap.get(key);
+                if (m==null) {
+                    m = new Marker(notice);
+                    m.addMouseListener(listener);
+                    markerMap.put(key, m);
+                    add(m);
+                }
+                else {
+                    m.addNotice(notice);
+                }
+            }
+        }
+
+        if (getShowMarkedOccurrences() && textArea.getMarkOccurrences()) {
+            List<DocumentRange> occurrences = textArea.getMarkedOccurrences();
+            addMarkersForRanges(occurrences, markerMap, textArea.getMarkOccurrencesColor());
+        }
+
+        if (getShowMarkAll() /*&& textArea.getMarkAll()*/) {
+            Color markAllColor = textArea.getMarkAllHighlightColor();
+            List<DocumentRange> ranges = textArea.getMarkAllHighlightRanges();
+            addMarkersForRanges(ranges, markerMap, markAllColor);
+        }
+
+        revalidate();
+        repaint();
+
+    }
+
+
+    /**
+     * Adds markers for a list of ranges in the document.
+     *
+     * @param ranges The list of ranges in the document.
+     * @param markerMap A mapping from line number to <code>Marker</code>.
+     * @param color The color to use for the markers.
+     */
+    private void addMarkersForRanges(List<DocumentRange> ranges,
+                                     Map<Integer, Marker> markerMap, Color color) {
+        for (DocumentRange range : ranges) {
+            int line = 0;
+            try {
+                line = textArea.getLineOfOffset(range.getStartOffset());
+            } catch (BadLocationException ble) { // Never happens
+                continue;
+            }
+            ParserNotice notice = new MarkedOccurrenceNotice(range, color);
+            Integer key = Integer.valueOf(line);
+            Marker m = markerMap.get(key);
+            if (m==null) {
+                m = new Marker(notice);
+                m.addMouseListener(listener);
+                markerMap.put(key, m);
+                add(m);
+            }
+            else {
+                if (!m.containsMarkedOccurence()) {
+                    m.addNotice(notice);
+                }
+            }
+        }
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void removeNotify() {
+        super.removeNotify();
+        textArea.removeCaretListener(listener);
+        textArea.removePropertyChangeListener(
+                RSyntaxTextArea.PARSER_NOTICES_PROPERTY, listener);
+        textArea.removePropertyChangeListener(
+                RSyntaxTextArea.MARK_OCCURRENCES_PROPERTY, listener);
+        textArea.removePropertyChangeListener(
+                RSyntaxTextArea.MARKED_OCCURRENCES_CHANGED_PROPERTY, listener);
+        textArea.removePropertyChangeListener(
+                RSyntaxTextArea.MARK_ALL_OCCURRENCES_CHANGED_PROPERTY, listener);
+    }
+
+
+    /**
+     * Sets the color to use when painting the caret marker.
+     *
+     * @param color The new caret marker color.
+     * @see #getCaretMarkerColor()
+     */
+    public void setCaretMarkerColor(Color color) {
+        if (color!=null) {
+            caretMarkerColor = color;
+            listener.caretUpdate(null); // Force repaint
+        }
+    }
+
+
+    /**
+     * Toggles whether the caret's current location should be drawn.
+     *
+     * @param follow Whether the caret's current location should be followed.
+     * @see #getFollowCaret()
+     */
+    public void setFollowCaret(boolean follow) {
+        if (followCaret!=follow) {
+            if (followCaret) {
+                repaint(0,caretLineY, getWidth(),2); // Erase
+            }
+            caretLineY = -1;
+            lastLineY = -1;
+            followCaret = follow;
+            listener.caretUpdate(null); // Possibly repaint
+        }
+    }
+
+
+    /**
+     * Sets the minimum severity a parser notice must be for it to be displayed
+     * in this error strip.  This should be one of the constants defined in
+     * the <code>ParserNotice</code> class.  The default value is
+     * {@link ParserNotice.Level#WARNING}.
+     *
+     * @param level The new severity threshold.
+     * @see #getLevelThreshold()
+     * @see ParserNotice
+     */
+    public void setLevelThreshold(ParserNotice.Level level) {
+        levelThreshold = level;
+        if (isDisplayable()) {
+            refreshMarkers();
+        }
+    }
+
+
+    /**
+     * Sets whether "mark all" highlights are shown in this error strip.
+     *
+     * @param show Whether to show markers for "mark all" highlights.
+     * @see #getShowMarkAll()
+     */
+    public void setShowMarkAll(boolean show) {
+        if (show!=showMarkAll) {
+            showMarkAll = show;
+            if (isDisplayable()) { // Skip this when we're first created
+                refreshMarkers();
+            }
+        }
+    }
+
+
+    /**
+     * Sets whether marked occurrences are shown in this error strip.
+     *
+     * @param show Whether to show marked occurrences.
+     * @see #getShowMarkedOccurrences()
+     */
+    public void setShowMarkedOccurrences(boolean show) {
+        if (show!=showMarkedOccurrences) {
+            showMarkedOccurrences = show;
+            if (isDisplayable()) { // Skip this when we're first created
+                refreshMarkers();
+            }
+        }
+    }
+
+
+    /**
+     * Returns the line in the text area corresponding to a y-offset in this
+     * component.
+     *
+     * @param y The y-offset.
+     * @return The line.
+     * @see #lineToY(int)
+     */
+    private final int yToLine(int y) {
+        int line = -1;
+        int h = textArea.getVisibleRect().height;
+        if (y<h) {
+            float at = y/(float)h;
+            line = Math.round((textArea.getLineCount()-1)*at);
+        }
+        return line;
+    }
+
+
+    /**
+     * Listens for events in the error strip and its markers.
+     */
+    private class Listener extends MouseAdapter
+            implements PropertyChangeListener, CaretListener {
+
+        private Rectangle visibleRect = new Rectangle();
+
+        public void caretUpdate(CaretEvent e) {
+            if (getFollowCaret()) {
+                int line = textArea.getCaretLineNumber();
+                float percent = line / (float)(textArea.getLineCount()-1);
+                textArea.computeVisibleRect(visibleRect);
+                caretLineY = (int)(visibleRect.height*percent);
+                if (caretLineY!=lastLineY) {
+                    repaint(0,lastLineY, getWidth(), 2); // Erase old position
+                    repaint(0,caretLineY, getWidth(), 2);
+                    lastLineY = caretLineY;
+                }
+            }
+        }
+
+        @Override
+        public void mouseClicked(MouseEvent e) {
+
+            Component source = (Component)e.getSource();
+            if (source instanceof Marker) {
+                ((Marker)source).mouseClicked(e);
+                return;
+            }
+
+            int line = yToLine(e.getY());
+            if (line>-1) {
+                try {
+                    int offs = textArea.getLineStartOffset(line);
+                    textArea.setCaretPosition(offs);
+                } catch (BadLocationException ble) { // Never happens
+                    UIManager.getLookAndFeel().provideErrorFeedback(textArea);
+                }
+            }
+
+        }
+
+        public void propertyChange(PropertyChangeEvent e) {
+
+            String propName = e.getPropertyName();
+
+            // If they change whether marked occurrences are visible in editor
+            if (RSyntaxTextArea.MARK_OCCURRENCES_PROPERTY.equals(propName)) {
+                if (getShowMarkedOccurrences()) {
+                    refreshMarkers();
+                }
+            }
+
+            // If parser notices changed.
+            // TODO: Don't update "mark all/occurrences" markers.
+            else if (RSyntaxTextArea.PARSER_NOTICES_PROPERTY.equals(propName)) {
+                refreshMarkers();
+            }
+
+            // If marked occurrences changed.
+            // TODO: Only update "mark occurrences" markers, not all of them.
+            else if (RSyntaxTextArea.MARKED_OCCURRENCES_CHANGED_PROPERTY.
+                    equals(propName)) {
+                if (getShowMarkedOccurrences()) {
+                    refreshMarkers();
+                }
+            }
+
+            // If "mark all" occurrences changed.
+            // TODO: Only update "mark all" markers, not all of them.
+            else if (RTextArea.MARK_ALL_OCCURRENCES_CHANGED_PROPERTY.
+                    equals(propName)) {
+                if (getShowMarkAll()) {
+                    refreshMarkers();
+                }
+            }
+
+        }
+
+    }
+
+
+    /**
+     * A notice that wraps a "marked occurrence."
+     */
+    private class MarkedOccurrenceNotice implements ParserNotice {
+
+        private DocumentRange range;
+        private Color color;
+
+        public MarkedOccurrenceNotice(DocumentRange range, Color color) {
+            this.range = range;
+            this.color = color;
+        }
+
+        public int compareTo(ParserNotice other) {
+            return 0; // Value doesn't matter
+        }
+
+        public boolean containsPosition(int pos) {
+            return pos>=range.getStartOffset() && pos<range.getEndOffset();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            // FindBugs - Define equals() when defining compareTo()
+            if (!(o instanceof ParserNotice)) {
+                return false;
+            }
+            return compareTo((ParserNotice)o)==0;
+        }
+
+        public Color getColor() {
+            return color;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public boolean getKnowsOffsetAndLength() {
+            return true;
+        }
+
+        public int getLength() {
+            return range.getEndOffset() - range.getStartOffset();
+        }
+
+        public Level getLevel() {
+            return Level.INFO; // Won't matter
+        }
+
+        public int getLine() {
+            try {
+                return textArea.getLineOfOffset(range.getStartOffset())+1;
+            } catch (BadLocationException ble) {
+                return 0;
+            }
+        }
+
+        public String getMessage() {
+            String text = null;
+            try {
+                String word = textArea.getText(range.getStartOffset(),
+                        getLength());
+                text = msg.getString("OccurrenceOf");
+                text = MessageFormat.format(text, word);
+            } catch (BadLocationException ble) {
+                UIManager.getLookAndFeel().provideErrorFeedback(textArea);
+            }
+            return text;
+        }
+
+        public int getOffset() {
+            return range.getStartOffset();
+        }
+
+        public Parser getParser() {
+            return null;
+        }
+
+        public boolean getShowInEditor() {
+            return false; // Value doesn't matter
+        }
+
+        public String getToolTipText() {
+            return null;
+        }
+
+        @Override
+        public int hashCode() { // FindBugs, since we override equals()
+            return 0; // Value doesn't matter for us.
+        }
+
+    }
+
+
+    /**
+     * A "marker" in this error strip, representing one or more notices.
+     */
+    private class Marker extends JComponent {
+
+        private List<ParserNotice> notices;
+
+        public Marker(ParserNotice notice) {
+            notices = new ArrayList<ParserNotice>(1); // Usually just 1
+            addNotice(notice);
+            setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+            setSize(getPreferredSize());
+            ToolTipManager.sharedInstance().registerComponent(this);
+        }
+
+        public void addNotice(ParserNotice notice) {
+            notices.add(notice);
+        }
+
+        public boolean containsMarkedOccurence() {
+            boolean result = false;
+            for (int i=0; i<notices.size(); i++) {
+                if (notices.get(i) instanceof MarkedOccurrenceNotice) {
+                    result = true;
+                    break;
+                }
+            }
+            return result;
+        }
+
+        public Color getColor() {
+            // Return the color for the highest-level parser.
+            Color c = null;
+            int lowestLevel = Integer.MAX_VALUE; // ERROR is 0
+            for (ParserNotice notice : notices) {
+                if (notice.getLevel().getNumericValue()<lowestLevel) {
+                    lowestLevel = notice.getLevel().getNumericValue();
+                    c = notice.getColor();
+                }
+            }
+            return c;
+        }
+
+        @Override
+        public Dimension getPreferredSize() {
+            int w = PREFERRED_WIDTH - 4; // 2-pixel empty border
+            return new Dimension(w, 5);
+        }
+
+        @Override
+        public String getToolTipText() {
+
+            String text = null;
+
+            if (notices.size()==1) {
+                text = notices.get(0).getMessage();
+            }
+            else { // > 1
+                StringBuilder sb = new StringBuilder("<html>");
+                sb.append(msg.getString("MultipleMarkers"));
+                sb.append("<br>");
+                for (int i=0; i<notices.size(); i++) {
+                    ParserNotice pn = notices.get(i);
+                    sb.append("&nbsp;&nbsp;&nbsp;- ");
+                    sb.append(pn.getMessage());
+                    sb.append("<br>");
+                }
+                text = sb.toString();
+            }
+
+            return text;
+
+        }
+
+        protected void mouseClicked(MouseEvent e) {
+            ParserNotice pn = notices.get(0);
+            int offs = pn.getOffset();
+            int len = pn.getLength();
+            if (offs>-1 && len>-1) { // These values are optional
+                textArea.setSelectionStart(offs);
+                textArea.setSelectionEnd(offs+len);
+            }
+            else {
+                int line = pn.getLine();
+                try {
+                    offs = textArea.getLineStartOffset(line);
+                    textArea.setCaretPosition(offs);
+                } catch (BadLocationException ble) { // Never happens
+                    UIManager.getLookAndFeel().provideErrorFeedback(textArea);
+                }
+            }
+        }
+
+        @Override
+        protected void paintComponent(Graphics g) {
+
+            // TODO: Give "priorities" and always pick color of a notice with
+            // highest priority (e.g. parsing errors will usually be red).
+
+            Color color = getColor();
+            if (color==null) {
+                color = Color.GRAY;
+            }
+
+            Color brighterColor = getBrighterColor(color);
+            Color darkerColor = getDarkerColor(color);
+
+            int w = getWidth();
+            int h = getHeight();
+
+            // Draw background
+            g.setColor(color);
+            g.fillRect(0,0, w,h);
+
+            // Draw border
+            w--;
+            h--;
+
+            g.setColor(darkerColor);
+            g.drawLine(w,0, w,h);
+            g.drawRect(0,h, w,h);
+
+            g.setColor(brighterColor);
+            g.drawLine(0,0, w,0);
+            g.drawRect(0,0, 0,h);
+        }
+
+        @Override
+        public void removeNotify() {
+            super.removeNotify();
+            ToolTipManager.sharedInstance().unregisterComponent(this);
+            removeMouseListener(listener);
+        }
+
+        public void updateLocation() {
+            int line = notices.get(0).getLine();
+            int y = lineToY(line);
+            setLocation(2, y);
+        }
+
+    }
+
+
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/jd/gui/view/component/TextPage.java b/services/src/main/java/org/jd/gui/view/component/TextPage.java
new file mode 100644
index 0000000..bbae1e4
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/TextPage.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.ContentCopyable;
+import org.jd.gui.api.feature.ContentSavable;
+import org.jd.gui.api.feature.ContentSelectable;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.io.NewlineOutputStream;
+
+import java.awt.datatransfer.StringSelection;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+public class TextPage extends AbstractTextPage implements ContentCopyable, ContentSelectable, ContentSavable {
+
+    // --- ContentCopyable --- //
+    @Override
+    public void copy() {
+        if (textArea.getSelectionStart() == textArea.getSelectionEnd()) {
+            getToolkit().getSystemClipboard().setContents(new StringSelection(""), null);
+        } else {
+            textArea.copyAsStyledText();
+        }
+    }
+
+    // --- ContentSelectable --- //
+    @Override
+    public void selectAll() {
+        textArea.selectAll();
+    }
+
+    // --- ContentSavable --- //
+    @Override
+    public String getFileName() { return "file.txt"; }
+
+    @Override
+    public void save(API api, OutputStream os) {
+        try (OutputStreamWriter writer = new OutputStreamWriter(new NewlineOutputStream(os), "UTF-8")) {
+            writer.write(textArea.getText());
+        } catch (IOException e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/TypePage.java b/services/src/main/java/org/jd/gui/view/component/TypePage.java
new file mode 100644
index 0000000..99dcaa9
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/TypePage.java
@@ -0,0 +1,540 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+package org.jd.gui.view.component;
+
+import org.fife.ui.rsyntaxtextarea.DocumentRange;
+import org.fife.ui.rtextarea.Marker;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.FocusedTypeGettable;
+import org.jd.gui.api.feature.IndexesChangeListener;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.feature.UriOpenable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.api.model.Type;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.index.IndexesUtil;
+import org.jd.gui.util.matcher.DescriptorMatcher;
+
+import java.awt.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.function.BiFunction;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class TypePage extends CustomLineNumbersPage implements UriGettable, UriOpenable, IndexesChangeListener, FocusedTypeGettable {
+    protected API api;
+    protected Container.Entry entry;
+    protected Collection<Future<Indexes>> collectionOfFutureIndexes = Collections.emptyList();
+
+    protected HashMap<String, DeclarationData> declarations = new HashMap<>();
+    protected TreeMap<Integer, DeclarationData> typeDeclarations = new TreeMap<>();
+    protected ArrayList<ReferenceData> references = new ArrayList<>();
+    protected ArrayList<StringData> strings = new ArrayList<>();
+
+    public TypePage(API api, Container.Entry entry) {
+        // Init attributes
+        this.api = api;
+        this.entry = entry;
+    }
+
+    @Override
+    protected boolean isHyperlinkEnabled(HyperlinkData hyperlinkData) {
+        return ((HyperlinkReferenceData)hyperlinkData).reference.enabled;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    protected void openHyperlink(int x, int y, HyperlinkData hyperlinkData) {
+        HyperlinkReferenceData hyperlinkReferenceData = (HyperlinkReferenceData)hyperlinkData;
+
+        if (hyperlinkReferenceData.reference.enabled) {
+            try {
+                // Save current position in history
+                Point location = textArea.getLocationOnScreen();
+                int offset = textArea.viewToModel(new Point(x - location.x, y - location.y));
+                URI uri = entry.getUri();
+                api.addURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), "position=" + offset, null));
+
+                // Open link
+                ReferenceData reference = hyperlinkReferenceData.reference;
+                String typeName = reference.typeName;
+                List<Container.Entry> entries = IndexesUtil.findInternalTypeName(collectionOfFutureIndexes, typeName);
+                String fragment = typeName;
+
+                if (reference.name != null) {
+                    fragment += '-' + reference.name;
+                }
+                if (reference.descriptor != null) {
+                    fragment += '-' + reference.descriptor;
+                }
+
+                if (entries.contains(entry)) {
+                    api.openURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), fragment));
+                } else {
+                    String rootUri = entry.getContainer().getRoot().getUri().toString();
+                    ArrayList<Container.Entry> sameContainerEntries = new ArrayList<>();
+
+                    for (Container.Entry entry : entries) {
+                        if (entry.getUri().toString().startsWith(rootUri)) {
+                            sameContainerEntries.add(entry);
+                        }
+                    }
+
+                    if (sameContainerEntries.size() > 0) {
+                        api.openURI(x, y, sameContainerEntries, null, fragment);
+                    } else if (entries.size() > 0) {
+                        api.openURI(x, y, entries, null, fragment);
+                    }
+                }
+            } catch (URISyntaxException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+    }
+
+    // --- UriGettable --- //
+    @Override public URI getUri() { return entry.getUri(); }
+
+    // --- UriOpenable --- //
+    /**
+     * @param uri for URI format, @see jd.gui.api.feature.UriOpenable
+     */
+    @Override
+    public boolean openUri(URI uri) {
+        ArrayList<DocumentRange> ranges = new ArrayList<>();
+        String fragment = uri.getFragment();
+        String query = uri.getQuery();
+
+        Marker.clearMarkAllHighlights(textArea);
+
+        if (fragment != null) {
+            matchFragmentAndAddDocumentRange(fragment, declarations, ranges);
+        }
+
+        if (query != null) {
+            Map<String, String> parameters = parseQuery(query);
+
+            if (parameters.containsKey("lineNumber")) {
+                String lineNumber = parameters.get("lineNumber");
+
+                try {
+                    goToLineNumber(Integer.parseInt(lineNumber));
+                    return true;
+                } catch (NumberFormatException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            } else if (parameters.containsKey("position")) {
+                String position = parameters.get("position");
+
+                try {
+                    int pos = Integer.parseInt(position);
+                    if (textArea.getDocument().getLength() > pos) {
+                        ranges.add(new DocumentRange(pos, pos));
+                    }
+                } catch (NumberFormatException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            } else {
+                matchQueryAndAddDocumentRange(parameters, declarations, hyperlinks, strings, ranges);
+            }
+        }
+
+        if ((ranges != null) && !ranges.isEmpty()) {
+            textArea.setMarkAllHighlightColor(SELECT_HIGHLIGHT_COLOR);
+            Marker.markAll(textArea, ranges);
+            ranges.sort(null);
+            setCaretPositionAndCenter(ranges.get(0));
+        }
+
+        return true;
+    }
+
+    public static void matchFragmentAndAddDocumentRange(String fragment, HashMap<String, DeclarationData> declarations, List<DocumentRange> ranges) {
+        if ((fragment.indexOf('?') != -1) || (fragment.indexOf('*') != -1)) {
+            // Unknown type and/or descriptor ==> Select all and scroll to the first one
+            int lastDash = fragment.lastIndexOf('-');
+
+            if (lastDash == -1) {
+                // Search types
+                String slashAndTypeName = fragment.substring(1);
+                String typeName = fragment.substring(2);
+
+                for (Map.Entry<String, DeclarationData> entry : declarations.entrySet()) {
+                    if (entry.getKey().endsWith(slashAndTypeName) || entry.getKey().equals(typeName)) {
+                        ranges.add(new DocumentRange(entry.getValue().startPosition, entry.getValue().endPosition));
+                    }
+                }
+            } else {
+                String prefix = fragment.substring(0, lastDash+1);
+                String suffix = fragment.substring(lastDash+1);
+                BiFunction<String, String, Boolean> matchDescriptors;
+
+                if (suffix.charAt(0) == '(') {
+                    matchDescriptors = DescriptorMatcher::matchMethodDescriptors;
+                } else {
+                    matchDescriptors = DescriptorMatcher::matchFieldDescriptors;
+                }
+
+                if (fragment.charAt(0) == '*') {
+                    // Unknown type
+                    String slashAndTypeNameAndName = prefix.substring(1);
+                    String typeNameAndName = prefix.substring(2);
+
+                    for (Map.Entry<String, DeclarationData> entry : declarations.entrySet()) {
+                        String key = entry.getKey();
+                        if ((key.indexOf(slashAndTypeNameAndName) != -1) || (key.startsWith(typeNameAndName))) {
+                            int index = key.lastIndexOf('-') + 1;
+                            if (matchDescriptors.apply(suffix, key.substring(index))) {
+                                ranges.add(new DocumentRange(entry.getValue().startPosition, entry.getValue().endPosition));
+                            }
+                        }
+                    }
+                } else {
+                    // Known type
+                    for (Map.Entry<String, DeclarationData> entry : declarations.entrySet()) {
+                        String key = entry.getKey();
+                        if (key.startsWith(prefix)) {
+                            int index = key.lastIndexOf('-') + 1;
+                            if (matchDescriptors.apply(suffix, key.substring(index))) {
+                                ranges.add(new DocumentRange(entry.getValue().startPosition, entry.getValue().endPosition));
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            // Known type and descriptor ==> Search and high light item
+            DeclarationData data = declarations.get(fragment);
+            if (data != null) {
+                ranges.add(new DocumentRange(data.startPosition, data.endPosition));
+            } else if (fragment.endsWith("-<clinit>-()V")) {
+                // 'static' bloc not found ==> Select type declaration
+                String typeName = fragment.substring(0, fragment.indexOf('-'));
+                data = declarations.get(typeName);
+                ranges.add(new DocumentRange(data.startPosition, data.endPosition));
+            }
+        }
+    }
+
+    public static void matchQueryAndAddDocumentRange(
+            Map<String, String> parameters,
+            HashMap<String, DeclarationData> declarations, TreeMap<Integer, HyperlinkData> hyperlinks, ArrayList<StringData> strings,
+            List<DocumentRange> ranges) {
+
+        String highlightFlags = parameters.get("highlightFlags");
+        String highlightPattern = parameters.get("highlightPattern");
+
+        if ((highlightFlags != null) && (highlightPattern != null)) {
+            String highlightScope = parameters.get("highlightScope");
+            String regexp = createRegExp(highlightPattern);
+            Pattern pattern = Pattern.compile(regexp + ".*");
+
+            if (highlightFlags.indexOf('s') != -1) {
+                // Highlight strings
+                Pattern patternForString = Pattern.compile(regexp);
+
+                for (StringData data : strings) {
+                    if (matchScope(highlightScope, data.owner)) {
+                        Matcher matcher = patternForString.matcher(data.text);
+                        int offset = data.startPosition;
+
+                        while(matcher.find()) {
+                            ranges.add(new DocumentRange(offset + matcher.start(), offset + matcher.end()));
+                        }
+                    }
+                }
+            }
+
+            boolean t = (highlightFlags.indexOf('t') != -1); // Highlight types
+            boolean f = (highlightFlags.indexOf('f') != -1); // Highlight fields
+            boolean m = (highlightFlags.indexOf('m') != -1); // Highlight methods
+            boolean c = (highlightFlags.indexOf('c') != -1); // Highlight constructors
+
+            if (highlightFlags.indexOf('d') != -1) {
+                // Highlight declarations
+                for (Map.Entry<String, DeclarationData> entry : declarations.entrySet()) {
+                    DeclarationData declaration = entry.getValue();
+
+                    if (matchScope(highlightScope, declaration.typeName)) {
+                        if ((t && declaration.isAType()) || (c && declaration.isAConstructor())) {
+                            matchAndAddDocumentRange(pattern, getMostInnerTypeName(declaration.typeName), declaration.startPosition, declaration.endPosition, ranges);
+                        }
+                        if ((f && declaration.isAField()) || (m && declaration.isAMethod())) {
+                            matchAndAddDocumentRange(pattern, declaration.name, declaration.startPosition, declaration.endPosition, ranges);
+                        }
+                    }
+                }
+            }
+
+            if (highlightFlags.indexOf('r') != -1) {
+                // Highlight references
+                for (Map.Entry<Integer, HyperlinkData> entry : hyperlinks.entrySet()) {
+                    HyperlinkData hyperlink = entry.getValue();
+                    ReferenceData reference = ((HyperlinkReferenceData)hyperlink).reference;
+
+                    if (matchScope(highlightScope, reference.owner)) {
+                        if ((t && reference.isAType()) || (c && reference.isAConstructor())) {
+                            matchAndAddDocumentRange(pattern, getMostInnerTypeName(reference.typeName), hyperlink.startPosition, hyperlink.endPosition, ranges);
+                        }
+                        if ((f && reference.isAField()) || (m && reference.isAMethod())) {
+                            matchAndAddDocumentRange(pattern, reference.name, hyperlink.startPosition, hyperlink.endPosition, ranges);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public static boolean matchScope(String scope, String type) {
+        if ((scope == null) || scope.isEmpty())
+            return true;
+        if (scope.charAt(0) == '*')
+            return type.endsWith(scope.substring(1)) || type.equals(scope.substring(2));
+        return type.equals(scope);
+    }
+
+    public static void matchAndAddDocumentRange(Pattern pattern, String text, int start, int end, List<DocumentRange> ranges) {
+        if (pattern.matcher(text).matches()) {
+            ranges.add(new DocumentRange(start, end));
+        }
+    }
+
+    public static String getMostInnerTypeName(String typeName) {
+        int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1;
+        int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1;
+        int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex);
+        return typeName.substring(lastIndex);
+    }
+
+    // --- FocusedTypeGettable --- //
+    @Override public String getFocusedTypeName() {
+        Map.Entry<Integer, DeclarationData> entry = typeDeclarations.floorEntry(textArea.getCaretPosition());
+
+        if (entry != null) {
+            DeclarationData data = entry.getValue();
+            if (data != null) {
+                return data.typeName;
+            }
+        }
+
+        return null;
+    }
+
+    @Override public Container.Entry getEntry() { return entry; }
+
+    // --- IndexesChangeListener --- //
+    @Override
+    public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        // Update the list of containers
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        // Refresh links
+        boolean refresh = false;
+
+        for (ReferenceData reference : references) {
+            String typeName = reference.typeName;
+            boolean enabled;
+
+            if (reference.name == null) {
+                enabled = false;
+
+                try {
+                    for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
+                        if (futureIndexes.isDone()) {
+                            Map<String, Collection> index = futureIndexes.get().getIndex("typeDeclarations");
+                            if ((index != null) && (index.get(typeName) != null)) {
+                                enabled = true;
+                                break;
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            } else {
+                try {
+                    // Recursive search
+                    typeName = searchTypeHavingMember(typeName, reference.name, reference.descriptor, entry);
+                    if (typeName != null) {
+                        // Replace type with the real type having the referenced member
+                        reference.typeName = typeName;
+                        enabled = true;
+                    } else {
+                        enabled = false;
+                    }
+                } catch (Error e) {
+                    // Catch StackOverflowError or OutOfMemoryError
+                    assert ExceptionUtil.printStackTrace(e);
+                    enabled = false;
+                }
+            }
+
+            if (reference.enabled != enabled) {
+                reference.enabled = enabled;
+                refresh = true;
+            }
+        }
+
+        if (refresh) {
+            textArea.repaint();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected String searchTypeHavingMember(String typeName, String name, String descriptor, Container.Entry entry) {
+        ArrayList<Container.Entry> entries = new ArrayList<>();
+
+        try {
+            for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
+                if (futureIndexes.isDone()) {
+                    Map<String, Collection> index = futureIndexes.get().getIndex("typeDeclarations");
+                    if (index != null) {
+                        Collection<Container.Entry> collection = index.get(typeName);
+                        if (collection != null) {
+                            entries.addAll(collection);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            assert ExceptionUtil.printStackTrace(e);
+        }
+
+        String rootUri = entry.getContainer().getRoot().getUri().toString();
+        ArrayList<Container.Entry> sameContainerEntries = new ArrayList<>();
+
+        for (Container.Entry e : entries) {
+            if (e.getUri().toString().startsWith(rootUri)) {
+                sameContainerEntries.add(e);
+            }
+        }
+
+        if (sameContainerEntries.size() > 0) {
+            return searchTypeHavingMember(typeName, name, descriptor, sameContainerEntries);
+        } else {
+            return searchTypeHavingMember(typeName, name, descriptor, entries);
+        }
+    }
+
+    protected String searchTypeHavingMember(String typeName, String name, String descriptor, List<Container.Entry> entries) {
+        for (Container.Entry entry : entries) {
+            Type type = api.getTypeFactory(entry).make(api, entry, typeName);
+
+            if (type != null) {
+                if (descriptor.indexOf('(') == -1) {
+                    // Search a field
+                    for (Type.Field field : type.getFields()) {
+                        if (field.getName().equals(name) && DescriptorMatcher.matchFieldDescriptors(field.getDescriptor(), descriptor)) {
+                            // Field found
+                            return typeName;
+                        }
+                    }
+                } else {
+                    // Search a method
+                    for (Type.Method method : type.getMethods()) {
+                        if (method.getName().equals(name) && DescriptorMatcher.matchMethodDescriptors(method.getDescriptor(), descriptor)) {
+                            // Method found
+                            return typeName;
+                        }
+                    }
+                }
+
+                // Not found -> Search in super type
+                String typeOwnerName = searchTypeHavingMember(type.getSuperName(), name, descriptor, entry);
+                if (typeOwnerName != null) {
+                    return typeOwnerName;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public static class StringData {
+        int startPosition;
+        int endPosition;
+        String text;
+        String owner;
+
+        public StringData(int startPosition, int length, String text, String owner) {
+            this.startPosition = startPosition;
+            this.endPosition = startPosition + length;
+            this.text = text;
+            this.owner = owner;
+        }
+    }
+
+    public static class DeclarationData {
+        int startPosition;
+        int endPosition;
+        String typeName;
+        /**
+         * Field or method name or null for type
+         */
+        String name;
+        String descriptor;
+
+        public DeclarationData(int startPosition, int length, String typeName, String name, String descriptor) {
+            this.startPosition = startPosition;
+            this.endPosition = startPosition + length;
+            this.typeName = typeName;
+            this.name = name;
+            this.descriptor = descriptor;
+        }
+
+        public boolean isAType() { return name == null; }
+        public boolean isAField() { return (descriptor != null) && descriptor.charAt(0) != '('; }
+        public boolean isAMethod() { return (descriptor != null) && descriptor.charAt(0) == '('; }
+        public boolean isAConstructor() { return "<init>".equals(name); }
+    }
+
+    public static class HyperlinkReferenceData extends HyperlinkData {
+        public ReferenceData reference;
+
+        public HyperlinkReferenceData(int startPosition, int length, ReferenceData reference) {
+            super(startPosition, startPosition+length);
+            this.reference = reference;
+        }
+    }
+
+    protected static class ReferenceData {
+        public String typeName;
+        /**
+         * Field or method name or null for type
+         */
+        public String name;
+        /**
+         * Field or method descriptor or null for type
+         */
+        public String descriptor;
+        /**
+         * Internal type name containing reference or null for "import" statement.
+         * Used to high light items matching with URI like "file://dir1/dir2/file?highlightPattern=hello&highlightFlags=drtcmfs&highlightScope=type".
+         */
+        public String owner;
+        /**
+         * "Enabled" flag for link of reference
+         */
+        public boolean enabled = false;
+
+        public ReferenceData(String typeName, String name, String descriptor, String owner) {
+            this.typeName = typeName;
+            this.name = name;
+            this.descriptor = descriptor;
+            this.owner = owner;
+        }
+
+        boolean isAType() { return name == null; }
+        boolean isAField() { return (descriptor != null) && descriptor.charAt(0) != '('; }
+        boolean isAMethod() { return (descriptor != null) && descriptor.charAt(0) == '('; }
+        boolean isAConstructor() { return "<init>".equals(name); }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/TypeReferencePage.java b/services/src/main/java/org/jd/gui/view/component/TypeReferencePage.java
new file mode 100644
index 0000000..b8f4c16
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/TypeReferencePage.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.fife.ui.rsyntaxtextarea.DocumentRange;
+import org.fife.ui.rtextarea.Marker;
+import org.jd.gui.util.exception.ExceptionUtil;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Page containing type references (Hyperlinks to pages of type)
+ */
+public abstract class TypeReferencePage extends HyperlinkPage {
+
+    // --- UriOpenable --- //
+    public boolean openUri(URI uri) {
+        ArrayList<DocumentRange> ranges = new ArrayList<>();
+        String query = uri.getQuery();
+
+        Marker.clearMarkAllHighlights(textArea);
+
+        if (query != null) {
+            Map<String, String> parameters = parseQuery(query);
+
+            if (parameters.containsKey("lineNumber")) {
+                String lineNumber = parameters.get("lineNumber");
+
+                try {
+                    goToLineNumber(Integer.parseInt(lineNumber));
+                    return true;
+                } catch (NumberFormatException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            } else if (parameters.containsKey("position")) {
+                String position = parameters.get("position");
+
+                try {
+                    int pos = Integer.parseInt(position);
+                    if (textArea.getDocument().getLength() > pos) {
+                        ranges.add(new DocumentRange(pos, pos));
+                    }
+                } catch (NumberFormatException e) {
+                    assert ExceptionUtil.printStackTrace(e);
+                }
+            } else {
+                String highlightFlags = parameters.get("highlightFlags");
+                String highlightPattern = parameters.get("highlightPattern");
+
+                if ((highlightFlags != null) && (highlightPattern != null)) {
+                    String regexp = createRegExp(highlightPattern);
+
+                    if (highlightFlags.indexOf('s') != -1) {
+                        // Highlight strings
+                        Pattern pattern = Pattern.compile(regexp);
+                        Matcher matcher = pattern.matcher(textArea.getText());
+
+                        while (matcher.find()) {
+                            ranges.add(new DocumentRange(matcher.start(), matcher.end()));
+                        }
+                    }
+
+                    if ((highlightFlags.indexOf('t') != -1) && (highlightFlags.indexOf('r') != -1)) {
+                        // Highlight type references
+                        Pattern pattern = Pattern.compile(regexp + ".*");
+
+                        for (Map.Entry<Integer, HyperlinkData> entry : hyperlinks.entrySet()) {
+                            TypeHyperlinkData hyperlink = (TypeHyperlinkData)entry.getValue();
+                            String name = getMostInnerTypeName(hyperlink.internalTypeName);
+
+                            if (pattern.matcher(name).matches()) {
+                                ranges.add(new DocumentRange(hyperlink.startPosition, hyperlink.endPosition));
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if ((ranges != null) && !ranges.isEmpty()) {
+            textArea.setMarkAllHighlightColor(SELECT_HIGHLIGHT_COLOR);
+            Marker.markAll(textArea, ranges);
+            ranges.sort(null);
+            setCaretPositionAndCenter(ranges.get(0));
+        }
+
+        return false;
+    }
+
+    public String getMostInnerTypeName(String typeName) {
+        int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1;
+        int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1;
+        int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex);
+        return typeName.substring(lastIndex);
+    }
+
+    public static class TypeHyperlinkData extends HyperlinkData {
+        public boolean enabled;
+        public String internalTypeName;
+
+        TypeHyperlinkData(int startPosition, int endPosition, String internalTypeName) {
+            super(startPosition, endPosition);
+            this.enabled = false;
+            this.internalTypeName = internalTypeName;
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/WebXmlFilePage.java b/services/src/main/java/org/jd/gui/view/component/WebXmlFilePage.java
new file mode 100644
index 0000000..4b9361f
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/WebXmlFilePage.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.IndexesChangeListener;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.index.IndexesUtil;
+import org.jd.gui.util.io.TextReader;
+import org.jd.gui.util.xml.AbstractXmlPathFinder;
+
+import java.awt.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.Future;
+
+public class WebXmlFilePage extends TypeReferencePage implements UriGettable, IndexesChangeListener {
+    protected API api;
+    protected Container.Entry entry;
+    protected Collection<Future<Indexes>> collectionOfFutureIndexes;
+
+    public WebXmlFilePage(API api, Container.Entry entry) {
+        this.api = api;
+        this.entry = entry;
+        // Load content file
+        String text = TextReader.getText(entry.getInputStream());
+        // Create hyperlinks
+        new PathFinder().find(text);
+        // Display
+        setText(text);
+    }
+
+    public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_XML; }
+
+    protected boolean isHyperlinkEnabled(HyperlinkData hyperlinkData) { return ((TypeHyperlinkData)hyperlinkData).enabled; }
+
+    protected void openHyperlink(int x, int y, HyperlinkData hyperlinkData) {
+        TypeHyperlinkData data = (TypeHyperlinkData)hyperlinkData;
+
+        if (data.enabled) {
+            try {
+                // Save current position in history
+                Point location = textArea.getLocationOnScreen();
+                int offset = textArea.viewToModel(new Point(x-location.x, y-location.y));
+                URI uri = entry.getUri();
+                api.addURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), "position=" + offset, null));
+
+                // Open link
+                if (hyperlinkData instanceof PathHyperlinkData) {
+                    PathHyperlinkData d = (PathHyperlinkData)hyperlinkData;
+                    String path = d.path;
+                    Container.Entry entry = searchEntry(this.entry.getContainer().getRoot(), path);
+                    if (entry != null) {
+                        api.openURI(x, y, Collections.singletonList(entry), null, path);
+                    }
+                } else {
+                    String internalTypeName = data.internalTypeName;
+                    List<Container.Entry> entries = IndexesUtil.findInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+                    String rootUri = entry.getContainer().getRoot().getUri().toString();
+                    ArrayList<Container.Entry> sameContainerEntries = new ArrayList<>();
+
+                    for (Container.Entry entry : entries) {
+                        if (entry.getUri().toString().startsWith(rootUri)) {
+                            sameContainerEntries.add(entry);
+                        }
+                    }
+
+                    if (sameContainerEntries.size() > 0) {
+                        api.openURI(x, y, sameContainerEntries, null, data.internalTypeName);
+                    } else if (entries.size() > 0) {
+                        api.openURI(x, y, entries, null, data.internalTypeName);
+                    }
+                }
+            } catch (URISyntaxException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+    }
+
+    public static Container.Entry searchEntry(Container.Entry parent, String path) {
+        if (path.charAt(0) == '/')
+            path = path.substring(1);
+        return recursiveSearchEntry(parent, path);
+    }
+
+    public static Container.Entry recursiveSearchEntry(Container.Entry parent, String path) {
+        Container.Entry entry = null;
+
+        for (Container.Entry child : parent.getChildren()) {
+            if (path.equals(child.getPath())) {
+                entry = child;
+                break;
+            }
+        }
+
+        if (entry != null) {
+            return entry;
+        } else {
+            for (Container.Entry child : parent.getChildren()) {
+                if (path.startsWith(child.getPath() + '/')) {
+                    entry = child;
+                    break;
+                }
+            }
+
+            return (entry != null) ? searchEntry(entry, path) : null;
+        }
+    }
+
+    // --- UriGettable --- //
+    public URI getUri() { return entry.getUri(); }
+
+    // --- ContentSavable --- //
+    public String getFileName() {
+        String path = entry.getPath();
+        int index = path.lastIndexOf('/');
+        return path.substring(index+1);
+    }
+
+    // --- IndexesChangeListener --- //
+    public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        // Update the list of containers
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        // Refresh links
+        boolean refresh = false;
+
+        for (Map.Entry<Integer, HyperlinkData> entry : hyperlinks.entrySet()) {
+            TypeHyperlinkData data = (TypeHyperlinkData)entry.getValue();
+            boolean enabled;
+
+            if (data instanceof PathHyperlinkData) {
+                PathHyperlinkData d = (PathHyperlinkData)data;
+                enabled = searchEntry(this.entry.getContainer().getRoot(), d.path) != null;
+            } else {
+                String internalTypeName = data.internalTypeName;
+                enabled = IndexesUtil.containsInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+            }
+
+            if (data.enabled != enabled) {
+                data.enabled = enabled;
+                refresh = true;
+            }
+        }
+
+        if (refresh) {
+            textArea.repaint();
+        }
+    }
+
+    public static class PathHyperlinkData extends TypeHyperlinkData {
+        public boolean enabled;
+        public String path;
+
+        PathHyperlinkData(int startPosition, int endPosition, String path) {
+            super(startPosition, endPosition, null);
+            this.enabled = false;
+            this.path = path;
+        }
+    }
+
+    protected static List<String> typeHyperlinkPaths = Arrays.asList(
+        "web-app/filter/filter-class",
+        "web-app/listener/listener-class",
+        "web-app/servlet/servlet-class");
+
+    protected static List<String> pathHyperlinkPaths = Arrays.asList(
+        "web-app/jsp-config/taglib/taglib-location",
+        "web-app/welcome-file-list/welcome-file",
+        "web-app/login-config/form-login-config/form-login-page",
+        "web-app/login-config/form-login-config/form-error-page",
+        "web-app/jsp-config/jsp-property-group/include-prelude",
+        "web-app/jsp-config/jsp-property-group/include-coda");
+
+    protected static List<String> hyperlinkPaths = new ArrayList<>(typeHyperlinkPaths.size() + pathHyperlinkPaths.size());
+
+    static {
+        hyperlinkPaths.addAll(typeHyperlinkPaths);
+        hyperlinkPaths.addAll(pathHyperlinkPaths);
+    }
+
+    public class PathFinder extends AbstractXmlPathFinder {
+        public PathFinder() {
+            super(hyperlinkPaths);
+        }
+
+        public void handle(String path, String text, int position) {
+            String trim = text.trim();
+            if (trim != null) {
+                int startIndex = position + text.indexOf(trim);
+                int endIndex = startIndex + trim.length();
+
+                if (pathHyperlinkPaths.contains(path)) {
+                    addHyperlink(new PathHyperlinkData(startIndex, endIndex, trim));
+                } else {
+                    String internalTypeName = trim.replace('.', '/');
+                    addHyperlink(new TypeHyperlinkData(startIndex, endIndex, internalTypeName));
+                }
+            }
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/component/XmlFilePage.java b/services/src/main/java/org/jd/gui/view/component/XmlFilePage.java
new file mode 100644
index 0000000..50c2128
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/component/XmlFilePage.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.component;
+
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.jd.gui.api.API;
+import org.jd.gui.api.feature.IndexesChangeListener;
+import org.jd.gui.api.feature.UriGettable;
+import org.jd.gui.api.model.Container;
+import org.jd.gui.api.model.Indexes;
+import org.jd.gui.util.exception.ExceptionUtil;
+import org.jd.gui.util.index.IndexesUtil;
+import org.jd.gui.util.io.TextReader;
+
+import java.awt.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class XmlFilePage extends TypeReferencePage implements UriGettable, IndexesChangeListener {
+    protected API api;
+    protected Container.Entry entry;
+    protected Collection<Future<Indexes>> collectionOfFutureIndexes;
+
+    public XmlFilePage(API api, Container.Entry entry) {
+        this.api = api;
+        this.entry = entry;
+        // Load content file
+        String text = TextReader.getText(entry.getInputStream());
+        // Create hyperlinks
+        Pattern pattern = Pattern.compile("(?s)<\\s*bean[^<]+class\\s*=\\s*\"([^\"]*)\"");
+        Matcher matcher = pattern.matcher(textArea.getText());
+
+        while (matcher.find()) {
+            // Spring type reference found
+            String value = matcher.group(1);
+            String trim = value.trim();
+
+            if (trim != null) {
+                int startIndex = matcher.start(1) - 1;
+                int endIndex = startIndex + value.length() + 2;
+                String internalTypeName = trim.replace('.', '/');
+                addHyperlink(new TypeHyperlinkData(startIndex, endIndex, internalTypeName));
+            }
+        }
+        // Display
+        setText(text);
+    }
+
+    public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_XML; }
+
+    protected boolean isHyperlinkEnabled(HyperlinkData hyperlinkData) { return ((TypeHyperlinkData)hyperlinkData).enabled; }
+
+    protected void openHyperlink(int x, int y, HyperlinkData hyperlinkData) {
+        TypeHyperlinkData data = (TypeHyperlinkData)hyperlinkData;
+
+        if (data.enabled) {
+            try {
+                // Save current position in history
+                Point location = textArea.getLocationOnScreen();
+                int offset = textArea.viewToModel(new Point(x-location.x, y-location.y));
+                URI uri = entry.getUri();
+                api.addURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), "position=" + offset, null));
+
+                // Open link
+                String internalTypeName = data.internalTypeName;
+                List<Container.Entry> entries = IndexesUtil.findInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+                String rootUri = entry.getContainer().getRoot().getUri().toString();
+                ArrayList<Container.Entry> sameContainerEntries = new ArrayList<>();
+
+                for (Container.Entry entry : entries) {
+                    if (entry.getUri().toString().startsWith(rootUri)) {
+                        sameContainerEntries.add(entry);
+                    }
+                }
+
+                if (sameContainerEntries.size() > 0) {
+                    api.openURI(x, y, sameContainerEntries, null, data.internalTypeName);
+                } else if (entries.size() > 0) {
+                    api.openURI(x, y, entries, null, data.internalTypeName);
+                }
+            } catch (URISyntaxException e) {
+                assert ExceptionUtil.printStackTrace(e);
+            }
+        }
+    }
+
+    // --- UriGettable --- //
+    public URI getUri() { return entry.getUri(); }
+
+    // --- ContentSavable --- //
+    public String getFileName() {
+        String path = entry.getPath();
+        int index = path.lastIndexOf('/');
+        return path.substring(index+1);
+    }
+
+    // --- IndexesChangeListener --- //
+    public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
+        // Update the list of containers
+        this.collectionOfFutureIndexes = collectionOfFutureIndexes;
+        // Refresh links
+        boolean refresh = false;
+
+        for (Map.Entry<Integer, HyperlinkData> entry : hyperlinks.entrySet()) {
+            TypeHyperlinkData data = (TypeHyperlinkData)entry.getValue();
+            String internalTypeName = data.internalTypeName;
+            boolean enabled = IndexesUtil.containsInternalTypeName(collectionOfFutureIndexes, internalTypeName);
+
+            if (data.enabled != enabled) {
+                data.enabled = enabled;
+                refresh = true;
+            }
+        }
+
+        if (refresh) {
+            textArea.repaint();
+        }
+    }
+}
diff --git a/services/src/main/java/org/jd/gui/view/data/TreeNodeBean.java b/services/src/main/java/org/jd/gui/view/data/TreeNodeBean.java
new file mode 100644
index 0000000..4523df6
--- /dev/null
+++ b/services/src/main/java/org/jd/gui/view/data/TreeNodeBean.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2008-2019 Emmanuel Dupuy.
+ * This project is distributed under the GPLv3 license.
+ * This is a Copyleft license that gives the user the right to use,
+ * copy and modify the code freely for non-commercial purposes.
+ */
+
+package org.jd.gui.view.data;
+
+import org.jd.gui.api.model.TreeNodeData;
+
+import javax.swing.*;
+
+public class TreeNodeBean implements TreeNodeData {
+    protected String label;
+    protected String tip;
+    protected Icon icon;
+    protected Icon openIcon;
+
+    public TreeNodeBean(String label, Icon icon) {
+        this.label = label;
+        this.icon = icon;
+    }
+
+    public TreeNodeBean(String label, String tip, Icon icon) {
+        this.label = label;
+        this.tip = tip;
+        this.icon = icon;
+    }
+
+    public TreeNodeBean(String label, Icon icon, Icon openIcon) {
+        this.label = label;
+        this.icon = icon;
+        this.openIcon = openIcon;
+    }
+
+    public TreeNodeBean(String label, String tip, Icon icon, Icon openIcon) {
+        this.label = label;
+        this.tip = tip;
+        this.icon = icon;
+        this.openIcon = openIcon;
+    }
+
+    public void setLabel(String label) {
+        this.label = label;
+    }
+
+    public void setTip(String tip) {
+        this.tip = tip;
+    }
+
+    public void setIcon(Icon icon) {
+        this.icon = icon;
+    }
+
+    public void setOpenIcon(Icon openIcon) {
+        this.openIcon = openIcon;
+    }
+
+    @Override
+    public String getLabel() {
+
+        return label;
+    }
+
+    @Override
+    public String getTip() {
+        return tip;
+    }
+
+    @Override
+    public Icon getIcon() {
+        return icon;
+    }
+
+    @Override
+    public Icon getOpenIcon() {
+        return openIcon;
+    }
+}
diff --git a/services/src/main/resources/META-INF/services/org.jd.gui.spi.ContainerFactory b/services/src/main/resources/META-INF/services/org.jd.gui.spi.ContainerFactory
new file mode 100644
index 0000000..de09b74
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.jd.gui.spi.ContainerFactory
@@ -0,0 +1,7 @@
+# Order is important : 'GenericContainerFactoryProvider' must be the last
+org.jd.gui.service.container.KarContainerFactoryProvider
+org.jd.gui.service.container.JavaModuleContainerFactoryProvider
+org.jd.gui.service.container.EarContainerFactoryProvider
+org.jd.gui.service.container.WarContainerFactoryProvider
+org.jd.gui.service.container.JarContainerFactoryProvider
+org.jd.gui.service.container.GenericContainerFactoryProvider
diff --git a/services/src/main/resources/META-INF/services/org.jd.gui.spi.ContextualActionsFactory b/services/src/main/resources/META-INF/services/org.jd.gui.spi.ContextualActionsFactory
new file mode 100644
index 0000000..f2a8242
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.jd.gui.spi.ContextualActionsFactory
@@ -0,0 +1 @@
+org.jd.gui.service.actions.CopyQualifiedNameContextualActionsFactory
diff --git a/services/src/main/resources/META-INF/services/org.jd.gui.spi.FileLoader b/services/src/main/resources/META-INF/services/org.jd.gui.spi.FileLoader
new file mode 100644
index 0000000..8c2851b
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.jd.gui.spi.FileLoader
@@ -0,0 +1,10 @@
+org.jd.gui.service.fileloader.AarFileLoaderProvider
+org.jd.gui.service.fileloader.ClassFileLoaderProvider
+org.jd.gui.service.fileloader.EarFileLoaderProvider
+org.jd.gui.service.fileloader.JarFileLoaderProvider
+org.jd.gui.service.fileloader.JavaFileLoaderProvider
+org.jd.gui.service.fileloader.JavaModuleFileLoaderProvider
+org.jd.gui.service.fileloader.KarFileLoaderProvider
+org.jd.gui.service.fileloader.LogFileLoaderProvider
+org.jd.gui.service.fileloader.WarFileLoaderProvider
+org.jd.gui.service.fileloader.ZipFileLoaderProvider
diff --git a/services/src/main/resources/META-INF/services/org.jd.gui.spi.Indexer b/services/src/main/resources/META-INF/services/org.jd.gui.spi.Indexer
new file mode 100644
index 0000000..73259c4
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.jd.gui.spi.Indexer
@@ -0,0 +1,12 @@
+org.jd.gui.service.indexer.DirectoryIndexerProvider
+org.jd.gui.service.indexer.ClassFileIndexerProvider
+org.jd.gui.service.indexer.EjbJarXmlFileIndexerProvider
+org.jd.gui.service.indexer.JavaFileIndexerProvider
+org.jd.gui.service.indexer.JavaModuleFileIndexerProvider
+org.jd.gui.service.indexer.JavaModuleInfoFileIndexerProvider
+org.jd.gui.service.indexer.MetainfServiceFileIndexerProvider
+org.jd.gui.service.indexer.TextFileIndexerProvider
+org.jd.gui.service.indexer.WebXmlFileIndexerProvider
+org.jd.gui.service.indexer.ZipFileIndexerProvider
+org.jd.gui.service.indexer.XmlBasedFileIndexerProvider
+org.jd.gui.service.indexer.XmlFileIndexerProvider
diff --git a/services/src/main/resources/META-INF/services/org.jd.gui.spi.PasteHandler b/services/src/main/resources/META-INF/services/org.jd.gui.spi.PasteHandler
new file mode 100644
index 0000000..b6d3e57
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.jd.gui.spi.PasteHandler
@@ -0,0 +1 @@
+org.jd.gui.service.pastehandler.LogPasteHandler
diff --git a/services/src/main/resources/META-INF/services/org.jd.gui.spi.PreferencesPanel b/services/src/main/resources/META-INF/services/org.jd.gui.spi.PreferencesPanel
new file mode 100644
index 0000000..9fc5fcd
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.jd.gui.spi.PreferencesPanel
@@ -0,0 +1,5 @@
+org.jd.gui.service.preferencespanel.DirectoryIndexerPreferencesProvider
+org.jd.gui.service.preferencespanel.ClassFileSaverPreferencesProvider
+org.jd.gui.service.preferencespanel.ClassFileDecompilerPreferencesProvider
+org.jd.gui.service.preferencespanel.ViewerPreferencesProvider
+org.jd.gui.service.preferencespanel.MavenOrgSourceLoaderPreferencesProvider
diff --git a/services/src/main/resources/META-INF/services/org.jd.gui.spi.SourceLoader b/services/src/main/resources/META-INF/services/org.jd.gui.spi.SourceLoader
new file mode 100644
index 0000000..d9ed447
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.jd.gui.spi.SourceLoader
@@ -0,0 +1 @@
+org.jd.gui.service.sourceloader.MavenOrgSourceLoaderProvider
diff --git a/services/src/main/resources/META-INF/services/org.jd.gui.spi.SourceSaver b/services/src/main/resources/META-INF/services/org.jd.gui.spi.SourceSaver
new file mode 100644
index 0000000..f2e0eb6
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.jd.gui.spi.SourceSaver
@@ -0,0 +1,5 @@
+org.jd.gui.service.sourcesaver.ClassFileSourceSaverProvider
+org.jd.gui.service.sourcesaver.DirectorySourceSaverProvider
+org.jd.gui.service.sourcesaver.FileSourceSaverProvider
+org.jd.gui.service.sourcesaver.PackageSourceSaverProvider
+org.jd.gui.service.sourcesaver.ZipFileSourceSaverProvider
diff --git a/services/src/main/resources/META-INF/services/org.jd.gui.spi.TreeNodeFactory b/services/src/main/resources/META-INF/services/org.jd.gui.spi.TreeNodeFactory
new file mode 100644
index 0000000..701e2e2
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.jd.gui.spi.TreeNodeFactory
@@ -0,0 +1,34 @@
+org.jd.gui.service.treenode.ClassesDirectoryTreeNodeFactoryProvider
+org.jd.gui.service.treenode.ClassFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.CssFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.DirectoryTreeNodeFactoryProvider
+org.jd.gui.service.treenode.DtdFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.EarFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.EjbJarXmlFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.FileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.HtmlFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.JarFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.JavaFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.JavascriptFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.JavaModuleFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.JavaModulePackageTreeNodeFactoryProvider
+org.jd.gui.service.treenode.JsonFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.JspFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.KarFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.ManifestFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.MetainfDirectoryTreeNodeFactoryProvider
+org.jd.gui.service.treenode.MetainfServiceFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.ModuleInfoFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.PackageTreeNodeFactoryProvider
+org.jd.gui.service.treenode.PropertiesFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.SqlFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.SpiFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.TextFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.WarFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.WarPackageTreeNodeFactoryProvider
+org.jd.gui.service.treenode.WebinfLibDirectoryTreeNodeFactoryProvider
+org.jd.gui.service.treenode.WebXmlFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.XmlBasedFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.XmlFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.ZipFileTreeNodeFactoryProvider
+org.jd.gui.service.treenode.ImageFileTreeNodeFactoryProvider
diff --git a/services/src/main/resources/META-INF/services/org.jd.gui.spi.TypeFactory b/services/src/main/resources/META-INF/services/org.jd.gui.spi.TypeFactory
new file mode 100644
index 0000000..31797b0
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.jd.gui.spi.TypeFactory
@@ -0,0 +1,2 @@
+org.jd.gui.service.type.ClassFileTypeFactoryProvider
+org.jd.gui.service.type.JavaFileTypeFactoryProvider
diff --git a/services/src/main/resources/META-INF/services/org.jd.gui.spi.UriLoader b/services/src/main/resources/META-INF/services/org.jd.gui.spi.UriLoader
new file mode 100644
index 0000000..8a74f94
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.jd.gui.spi.UriLoader
@@ -0,0 +1 @@
+org.jd.gui.service.uriloader.FileUriLoaderProvider
diff --git a/services/src/main/resources/org/jd/gui/images/abstract_ovr.png b/services/src/main/resources/org/jd/gui/images/abstract_ovr.png
new file mode 100644
index 0000000..658b8fd
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/abstract_ovr.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/annotation_obj.png b/services/src/main/resources/org/jd/gui/images/annotation_obj.png
new file mode 100644
index 0000000..38d5eab
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/annotation_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/archivefolder_obj.png b/services/src/main/resources/org/jd/gui/images/archivefolder_obj.png
new file mode 100644
index 0000000..8d653f6
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/archivefolder_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/ascii_obj.png b/services/src/main/resources/org/jd/gui/images/ascii_obj.png
new file mode 100644
index 0000000..4e9dc38
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/ascii_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/class_default_obj.png b/services/src/main/resources/org/jd/gui/images/class_default_obj.png
new file mode 100644
index 0000000..078a164
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/class_default_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/class_obj.png b/services/src/main/resources/org/jd/gui/images/class_obj.png
new file mode 100644
index 0000000..b58c84c
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/class_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/class_private_obj.png b/services/src/main/resources/org/jd/gui/images/class_private_obj.png
new file mode 100644
index 0000000..ab22985
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/class_private_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/class_protected_obj.png b/services/src/main/resources/org/jd/gui/images/class_protected_obj.png
new file mode 100644
index 0000000..678aa32
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/class_protected_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/classf_obj.png b/services/src/main/resources/org/jd/gui/images/classf_obj.png
new file mode 100644
index 0000000..6393f81
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/classf_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/constr_ovr.png b/services/src/main/resources/org/jd/gui/images/constr_ovr.png
new file mode 100644
index 0000000..65964a9
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/constr_ovr.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/cpyqual_menu.png b/services/src/main/resources/org/jd/gui/images/cpyqual_menu.png
new file mode 100644
index 0000000..fab5842
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/cpyqual_menu.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/css_obj.png b/services/src/main/resources/org/jd/gui/images/css_obj.png
new file mode 100644
index 0000000..4caa409
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/css_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/dtd_obj.gif b/services/src/main/resources/org/jd/gui/images/dtd_obj.gif
new file mode 100644
index 0000000..64ee536
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/dtd_obj.gif differ
diff --git a/services/src/main/resources/org/jd/gui/images/ear_obj.gif b/services/src/main/resources/org/jd/gui/images/ear_obj.gif
new file mode 100644
index 0000000..4468d66
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/ear_obj.gif differ
diff --git a/services/src/main/resources/org/jd/gui/images/ejbmodule_obj.gif b/services/src/main/resources/org/jd/gui/images/ejbmodule_obj.gif
new file mode 100644
index 0000000..f8b5c0a
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/ejbmodule_obj.gif differ
diff --git a/services/src/main/resources/org/jd/gui/images/enum_obj.png b/services/src/main/resources/org/jd/gui/images/enum_obj.png
new file mode 100644
index 0000000..a9ecedf
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/enum_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/field_default_obj.png b/services/src/main/resources/org/jd/gui/images/field_default_obj.png
new file mode 100644
index 0000000..becfe6b
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/field_default_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/field_private_obj.png b/services/src/main/resources/org/jd/gui/images/field_private_obj.png
new file mode 100644
index 0000000..d3fa87d
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/field_private_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/field_protected_obj.png b/services/src/main/resources/org/jd/gui/images/field_protected_obj.png
new file mode 100644
index 0000000..dc80489
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/field_protected_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/field_public_obj.png b/services/src/main/resources/org/jd/gui/images/field_public_obj.png
new file mode 100644
index 0000000..40a5ef1
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/field_public_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/file-image.gif b/services/src/main/resources/org/jd/gui/images/file-image.gif
new file mode 100644
index 0000000..07e2598
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/file-image.gif differ
diff --git a/services/src/main/resources/org/jd/gui/images/file_plain_obj.png b/services/src/main/resources/org/jd/gui/images/file_plain_obj.png
new file mode 100644
index 0000000..735dd02
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/file_plain_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/final_ovr.png b/services/src/main/resources/org/jd/gui/images/final_ovr.png
new file mode 100644
index 0000000..f20a9c1
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/final_ovr.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/folder.gif b/services/src/main/resources/org/jd/gui/images/folder.gif
new file mode 100644
index 0000000..42e027c
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/folder.gif differ
diff --git a/services/src/main/resources/org/jd/gui/images/folder_open.png b/services/src/main/resources/org/jd/gui/images/folder_open.png
new file mode 100644
index 0000000..9759da4
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/folder_open.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/html_obj.gif b/services/src/main/resources/org/jd/gui/images/html_obj.gif
new file mode 100644
index 0000000..53e7dbd
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/html_obj.gif differ
diff --git a/services/src/main/resources/org/jd/gui/images/inf_obj.png b/services/src/main/resources/org/jd/gui/images/inf_obj.png
new file mode 100644
index 0000000..f9f9c42
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/inf_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/int_default_obj.png b/services/src/main/resources/org/jd/gui/images/int_default_obj.png
new file mode 100644
index 0000000..35241b0
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/int_default_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/int_obj.png b/services/src/main/resources/org/jd/gui/images/int_obj.png
new file mode 100644
index 0000000..745cdea
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/int_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/int_private_obj.png b/services/src/main/resources/org/jd/gui/images/int_private_obj.png
new file mode 100644
index 0000000..f36c8b5
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/int_private_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/int_protected_obj.png b/services/src/main/resources/org/jd/gui/images/int_protected_obj.png
new file mode 100644
index 0000000..5e5639d
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/int_protected_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/jar_obj.png b/services/src/main/resources/org/jd/gui/images/jar_obj.png
new file mode 100644
index 0000000..6124fd2
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/jar_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/java_obj.png b/services/src/main/resources/org/jd/gui/images/java_obj.png
new file mode 100644
index 0000000..dceae72
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/java_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/jcu_obj.png b/services/src/main/resources/org/jd/gui/images/jcu_obj.png
new file mode 100644
index 0000000..dceae72
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/jcu_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/js_obj.png b/services/src/main/resources/org/jd/gui/images/js_obj.png
new file mode 100644
index 0000000..6f6fa6a
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/js_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/manifest_obj.png b/services/src/main/resources/org/jd/gui/images/manifest_obj.png
new file mode 100644
index 0000000..8ec970c
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/manifest_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/methdef_obj.png b/services/src/main/resources/org/jd/gui/images/methdef_obj.png
new file mode 100644
index 0000000..a4131b9
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/methdef_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/methpri_obj.png b/services/src/main/resources/org/jd/gui/images/methpri_obj.png
new file mode 100644
index 0000000..57dbb76
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/methpri_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/methpro_obj.png b/services/src/main/resources/org/jd/gui/images/methpro_obj.png
new file mode 100644
index 0000000..3e4c0d7
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/methpro_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/methpub_obj.png b/services/src/main/resources/org/jd/gui/images/methpub_obj.png
new file mode 100644
index 0000000..3004195
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/methpub_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/minus.png b/services/src/main/resources/org/jd/gui/images/minus.png
new file mode 100644
index 0000000..df26d9e
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/minus.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/module_obj.png b/services/src/main/resources/org/jd/gui/images/module_obj.png
new file mode 100644
index 0000000..300adac
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/module_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/package_obj.png b/services/src/main/resources/org/jd/gui/images/package_obj.png
new file mode 100644
index 0000000..922c435
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/package_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/packagefolder_obj.png b/services/src/main/resources/org/jd/gui/images/packagefolder_obj.png
new file mode 100644
index 0000000..93053b7
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/packagefolder_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/packd_obj.png b/services/src/main/resources/org/jd/gui/images/packd_obj.png
new file mode 100644
index 0000000..aa7b6a6
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/packd_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/plus.png b/services/src/main/resources/org/jd/gui/images/plus.png
new file mode 100644
index 0000000..744f799
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/plus.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/sql_obj.png b/services/src/main/resources/org/jd/gui/images/sql_obj.png
new file mode 100644
index 0000000..1bbf8b1
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/sql_obj.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/static_ovr.png b/services/src/main/resources/org/jd/gui/images/static_ovr.png
new file mode 100644
index 0000000..3c721da
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/static_ovr.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/th_showqualified.png b/services/src/main/resources/org/jd/gui/images/th_showqualified.png
new file mode 100644
index 0000000..702c329
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/th_showqualified.png differ
diff --git a/services/src/main/resources/org/jd/gui/images/war_obj.gif b/services/src/main/resources/org/jd/gui/images/war_obj.gif
new file mode 100644
index 0000000..31ecb7a
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/war_obj.gif differ
diff --git a/services/src/main/resources/org/jd/gui/images/xml_obj.gif b/services/src/main/resources/org/jd/gui/images/xml_obj.gif
new file mode 100644
index 0000000..4083e21
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/xml_obj.gif differ
diff --git a/services/src/main/resources/org/jd/gui/images/zip_obj.png b/services/src/main/resources/org/jd/gui/images/zip_obj.png
new file mode 100644
index 0000000..af4d58f
Binary files /dev/null and b/services/src/main/resources/org/jd/gui/images/zip_obj.png differ
diff --git a/services/src/main/resources/rsyntaxtextarea/RSyntaxTextArea_License.txt b/services/src/main/resources/rsyntaxtextarea/RSyntaxTextArea_License.txt
new file mode 100644
index 0000000..f0f2d4c
--- /dev/null
+++ b/services/src/main/resources/rsyntaxtextarea/RSyntaxTextArea_License.txt
@@ -0,0 +1,24 @@
+Copyright (c) 2012, Robert Futrell
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of the author nor the names of its contributors may
+      be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/services/src/main/resources/rsyntaxtextarea/themes/eclipse.xml b/services/src/main/resources/rsyntaxtextarea/themes/eclipse.xml
new file mode 100644
index 0000000..200d7a1
--- /dev/null
+++ b/services/src/main/resources/rsyntaxtextarea/themes/eclipse.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE RSyntaxTheme SYSTEM "theme.dtd">
+
+<!--
+	Theme that mimics Eclipse's defaults.
+	See theme.dtd and org.fife.ui.rsyntaxtextarea.Theme for more information.
+-->
+<RSyntaxTheme version="1.0">
+
+   <!-- Omitting baseFont will use a system-appropriate monospaced. -->
+   <!--<baseFont family="..." size="13"/>-->
+    <baseFont size="12"/>
+   
+   <!--  General editor colors. -->
+   <background color="ffffff"/>
+   <caret color="000000"/>
+   <selection bg="default" fg="default"/>
+   <currentLineHighlight color="e8f2fe" fade="false"/>
+   <marginLine fg="b0b4b9"/>
+   <markAllHighlight color="6b8189"/> <!-- TODO: Fix me -->
+   <markOccurrencesHighlight color="d4d4d4" border="false"/>
+   <matchedBracket fg="c0c0c0" highlightBoth="false" animate="false"/>
+   <hyperlinks fg="0000ff"/>
+   <secondaryLanguages>
+      <language index="1" bg="fff0cc"/>
+      <language index="2" bg="dafeda"/>
+      <language index="3" bg="ffe0f0"/>
+   </secondaryLanguages>
+   
+   <!-- Gutter styling. -->
+   <gutterBorder color="dddddd"/>
+   <lineNumbers fg="787878"/>
+   <foldIndicator fg="808080" iconBg="ffffff"/>
+   <iconRowHeader activeLineRange="3399ff"/>
+   
+   <!-- Syntax tokens. -->
+   <tokenStyles>
+      <style token="IDENTIFIER" fg="000000"/>
+      <style token="RESERVED_WORD" fg="7f0055" bold="true"/>
+      <style token="RESERVED_WORD_2" fg="7f0055" bold="true"/>
+      <style token="ANNOTATION" fg="808080"/>
+      <style token="COMMENT_DOCUMENTATION" fg="3f5fbf"/>
+      <style token="COMMENT_EOL" fg="3f7f5f"/>
+      <style token="COMMENT_MULTILINE" fg="3f7f5f"/>
+      <style token="COMMENT_KEYWORD" fg="7F9FBF" bold="true"/>
+      <style token="COMMENT_MARKUP" fg="7f7f9f"/>
+      <style token="DATA_TYPE" fg="7f0055" bold="true"/>
+      <style token="FUNCTION" fg="000000"/>
+      <style token="LITERAL_BOOLEAN" fg="7f0055" bold="true"/>
+      <style token="LITERAL_NUMBER_DECIMAL_INT" fg="000000"/>
+      <style token="LITERAL_NUMBER_FLOAT" fg="000000"/>
+      <style token="LITERAL_NUMBER_HEXADECIMAL" fg="000000"/>
+      <style token="LITERAL_STRING_DOUBLE_QUOTE" fg="2900ff"/>
+      <style token="LITERAL_CHAR" fg="2900ff"/>
+      <style token="LITERAL_BACKQUOTE" fg="2900ff"/>
+      <style token="MARKUP_TAG_DELIMITER" fg="008080"/>
+      <style token="MARKUP_TAG_NAME" fg="3f7f7f"/>
+      <style token="MARKUP_TAG_ATTRIBUTE" fg="7f007f"/>
+      <style token="MARKUP_TAG_ATTRIBUTE_VALUE" fg="2a00ff" italic="true"/>
+      <style token="MARKUP_COMMENT" fg="3f5fbf"/>
+      <style token="MARKUP_DTD" fg="008080"/>
+      <style token="MARKUP_PROCESSING_INSTRUCTION" fg="808080"/>
+      <style token="MARKUP_CDATA" fg="000000"/>
+      <style token="MARKUP_CDATA_DELIMITER" fg="008080"/>
+      <style token="MARKUP_ENTITY_REFERENCE" fg="2a00ff"/>
+      <style token="OPERATOR" fg="000000"/>
+      <style token="PREPROCESSOR" fg="808080"/>
+      <style token="REGEX" fg="008040"/>
+      <style token="SEPARATOR" fg="000000"/>
+      <style token="VARIABLE" fg="ff9900" bold="true"/>
+      <style token="WHITESPACE" fg="000000"/>
+      
+      <style token="ERROR_IDENTIFIER" fg="000000" bg="ffcccc"/>
+      <style token="ERROR_NUMBER_FORMAT" fg="000000" bg="ffcccc"/>
+      <style token="ERROR_STRING_DOUBLE" fg="000000" bg="ffcccc"/>
+      <style token="ERROR_CHAR" fg="000000" bg="ffcccc"/>
+   </tokenStyles>
+
+</RSyntaxTheme>
diff --git a/services/src/test/java/org/jd/gui/util/matcher/DescriptorMatcherTest.java b/services/src/test/java/org/jd/gui/util/matcher/DescriptorMatcherTest.java
new file mode 100644
index 0000000..b67bb7e
--- /dev/null
+++ b/services/src/test/java/org/jd/gui/util/matcher/DescriptorMatcherTest.java
@@ -0,0 +1,84 @@
+package org.jd.gui.util.matcher;
+
+import junit.framework.TestCase;
+import org.junit.Assert;
+
+public class DescriptorMatcherTest extends TestCase {
+    public void testMatchFieldDescriptors() {
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("?", "?"));
+
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("I", "I"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("?", "I"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("I", "?"));
+
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("Ltest/Test;", "Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("?", "Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("Ltest/Test;", "?"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("L*/Test;", "Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("Ltest/Test;", "L*/Test;"));
+
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("L*/Test;", "L*/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("?", "L*/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("L*/Test;", "?"));
+
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("[Z", "[Z"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("[Z", "?"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("?", "[Z"));
+
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("Ltest/Test;", "Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("Ltest/Test;", "?"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("?", "Ltest/Test;"));
+
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("[[[Ltest/Test;", "[[[Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("[[[Ltest/Test;", "?"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("?", "[[[Ltest/Test;"));
+
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("[[[L*/Test;", "[[[L*/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("[[[L*/Test;", "?"));
+        Assert.assertTrue(DescriptorMatcher.matchFieldDescriptors("?", "[[[L*/Test;"));
+    }
+
+    public void testMatchMethodDescriptors() {
+        Assert.assertFalse(DescriptorMatcher.matchMethodDescriptors("I", "I"));
+
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("()I", "()I"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(*)?", "()I"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("()I", "(*)?"));
+
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(I)I", "(I)I"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(*)?", "(I)I"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(I)I", "(*)?"));
+
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(IJ)I", "(IJ)I"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(*)?", "(IJ)I"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(IJ)I", "(*)?"));
+
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(Ltest/Test;)Ltest/Test;", "(Ltest/Test;)Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(*)?", "(Ltest/Test;)Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(Ltest/Test;)Ltest/Test;", "(*)?"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("([[Ltest/Test;[[Ltest/Test;)Ltest/Test;", "([[L*/Test;[[L*/Test;)L*/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("([[L*/Test;[[L*/Test;)L*/Test;", "([[Ltest/Test;[[Ltest/Test;)Ltest/Test;"));
+
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(Ltest/Test;Ltest/Test;)Ltest/Test;", "(Ltest/Test;Ltest/Test;)Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(*)?", "(Ltest/Test;Ltest/Test;)Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(Ltest/Test;Ltest/Test;)Ltest/Test;", "(*)?"));
+
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("([[Ltest/Test;[[Ltest/Test;)Ltest/Test;", "([[Ltest/Test;[[Ltest/Test;)Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(*)?", "([[Ltest/Test;[[Ltest/Test;)Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("([[Ltest/Test;[[Ltest/Test;)Ltest/Test;", "(*)?"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("([[L*/Test;[[L*/Test;)L*/Test;", "([[Ltest/Test;[[Ltest/Test;)Ltest/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("([[Ltest/Test;[[Ltest/Test;)Ltest/Test;", "([[L*/Test;[[L*/Test;)L*/Test;"));
+
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(L*/Test;)L*/Test;", "(L*/Test;)L*/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(*)?", "(L*/Test;)L*/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(L*/Test;)L*/Test;", "(*)?"));
+
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(L*/Test;L*/Test;)L*/Test;", "(L*/Test;L*/Test;)L*/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(*)?", "(L*/Test;L*/Test;)L*/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(Ltest/Test;Ltest/Test;)Ltest/Test;", "(*)?"));
+
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("([[L*/Test;[[L*/Test;)L*/Test;", "([[L*/Test;[[L*/Test;)L*/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("(*)?", "([[L*/Test;[[L*/Test;)L*/Test;"));
+        Assert.assertTrue(DescriptorMatcher.matchMethodDescriptors("([[L*/Test;[[L*/Test;)L*/Test;", "(*)?"));
+    }
+}
diff --git a/services/src/test/java/org/jd/gui/view/component/ClassFilePageTest.java b/services/src/test/java/org/jd/gui/view/component/ClassFilePageTest.java
new file mode 100644
index 0000000..449e5bf
--- /dev/null
+++ b/services/src/test/java/org/jd/gui/view/component/ClassFilePageTest.java
@@ -0,0 +1,136 @@
+package org.jd.gui.view.component;
+
+import junit.framework.TestCase;
+import org.fife.ui.rsyntaxtextarea.DocumentRange;
+import org.junit.Assert;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.TreeMap;
+
+public class ClassFilePageTest extends TestCase {
+
+    public HashMap<String, TypePage.DeclarationData> initDeclarations() {
+        TypePage.DeclarationData data = new TypePage.DeclarationData(0, 1, "Test", "test", "I");
+        HashMap<String, TypePage.DeclarationData> declarations = new HashMap<>();
+
+        // Init type declarations
+        declarations.put("Test", data);
+        declarations.put("test/Test", data);
+
+        // Init field declarations
+        declarations.put("Test-attributeInt-I", data);
+        declarations.put("Test-attributeBoolean-Z", data);
+        declarations.put("Test-attributeArrayBoolean-[[Z", data);
+        declarations.put("Test-attributeString-Ljava/lang/String;", data);
+
+        declarations.put("test/Test-attributeInt-I", data);
+        declarations.put("test/Test-attributeBoolean-Z", data);
+        declarations.put("test/Test-attributeArrayBoolean-[[Z", data);
+        declarations.put("test/Test-attributeString-Ljava/lang/String;", data);
+
+        // Init method declarations
+        declarations.put("Test-getInt-()I", data);
+        declarations.put("Test-getString-()Ljava/lang/String;", data);
+        declarations.put("Test-add-(JJ)J", data);
+        declarations.put("Test-createBuffer-(I)[C", data);
+
+        declarations.put("test/Test-getInt-()I", data);
+        declarations.put("test/Test-getString-()Ljava/lang/String;", data);
+        declarations.put("test/Test-add-(JJ)J", data);
+        declarations.put("test/Test-createBuffer-(I)[C", data);
+
+        return declarations;
+    }
+
+    public TreeMap<Integer, HyperlinkPage.HyperlinkData> initHyperlinks() {
+        TreeMap<Integer, HyperlinkPage.HyperlinkData> hyperlinks = new TreeMap<>();
+
+        hyperlinks.put(0, new TypePage.HyperlinkReferenceData(0, 1, new TypePage.ReferenceData("java/lang/Integer", "MAX_VALUE", "I", "Test")));
+        hyperlinks.put(1, new TypePage.HyperlinkReferenceData(0, 1, new TypePage.ReferenceData("java/lang/Integer", "toString", "()Ljava/lang/String;", "Test")));
+
+        return hyperlinks;
+    }
+
+    public ArrayList<TypePage.StringData> initStrings() {
+        ArrayList<TypePage.StringData> strings = new ArrayList<>();
+
+        strings.add(new TypePage.StringData(0, 3, "abc", "Test"));
+
+        return strings;
+    }
+
+    public void testMatchFragmentAndAddDocumentRange() {
+        HashMap<String, TypePage.DeclarationData> declarations = initDeclarations();
+        ArrayList<DocumentRange> ranges = new ArrayList<>();
+
+        ranges.clear();
+        ClassFilePage.matchFragmentAndAddDocumentRange("Test-attributeBoolean-Z", declarations, ranges);
+        Assert.assertTrue(ranges.size() == 1);
+
+        ranges.clear();
+        ClassFilePage.matchFragmentAndAddDocumentRange("test/Test-attributeBoolean-Z", declarations, ranges);
+        Assert.assertTrue(ranges.size() == 1);
+
+        ranges.clear();
+        ClassFilePage.matchFragmentAndAddDocumentRange("*/Test-attributeBoolean-Z", declarations, ranges);
+        Assert.assertTrue(ranges.size() == 2);
+
+        ranges.clear();
+        ClassFilePage.matchFragmentAndAddDocumentRange("Test-createBuffer-(I)[C", declarations, ranges);
+        Assert.assertTrue(ranges.size() == 1);
+
+        ranges.clear();
+        ClassFilePage.matchFragmentAndAddDocumentRange("test/Test-createBuffer-(I)[C", declarations, ranges);
+        Assert.assertTrue(ranges.size() == 1);
+
+        ranges.clear();
+        ClassFilePage.matchFragmentAndAddDocumentRange("*/Test-getString-(*)?", declarations, ranges);
+        Assert.assertTrue(ranges.size() == 2);
+
+        ranges.clear();
+        ClassFilePage.matchFragmentAndAddDocumentRange("test/Test-add-(?J)?", declarations, ranges);
+        Assert.assertTrue(ranges.size() == 1);
+    }
+
+    public void testMatchQueryAndAddDocumentRange() {
+        HashMap<String, String> parameters = new HashMap<>();
+        HashMap<String, TypePage.DeclarationData> declarations = initDeclarations();
+        TreeMap<Integer, HyperlinkPage.HyperlinkData> hyperlinks = initHyperlinks();
+        ArrayList<TypePage.StringData> strings = initStrings();
+        ArrayList<DocumentRange> ranges = new ArrayList<>();
+
+        parameters.put("highlightPattern", "ab");
+        parameters.put("highlightFlags", "s");
+
+        parameters.put("highlightScope", null);
+        ranges.clear();
+        ClassFilePage.matchQueryAndAddDocumentRange(parameters, declarations, hyperlinks, strings, ranges);
+        Assert.assertTrue(ranges.size() == 1);
+
+        parameters.put("highlightScope", "");
+        ranges.clear();
+        ClassFilePage.matchQueryAndAddDocumentRange(parameters, declarations, hyperlinks, strings, ranges);
+        Assert.assertTrue(ranges.size() == 1);
+
+        parameters.put("highlightScope", "Test");
+        ranges.clear();
+        ClassFilePage.matchQueryAndAddDocumentRange(parameters, declarations, hyperlinks, strings, ranges);
+        Assert.assertTrue(ranges.size() == 1);
+    }
+
+    public void testMatchScope() {
+        Assert.assertTrue(ClassFilePage.matchScope(null, "java/lang/String"));
+        Assert.assertTrue(ClassFilePage.matchScope("", "java/lang/String"));
+
+        Assert.assertTrue(ClassFilePage.matchScope("java/lang/String", "java/lang/String"));
+        Assert.assertTrue(ClassFilePage.matchScope("*/lang/String", "java/lang/String"));
+        Assert.assertTrue(ClassFilePage.matchScope("*/String", "java/lang/String"));
+
+        Assert.assertTrue(ClassFilePage.matchScope(null, "Test"));
+        Assert.assertTrue(ClassFilePage.matchScope("", "Test"));
+
+        Assert.assertTrue(ClassFilePage.matchScope("Test", "Test"));
+        Assert.assertTrue(ClassFilePage.matchScope("*/Test", "Test"));
+    }
+}
diff --git a/services/src/test/java/org/jd/gui/view/component/JavaFilePageTest.java b/services/src/test/java/org/jd/gui/view/component/JavaFilePageTest.java
new file mode 100644
index 0000000..b1169a7
--- /dev/null
+++ b/services/src/test/java/org/jd/gui/view/component/JavaFilePageTest.java
@@ -0,0 +1,57 @@
+package org.jd.gui.view.component;
+
+import junit.framework.TestCase;
+
+import java.util.HashMap;
+
+public class JavaFilePageTest extends TestCase {
+
+    public HashMap<String, TypePage.DeclarationData> initDeclarations() {
+        TypePage.DeclarationData data = new TypePage.DeclarationData(0, 1, "Test", "test", "I");
+        HashMap<String, TypePage.DeclarationData> declarations = new HashMap<>();
+
+        // Init type declarations
+        declarations.put("Test", data);
+        declarations.put("test/Test", data);
+        declarations.put("*/Test", data);
+
+        // Init field declarations
+        declarations.put("Test-attributeInt-I", data);
+        declarations.put("Test-attributeBoolean-Z", data);
+        declarations.put("Test-attributeArrayBoolean-[[Z", data);
+        declarations.put("Test-attributeString-Ljava/lang/String;", data);
+
+        declarations.put("test/Test-attributeInt-I", data);
+        declarations.put("test/Test-attributeBoolean-Z", data);
+        declarations.put("test/Test-attributeArrayBoolean-[[Z", data);
+        declarations.put("test/Test-attributeString-Ljava/lang/String;", data);
+
+        declarations.put("*/Test-attributeBoolean-?", data);
+        declarations.put("*/Test-attributeBoolean-Z", data);
+        declarations.put("test/Test-attributeBoolean-?", data);
+
+        // Init method declarations
+        declarations.put("*/Test-getInt-()I", data);
+        declarations.put("*/Test-getString-()Ljava/lang/String;", data);
+        declarations.put("*/Test-add-(JJ)J", data);
+        declarations.put("*/Test-createBuffer-(I)[C", data);
+
+        declarations.put("test/Test-getInt-(*)?", data);
+        declarations.put("test/Test-getString-(*)?", data);
+        declarations.put("test/Test-add-(*)?", data);
+        declarations.put("test/Test-createBuffer-(*)?", data);
+
+        declarations.put("*/Test-getInt-(*)?", data);
+        declarations.put("*/Test-getString-(*)?", data);
+        declarations.put("*/Test-add-(*)?", data);
+        declarations.put("*/Test-createBuffer-(*)?", data);
+
+        return declarations;
+    }
+
+    public void testMatchFragmentAndAddDocumentRange() {}
+
+    public void testMatchQueryAndAddDocumentRange() {}
+
+    public void testMatchScope() {}
+}
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..6a2daca
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,4 @@
+include 'api', 'app', 'services'
+
+rootProject.name='jd-gui'
+
diff --git a/src/launch4j/resources/images/jd-gui.ico b/src/launch4j/resources/images/jd-gui.ico
new file mode 100644
index 0000000..34c5acb
Binary files /dev/null and b/src/launch4j/resources/images/jd-gui.ico differ
diff --git a/jd-gui.desktop b/src/linux/resources/jd-gui.desktop
old mode 100755
new mode 100644
similarity index 87%
rename from jd-gui.desktop
rename to src/linux/resources/jd-gui.desktop
index 23a41bb..6ba8b03
--- a/jd-gui.desktop
+++ b/src/linux/resources/jd-gui.desktop
@@ -1,8 +1,9 @@
-[Desktop Entry]
-Comment=Java Decompiler JD-GUI
-Terminal=false
-Name=JD-GUI
-Exec=java -jar /opt/jd-gui/jd-gui.jar
-Type=Application
-Icon=jd-gui
-MimeType=application/java;application/java-vm;application/java-archive
+[Desktop Entry]
+Comment=Java Decompiler JD-GUI
+Terminal=false
+Name=JD-GUI
+Exec=java -jar /opt/jd-gui/jd-gui.jar
+Type=Application
+Icon=jd-gui
+MimeType=application/java;application/java-vm;application/java-archive
+StartupWMClass=org-jd-gui-App
diff --git a/src/linux/resources/jd_icon_128.png b/src/linux/resources/jd_icon_128.png
new file mode 100644
index 0000000..b29e5a9
Binary files /dev/null and b/src/linux/resources/jd_icon_128.png differ
diff --git a/src/osx/dist/JD-GUI.app/Contents/Resources/jd-gui.icns b/src/osx/dist/JD-GUI.app/Contents/Resources/jd-gui.icns
new file mode 100644
index 0000000..9ef3cc2
Binary files /dev/null and b/src/osx/dist/JD-GUI.app/Contents/Resources/jd-gui.icns differ
diff --git a/src/osx/resources/Info.plist b/src/osx/resources/Info.plist
new file mode 100644
index 0000000..264b3fd
--- /dev/null
+++ b/src/osx/resources/Info.plist
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<key>CFBundleDevelopmentRegion</key>		<string>English</string>
+		<key>CFBundleExecutable</key>				<string>universalJavaApplicationStub.sh</string>
+		<key>CFBundleName</key>						<string>JD-GUI</string>
+		<key>CFBundleGetInfoString</key>			<string>JD-GUI version ${VERSION}, Copyright 2008, 2019 Emmanuel Dupuy</string>
+		<key>CFBundleIconFile</key>					<string>jd-gui.icns</string>
+		<key>CFBundleIdentifier</key>				<string>jd.jd-gui</string>
+		<key>CFBundleInfoDictionaryVersion</key>	<string>6.0</string>
+		<key>CFBundlePackageType</key>				<string>APPL</string>
+		<key>CFBundleLongVersionString</key>		<string>${VERSION}, Copyright 2008, 2019 Emmanuel Dupuy</string>
+		<key>CFBundleShortVersionString</key>		<string>${VERSION}</string>
+		<key>CSResourcesFileMapped</key>			<true/>
+		<key>LSRequiresCarbon</key>					<true/>
+		<key>NSHumanReadableCopyright</key>			<string>Copyright 2008, 2019 Emmanuel Dupuy</string>
+		<key>NSPrincipalClass</key>					<string>NSApplication</string>
+		<key>NSHighResolutionCapable</key>			<true/>
+		<key>CFBundleDocumentTypes</key>
+		<array>
+			<dict>
+				<key>CFBundleTypeExtensions</key>
+				<array>
+					<string>class</string>
+				</array>
+				<key>CFBundleTypeMIMETypes</key>
+				<array>
+					<string>application/java</string>
+				</array>
+				<key>CFBundleTypeRole</key>			<string>Viewer</string>
+				<key>CFBundleTypeName</key>			<string>Class File</string>
+				<key>LSIsAppleDefaultForType</key>	<true/>
+				<key>LSTypeIsPackage</key>			<false/>
+			</dict>
+			<dict>
+				<key>CFBundleTypeExtensions</key>
+				<array>
+					<string>java</string>
+				</array>
+				<key>CFBundleTypeMIMETypes</key>
+				<array>
+					<string>text/plain</string>
+				</array>
+				<key>CFBundleTypeRole</key>			<string>Viewer</string>
+				<key>CFBundleTypeName</key>			<string>Java File</string>
+				<key>LSIsAppleDefaultForType</key>	<false/>
+				<key>LSTypeIsPackage</key>			<false/>
+			</dict>
+			<dict>
+				<key>CFBundleTypeExtensions</key>
+				<array>
+					<string>jar</string>
+				</array>
+				<key>CFBundleTypeMIMETypes</key>
+				<array>
+					<string>application/java-archive</string>
+				</array>
+				<key>CFBundleTypeName</key>			<string>Jar File</string>
+				<key>CFBundleTypeRole</key>			<string>Viewer</string>
+				<key>LSIsAppleDefaultForType</key>	<false/>
+				<key>LSTypeIsPackage</key>			<false/>
+			</dict>
+			<dict>
+				<key>CFBundleTypeExtensions</key>
+				<array>
+					<string>war</string>
+				</array>
+				<key>CFBundleTypeMIMETypes</key>
+				<array>
+					<string>application/java-archive</string>
+				</array>
+				<key>CFBundleTypeName</key>			<string>War File</string>
+				<key>CFBundleTypeRole</key>			<string>Viewer</string>
+				<key>LSIsAppleDefaultForType</key>	<false/>
+				<key>LSTypeIsPackage</key>			<false/>
+			</dict>
+			<dict>
+				<key>CFBundleTypeExtensions</key>
+				<array>
+					<string>ear</string>
+				</array>
+				<key>CFBundleTypeMIMETypes</key>
+				<array>
+					<string>application/java-archive</string>
+				</array>
+				<key>CFBundleTypeName</key>			<string>Ear File</string>
+				<key>CFBundleTypeRole</key>			<string>Viewer</string>
+				<key>LSIsAppleDefaultForType</key>	<false/>
+				<key>LSTypeIsPackage</key>			<false/>
+			</dict>
+			<dict>
+				<key>CFBundleTypeExtensions</key>
+				<array>
+					<string>aar</string>
+				</array>
+				<key>CFBundleTypeName</key>			<string>Android archive File</string>
+				<key>CFBundleTypeRole</key>			<string>Viewer</string>
+				<key>LSIsAppleDefaultForType</key>	<false/>
+				<key>LSTypeIsPackage</key>			<false/>
+			</dict>
+			<dict>
+				<key>CFBundleTypeExtensions</key>
+				<array>
+					<string>jmod</string>
+				</array>
+				<key>CFBundleTypeMIMETypes</key>
+				<array>
+					<string>application/java-archive</string>
+				</array>
+				<key>CFBundleTypeName</key>			<string>Java module File</string>
+				<key>CFBundleTypeRole</key>			<string>Viewer</string>
+				<key>LSIsAppleDefaultForType</key>	<false/>
+				<key>LSTypeIsPackage</key>			<false/>
+			</dict>
+			<dict>
+				<key>CFBundleTypeExtensions</key>
+				<array>
+					<string>zip</string>
+				</array>
+				<key>CFBundleTypeMIMETypes</key>
+				<array>
+					<string>application/zip</string>
+				</array>
+				<key>CFBundleTypeName</key>			<string>Zip File</string>
+				<key>CFBundleTypeRole</key>			<string>Viewer</string>
+				<key>LSIsAppleDefaultForType</key>	<false/>
+				<key>LSTypeIsPackage</key>			<false/>
+			</dict>
+			<dict>
+				<key>CFBundleTypeExtensions</key>
+				<array>
+					<string>log</string>
+					<string>txt</string>
+				</array>
+				<key>CFBundleTypeMIMETypes</key>
+				<array>
+					<string>text/plain</string>
+				</array>
+				<key>CFBundleTypeName</key>			<string>Log File</string>
+				<key>CFBundleTypeRole</key>			<string>Viewer</string>
+				<key>LSIsAppleDefaultForType</key>	<false/>
+				<key>LSTypeIsPackage</key>			<false/>
+			</dict>
+		</array>
+		<key>JavaX</key>
+		<dict>
+			<key>MainClass</key>					<string>org.jd.gui.OsxApp</string>
+			<key>JVMVersion</key>					<string>1.8+</string>
+			<key>ClassPath</key>					<string>\$JAVAROOT/${JAR}</string>
+			<key>WorkingDirectory</key>				<string>\$JAVAROOT</string>
+			<key>Properties</key>
+			<dict>
+				<key>apple.laf.useScreenMenuBar</key>
+				<string>true</string>
+			</dict>
+			<key>VMOptions</key>					<string>-Xms512m</string>
+		</dict>
+	</dict>
+</plist>
diff --git a/src/osx/resources/universalJavaApplicationStub.sh b/src/osx/resources/universalJavaApplicationStub.sh
new file mode 100644
index 0000000..686db22
--- /dev/null
+++ b/src/osx/resources/universalJavaApplicationStub.sh
@@ -0,0 +1,308 @@
+#!/bin/sh
+##################################################################################
+#                                                                                #
+# universalJavaApplicationStub                                                   #
+#                                                                                #
+#                                                                                #
+# A shellscript JavaApplicationStub for Java Apps on Mac OS X                    #
+# that works with both Apple's and Oracle's plist format.                        #
+#                                                                                #
+# Inspired by Ian Roberts stackoverflow answer                                   #
+# at http://stackoverflow.com/a/17546508/1128689                                 #
+#                                                                                #
+#                                                                                #
+# @author    Tobias Fischer                                                      #
+# @url       https://github.com/tofi86/universalJavaApplicationStub              #
+# @date      2015-05-15                                                          #
+# @version   0.9.0                                                               #
+#                                                                                #
+#                                                                                #
+##################################################################################
+#                                                                                #
+#                                                                                #
+# The MIT License (MIT)                                                          #
+#                                                                                #
+# Copyright (c) 2015 Tobias Fischer                                              #
+#                                                                                #
+# Permission is hereby granted, free of charge, to any person obtaining a copy   #
+# of this software and associated documentation files (the "Software"), to deal  #
+# in the Software without restriction, including without limitation the rights   #
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell      #
+# copies of the Software, and to permit persons to whom the Software is          #
+# furnished to do so, subject to the following conditions:                       #
+#                                                                                #
+# The above copyright notice and this permission notice shall be included in all #
+# copies or substantial portions of the Software.                                #
+#                                                                                #
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR     #
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,       #
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE    #
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER         #
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  #
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  #
+# SOFTWARE.                                                                      #
+#                                                                                #
+##################################################################################
+
+
+
+
+#
+# resolve symlinks
+#####################
+
+PRG=$0
+
+while [ -h "$PRG" ]; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '^.*-> \(.*\)$' 2>/dev/null`
+    if expr "$link" : '^/' 2> /dev/null >/dev/null; then
+        PRG="$link"
+    else
+        PRG="`dirname "$PRG"`/$link"
+    fi
+done
+
+# set the directory abspath of the current shell script
+PROGDIR=`dirname "$PRG"`
+
+
+
+
+#
+# set files and folders
+############################################
+
+# the absolute path of the app package
+cd "$PROGDIR"/../../
+AppPackageFolder=`pwd`
+
+# the base path of the app package
+cd ..
+AppPackageRoot=`pwd`
+
+# set Apple's Java folder
+AppleJavaFolder="${AppPackageFolder}"/Contents/Resources/Java
+
+# set Apple's Resources folder
+AppleResourcesFolder="${AppPackageFolder}"/Contents/Resources
+
+# set Oracle's Java folder
+OracleJavaFolder="${AppPackageFolder}"/Contents/Java
+
+# set Oracle's Resources folder
+OracleResourcesFolder="${AppPackageFolder}"/Contents/Resources
+
+# set path to Info.plist in bundle
+InfoPlistFile="${AppPackageFolder}"/Contents/Info.plist
+
+# set the default JVM Version to a null string
+JVMVersion=""
+
+
+
+#
+# read Info.plist and extract JVM options
+############################################
+
+
+# read the program name from CFBundleName
+CFBundleName=`/usr/libexec/PlistBuddy -c "print :CFBundleName" "${InfoPlistFile}"`
+
+# read the icon file name
+CFBundleIconFile=`/usr/libexec/PlistBuddy -c "print :CFBundleIconFile" "${InfoPlistFile}"`
+
+
+# check Info.plist for Apple style Java keys -> if key :Java is present, parse in apple mode
+/usr/libexec/PlistBuddy -c "print :Java" "${InfoPlistFile}" > /dev/null 2>&1
+exitcode=$?
+JavaKey=":Java"
+
+# if no :Java key is present, check Info.plist for universalJavaApplication style JavaX keys -> if key :JavaX is present, parse in apple mode
+if [ $exitcode -ne 0 ]; then
+	/usr/libexec/PlistBuddy -c "print :JavaX" "${InfoPlistFile}" > /dev/null 2>&1
+	exitcode=$?
+	JavaKey=":JavaX"
+fi
+
+
+# read Info.plist in Apple style if exit code returns 0 (true, :Java key is present)
+if [ $exitcode -eq 0 ]; then
+
+	# set Java and Resources folder
+	JavaFolder="${AppleJavaFolder}"
+	ResourcesFolder="${AppleResourcesFolder}"
+
+	APP_PACKAGE="${AppPackageFolder}"
+	JAVAROOT="${AppleJavaFolder}"
+	USER_HOME="$HOME"
+
+
+	# read the Java WorkingDirectory
+	JVMWorkDir=`/usr/libexec/PlistBuddy -c "print ${JavaKey}:WorkingDirectory" "${InfoPlistFile}" 2> /dev/null | xargs`
+	
+	# set Working Directory based upon Plist info
+	if [[ ! -z ${JVMWorkDir} ]]; then
+		WorkingDirectory="${JVMWorkDir}"
+	else
+		# AppPackageRoot is the standard WorkingDirectory when the script is started
+		WorkingDirectory="${AppPackageRoot}"
+	fi
+
+	# expand variables $APP_PACKAGE, $JAVAROOT, $USER_HOME
+	WorkingDirectory=`eval "echo ${WorkingDirectory}"`
+
+
+	# read the MainClass name
+	JVMMainClass=`/usr/libexec/PlistBuddy -c "print ${JavaKey}:MainClass" "${InfoPlistFile}" 2> /dev/null`
+
+	# read the JVM Options
+	JVMOptions=`/usr/libexec/PlistBuddy -c "print ${JavaKey}:Properties" "${InfoPlistFile}" 2> /dev/null | grep " =" | sed 's/^ */-D/g' | tr '\n' ' ' | sed 's/  */ /g' | sed 's/ = /=/g' | xargs`
+
+	# read StartOnMainThread
+	JVMStartOnMainThread=`/usr/libexec/PlistBuddy -c "print ${JavaKey}:StartOnMainThread" "${InfoPlistFile}" 2> /dev/null`
+	if [ "${JVMStartOnMainThread}" == "true" ]; then
+		echo ${JVMStartOnMainThread} > ~/Desktop/test.txt
+		JVMOptions+=" -XstartOnFirstThread"
+	fi
+
+	# read the ClassPath in either Array or String style
+	JVMClassPath_RAW=`/usr/libexec/PlistBuddy -c "print ${JavaKey}:ClassPath" "${InfoPlistFile}" 2> /dev/null`
+	if [[ $JVMClassPath_RAW == *Array* ]] ; then
+		JVMClassPath=.`/usr/libexec/PlistBuddy -c "print ${JavaKey}:ClassPath" "${InfoPlistFile}" 2> /dev/null | grep "    " | sed 's/^ */:/g' | tr -d '\n' | xargs`
+	else
+		JVMClassPath=${JVMClassPath_RAW}
+	fi
+	# expand variables $APP_PACKAGE, $JAVAROOT, $USER_HOME
+	JVMClassPath=`eval "echo ${JVMClassPath}"`
+
+	# read the JVM Default Options
+	JVMDefaultOptions=`/usr/libexec/PlistBuddy -c "print ${JavaKey}:VMOptions" "${InfoPlistFile}" 2> /dev/null | xargs`
+
+	# read the JVM Arguments
+	JVMArguments=`/usr/libexec/PlistBuddy -c "print ${JavaKey}:Arguments" "${InfoPlistFile}" 2> /dev/null | xargs`
+
+    # read the Java version we want to find
+    JVMVersion=`/usr/libexec/PlistBuddy -c "print ${JavaKey}:JVMVersion" "${InfoPlistFile}" 2> /dev/null | xargs`
+
+# read Info.plist in Oracle style
+else
+
+	# set Working Directory and Java and Resources folder
+	JavaFolder="${OracleJavaFolder}"
+	ResourcesFolder="${OracleResourcesFolder}"
+	WorkingDirectory="${OracleJavaFolder}"
+
+	APP_ROOT="${AppPackageFolder}"
+
+	# read the MainClass name
+	JVMMainClass=`/usr/libexec/PlistBuddy -c "print :JVMMainClassName" "${InfoPlistFile}" 2> /dev/null`
+
+	# read the JVM Options
+	JVMOptions=`/usr/libexec/PlistBuddy -c "print :JVMOptions" "${InfoPlistFile}" 2> /dev/null | grep " -" | tr -d '\n' | sed 's/  */ /g' | xargs`
+	# replace occurances of $APP_ROOT with it's content
+	JVMOptions=`eval "echo ${JVMOptions}"`
+
+	JVMClassPath="${JavaFolder}/*"
+
+	# read the JVM Default Options
+	JVMDefaultOptions=`/usr/libexec/PlistBuddy -c "print :JVMDefaultOptions" "${InfoPlistFile}" 2> /dev/null | grep -o "\-.*" | tr -d '\n' | xargs`
+
+	# read the JVM Arguments
+	JVMArguments=`/usr/libexec/PlistBuddy -c "print :JVMArguments" "${InfoPlistFile}" 2> /dev/null | tr -d '\n' | sed -E 's/Array \{ *(.*) *\}/\1/g' | sed 's/  */ /g' | xargs`
+	# replace occurances of $APP_ROOT with it's content
+	JVMArguments=`eval "echo ${JVMArguments}"`
+fi
+
+
+
+
+#
+# find installed Java versions
+#################################
+
+# first check system variable "$JAVA_HOME"
+if [ -n "$JAVA_HOME" ] ; then
+	JAVACMD="$JAVA_HOME/bin/java"
+	
+# check for specified JVMversion in "/usr/libexec/java_home" symlinks
+elif [ ! -z ${JVMVersion} ] && [ -x /usr/libexec/java_home ] && /usr/libexec/java_home -F; then
+
+	if /usr/libexec/java_home -F -v ${JVMVersion}; then
+		JAVACMD="`/usr/libexec/java_home -F -v ${JVMVersion} 2> /dev/null`/bin/java"
+	else
+		# display error message with applescript
+		osascript -e "tell application \"System Events\" to display dialog \"ERROR launching '${CFBundleName}'\n\nNo suitable Java version found on your system!\nThis program requires Java ${JVMVersion}\nMake sure you install the required Java version.\" with title \"${CFBundleName}\" buttons {\" OK \"} default button 1 with icon path to resource \"${CFBundleIconFile}\" in bundle (path to me)"
+		# exit with error
+		exit 3
+	fi
+
+# otherwise check "/usr/libexec/java_home" symlinks
+elif [ -x /usr/libexec/java_home ] && /usr/libexec/java_home -F; then
+	JAVACMD="`/usr/libexec/java_home 2> /dev/null`/bin/java"
+
+# otherwise check Java standard symlink (old Apple Java)
+elif [ -h /Library/Java/Home ]; then
+	JAVACMD="/Library/Java/Home/bin/java"
+
+# fallback: public JRE plugin (Oracle Java)
+else
+	JAVACMD="/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"
+fi
+
+# fallback fallback: /usr/bin/java
+# but this would prompt to install deprecated Apple Java 6
+
+
+
+
+#
+# execute JAVA commandline and do some pre-checks
+####################################################
+
+# display error message if MainClassName is empty
+if [ -z ${JVMMainClass} ]; then
+	# display error message with applescript
+	osascript -e "tell application \"System Events\" to display dialog \"ERROR launching '${CFBundleName}'!\n\n'MainClass' isn't specified!\nJava application cannot be started!\" with title \"${CFBundleName}\" buttons {\" OK \"} default button 1 with icon path to resource \"${CFBundleIconFile}\" in bundle (path to me)"
+	# exit with error
+	exit 2
+
+
+# check whether $JAVACMD is a file and executable
+elif [ -f "$JAVACMD" ] && [ -x "$JAVACMD" ] ; then
+
+	# enable drag&drop to the dock icon
+	export CFProcessPath="$0"
+
+	# change to Working Directory based upon Apple/Oracle Plist info
+	cd "${WorkingDirectory}"
+
+	# execute Java and set
+	#	- classpath
+	#	- dock icon
+	#	- application name
+	#	- JVM options
+	#	- JVM default options
+	#	- main class
+	#	- JVM arguments
+	exec "$JAVACMD" \
+			-cp "${JVMClassPath}" \
+			-Xdock:icon="${ResourcesFolder}/${CFBundleIconFile}" \
+			-Xdock:name="${CFBundleName}" \
+			${JVMOptions:+$JVMOptions }\
+			${JVMDefaultOptions:+$JVMDefaultOptions }\
+			${JVMMainClass}\
+			${JVMArguments:+ $JVMArguments}
+
+
+else
+
+	# display error message with applescript
+	osascript -e "tell application \"System Events\" to display dialog \"ERROR launching '${CFBundleName}'!\n\nYou need to have JAVA installed on your Mac!\nVisit http://java.com for more information...\" with title \"${CFBundleName}\" buttons {\" OK \"} default button 1 with icon path to resource \"${CFBundleIconFile}\" in bundle (path to me)"
+
+	# and open java.com
+	open http://java.com
+
+	# exit with error
+	exit 1
+fi
diff --git a/src/proguard/resources/proguard.config.txt b/src/proguard/resources/proguard.config.txt
new file mode 100644
index 0000000..2582c5f
--- /dev/null
+++ b/src/proguard/resources/proguard.config.txt
@@ -0,0 +1,40 @@
+# java -jar proguard.jar @proguard.config.txt
+
+#-injars       jd-gui-1.4.2.jar
+#-outjars      jd-gui-1.4.2-min.jar
+#-libraryjars  C:/Program Files/Java/jre1.8.0_121/lib/rt.jar
+#-printmapping myapplication.map
+
+-keep public class org.jd.gui.App {
+    public static void main(java.lang.String[]);
+}
+
+-keep public class org.jd.gui.OsxApp {
+    public static void main(java.lang.String[]);
+}
+
+-dontwarn com.apple.eawt.**
+-keepattributes Signature
+
+-keep public interface org.jd.gui.api.** {*;}
+-keep public interface org.jd.gui.spi.** {*;}
+-keep public class * extends org.jd.gui.spi.*
+
+-keep class org.fife.ui.rsyntaxtextarea.TokenTypes {public static final <fields>;}
+-keep class org.fife.ui.rsyntaxtextarea.DefaultTokenMakerFactory
+
+-keep class org.fife.ui.rsyntaxtextarea.modes.CSSTokenMaker
+-keep class org.fife.ui.rsyntaxtextarea.modes.DtdTokenMaker
+-keep class org.fife.ui.rsyntaxtextarea.modes.HTMLTokenMaker
+-keep class org.fife.ui.rsyntaxtextarea.modes.JavaScriptTokenMaker
+-keep class org.fife.ui.rsyntaxtextarea.modes.JavaTokenMaker
+-keep class org.fife.ui.rsyntaxtextarea.modes.JsonTokenMaker
+-keep class org.fife.ui.rsyntaxtextarea.modes.JSPTokenMaker
+-keep class org.fife.ui.rsyntaxtextarea.modes.PlainTextTokenMaker
+-keep class org.fife.ui.rsyntaxtextarea.modes.PropertiesFileTokenMaker
+-keep class org.fife.ui.rsyntaxtextarea.modes.SQLTokenMaker
+-keep class org.fife.ui.rsyntaxtextarea.modes.XMLTokenMaker
+
+-adaptresourcefilenames ErrorStrip.properties
+-adaptresourcefilenames RSyntaxTextArea.properties
+-adaptresourcefilenames FocusableTip.properties
diff --git a/src/website/img/btn_donate_euro.gif b/src/website/img/btn_donate_euro.gif
new file mode 100644
index 0000000..f317055
Binary files /dev/null and b/src/website/img/btn_donate_euro.gif differ
diff --git a/src/website/img/btn_donate_usd.gif b/src/website/img/btn_donate_usd.gif
new file mode 100644
index 0000000..3682dea
Binary files /dev/null and b/src/website/img/btn_donate_usd.gif differ
diff --git a/src/website/img/jd-gui.png b/src/website/img/jd-gui.png
new file mode 100644
index 0000000..2bc068c
Binary files /dev/null and b/src/website/img/jd-gui.png differ