diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/installer_omod | |
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem,
Proton/umu-run integration, and Flatpak packaging.
Key features:
- FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak)
- Proton/GE-Proton/umu-run launcher with env var forwarding
- Flatpak support (sandbox-aware VFS, NXM handler, umu-run)
- Wine prefix management UI
- Case-insensitive path resolution for Linux filesystems
- QSettings-safe INI handling (avoids Bethesda INI corruption)
- Portable instance support with auto-generated launcher scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/installer_omod')
42 files changed, 3886 insertions, 0 deletions
diff --git a/libs/installer_omod/.editorconfig b/libs/installer_omod/.editorconfig new file mode 100644 index 0000000..16be790 --- /dev/null +++ b/libs/installer_omod/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*.cpp] +indent_style = space +indent_size = 2 +insert_final_newline = true + +[*.h] +indent_style = space +indent_size = 2 +insert_final_newline = true + +[*.ui] +indent_style = space +indent_size = 2 +insert_final_newline = true diff --git a/libs/installer_omod/.gitattributes b/libs/installer_omod/.gitattributes new file mode 100644 index 0000000..d0b87f4 --- /dev/null +++ b/libs/installer_omod/.gitattributes @@ -0,0 +1,9 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf + +*.ts text eol=lf diff --git a/libs/installer_omod/.github/workflows/build.yml b/libs/installer_omod/.github/workflows/build.yml new file mode 100644 index 0000000..1c71bdc --- /dev/null +++ b/libs/installer_omod/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Installer OMOD + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }} + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Installer OMOD + id: build-installer-omod + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/installer_omod/.gitignore b/libs/installer_omod/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/installer_omod/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/installer_omod/.pre-commit-config.yaml b/libs/installer_omod/.pre-commit-config.yaml new file mode 100644 index 0000000..7bd1380 --- /dev/null +++ b/libs/installer_omod/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + # - repo: https://github.com/pre-commit/mirrors-clang-format + # rev: v19.1.5 + # hooks: + # - id: clang-format + # 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/installer_omod/CMakeLists.txt b/libs/installer_omod/CMakeLists.txt new file mode 100644 index 0000000..ed45650 --- /dev/null +++ b/libs/installer_omod/CMakeLists.txt @@ -0,0 +1,9 @@ +# CMake 3.18 due to https://gitlab.kitware.com/cmake/cmake/-/issues/20764 +cmake_minimum_required(VERSION 3.18) + +# set globally as Nuget gets confused about ZERO_CHECK, ALL_BUILD and INSTALL otherwise +set(CMAKE_DOTNET_TARGET_FRAMEWORK_VERSION "v4.8") + +project(installer_omod LANGUAGES CXX CSharp) + +add_subdirectory(src) diff --git a/libs/installer_omod/CMakePresets.json b/libs/installer_omod/CMakePresets.json new file mode 100644 index 0000000..546b445 --- /dev/null +++ b/libs/installer_omod/CMakePresets.json @@ -0,0 +1,59 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_VS_NUGET_PACKAGE_RESTORE": { + "type": "BOOL", + "value": "ON" + }, + "INSTALL_REQUIRES_VS_PACKAGE_RESTORE": { + "type": "BOOL", + "value": "ON" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/installer_omod/LICENSE b/libs/installer_omod/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/libs/installer_omod/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://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 +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/libs/installer_omod/src/CMakeLists.txt b/libs/installer_omod/src/CMakeLists.txt new file mode 100644 index 0000000..f28851c --- /dev/null +++ b/libs/installer_omod/src/CMakeLists.txt @@ -0,0 +1,67 @@ +# 3.23 for CMAKE_VS_NUGET_PACKAGE_RESTORE +cmake_minimum_required(VERSION 3.23) + +find_package(mo2-cmake CONFIG REQUIRED) +find_package(mo2-uibase CONFIG REQUIRED) + +# Dummy .NET library as VS_PACKAGE_REFERENCES doesn't work on C++/CLI projects yet +# Needs to be declared before cmake_common stuff is included as that polutes the environment and makes C# get compiled as C++ +add_library(dummy_cs_project SHARED DummyCSFile.cs) +set_target_properties(dummy_cs_project PROPERTIES + LINKER_LANGUAGE CSharp + VS_PACKAGE_REFERENCES "OMODFramework_2.2.2;OMODFramework.Scripting_2.2.2;RtfPipe_1.0.7388.1242" +) + +add_library(installer_omod SHARED) +mo2_configure_plugin(installer_omod WARNINGS OFF CLI ON) +target_link_libraries(installer_omod PRIVATE mo2::uibase) + +# I'd like to use get_target_property(source_files ${PROJECT_NAME} SOURCES) as +# globbing is naughty, but need to filter out the things that aren't relative to this directory. +file(GLOB_RECURSE source_files CONFIGURE_DEPENDS *.cpp;*.h;*.ui) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX src FILES ${source_files}) + +# Ideally we'd use "$<TARGET_FILE_DIR:dummy_cs_project>", but the relevant property +# doesn't support generator expressions. +set(omod_framework_prefix "${CMAKE_BINARY_DIR}/$(Configuration)" + CACHE PATH "Path where OMODFramework.dll and OMODFramework.Scripting.dll can be found. The default value is where the nuget package artifacts will end up. Override if you want to use a custom build.") + +set_target_properties(${PROJECT_NAME} PROPERTIES + COMMON_LANGUAGE_RUNTIME "" + # latest may not work with CLR + CXX_STANDARD 20 + #DOTNET_TARGET_FRAMEWORK "netstandard2.0" + VS_DOTNET_REFERENCE_OMODFramework "${omod_framework_prefix}/OMODFramework.dll" + VS_DOTNET_REFERENCE_OMODFramework.Scripting "${omod_framework_prefix}/OMODFramework.Scripting.dll" + VS_DOTNET_REFERENCE_RtfPipe "${CMAKE_BINARY_DIR}/$(Configuration)/RtfPipe.dll" + VS_DOTNET_REFERENCES "System.dll;System.Core.dll" +) + +target_compile_options( + installer_omod + PRIVATE + # OMODFramework and OMODFramework.Scripting reference different .NET standard libraries. This generates warnings when using them together. + "/wd4691") + +# We don't want ERROR defined +target_compile_definitions(installer_omod PRIVATE "NOGDI") + +# We don't need to actually link with dummy_cs_project, especially as its dependencies +# aren't pulled in. We do need it to build first, though. +add_dependencies(installer_omod dummy_cs_project) + +mo2_install_plugin(installer_omod FOLDER) + +install( + FILES + "$<TARGET_FILE_DIR:${PROJECT_NAME}>/OMODFramework.dll" + "$<TARGET_FILE_DIR:${PROJECT_NAME}>/OMODFramework.Scripting.dll" + "$<TARGET_FILE_DIR:${PROJECT_NAME}>/ICSharpCode.SharpZipLib.dll" + "$<TARGET_FILE_DIR:${PROJECT_NAME}>/System.Drawing.Common.dll" + "$<TARGET_FILE_DIR:${PROJECT_NAME}>/RtfPipe.dll" + DESTINATION bin/plugins/installer_omod/ +) +install( + FILES "$<TARGET_PDB_FILE_DIR:${PROJECT_NAME}>/ICSharpCode.SharpZipLib.pdb" + DESTINATION pdb) +# Other PDB files get downloaded by Visual Studio during debugging when https://symbols.nuget.org/download/symbols is in the symbol server list diff --git a/libs/installer_omod/src/DummyCSFile.cs b/libs/installer_omod/src/DummyCSFile.cs new file mode 100644 index 0000000..b093f32 --- /dev/null +++ b/libs/installer_omod/src/DummyCSFile.cs @@ -0,0 +1,16 @@ +using OMODFramework; +using OMODFramework.Scripting; +using RtfPipe; + +namespace Dummy +{ + class Stub + { + static void Stub2() + { + OMOD omod = null; + IScriptFunctions scriptFunctions = null; + Rtf.ToHtml(""); + } + } +} diff --git a/libs/installer_omod/src/MessageBoxHelper.cpp b/libs/installer_omod/src/MessageBoxHelper.cpp new file mode 100644 index 0000000..29f0be3 --- /dev/null +++ b/libs/installer_omod/src/MessageBoxHelper.cpp @@ -0,0 +1,61 @@ +#include "MessageBoxHelper.h" + +#include <QApplication> + +MessageBoxHelper::MessageBoxHelper() +{ + moveToThread(QApplication::instance()->thread()); + + connect(this, &MessageBoxHelper::criticalMessageBoxSignal, this, &MessageBoxHelper::criticalMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &MessageBoxHelper::informationMessageBoxSignal, this, &MessageBoxHelper::informationMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &MessageBoxHelper::questionMessageBoxSignal, this, &MessageBoxHelper::questionMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &MessageBoxHelper::warningMessageBoxSignal, this, &MessageBoxHelper::warningMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); +} + +QMessageBox::StandardButton MessageBoxHelper::critical(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit criticalMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +QMessageBox::StandardButton MessageBoxHelper::information(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit informationMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +QMessageBox::StandardButton MessageBoxHelper::question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit questionMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +QMessageBox::StandardButton MessageBoxHelper::warning(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit warningMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +void MessageBoxHelper::criticalMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::critical(parent, title, text, buttons, defaultButton); +} + +void MessageBoxHelper::informationMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::information(parent, title, text, buttons, defaultButton); +} + +void MessageBoxHelper::questionMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::question(parent, title, text, buttons, defaultButton); +} + +void MessageBoxHelper::warningMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::warning(parent, title, text, buttons, defaultButton); +} diff --git a/libs/installer_omod/src/MessageBoxHelper.h b/libs/installer_omod/src/MessageBoxHelper.h new file mode 100644 index 0000000..aa6b926 --- /dev/null +++ b/libs/installer_omod/src/MessageBoxHelper.h @@ -0,0 +1,37 @@ +#pragma once + +#include <QMessageBox> + +class MessageBoxHelper : public QObject +{ + Q_OBJECT + +public: + MessageBoxHelper(); + + QMessageBox::StandardButton critical(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + QMessageBox::StandardButton information(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + QMessageBox::StandardButton question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + QMessageBox::StandardButton warning(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + +signals: + void criticalMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + void informationMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + void questionMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + void warningMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + +private slots: + void criticalMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); + + void informationMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); + + void questionMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); + + void warningMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); +}; diff --git a/libs/installer_omod/src/OMODFrameworkWrapper.cpp b/libs/installer_omod/src/OMODFrameworkWrapper.cpp new file mode 100644 index 0000000..0a13620 --- /dev/null +++ b/libs/installer_omod/src/OMODFrameworkWrapper.cpp @@ -0,0 +1,771 @@ +#include "OMODFrameworkWrapper.h" + +using namespace cli; + +#include <algorithm> +#include <array> +#include <filesystem> + +#include <QMessageBox> +#include <QTemporaryDir> +#include <QProgressDialog> + +#include <uibase/imodinterface.h> +#include <uibase/imodlist.h> +#include <uibase/iplugingame.h> +#include <uibase/ipluginlist.h> +#include <uibase/game_features/igamefeatures.h> +#include <uibase/log.h> +#include <uibase/utility.h> +#include <uibase/registry.h> + +#include <uibase/game_features/dataarchives.h> +#include <uibase/game_features/gameplugins.h> + +#include "implementations/CodeProgress.h" +#include "implementations/Logger.h" +#include "implementations/ScriptFunctions.h" + +#include "interop/QtDotNetConverters.h" +#include "interop/StdDotNetConverters.h" + +#include "newstuff/namedialog.h" +#include "newstuff/rtfPopup.h" + +#include "MessageBoxHelper.h" + +namespace fs = std::filesystem; + +// retrieve the path to the folder containing the proxy DLL +fs::path getPluginFolder() +{ + wchar_t path[MAX_PATH]; + HMODULE hm = NULL; + + if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCWSTR)&getPluginFolder, &hm) == 0) + { + return {}; + } + if (GetModuleFileName(hm, path, sizeof(path)) == 0) + { + return {}; + } + + return fs::path(path).parent_path(); +} + +// We want to search the plugin data directory for .NET DLLs +class AssemblyResolver +{ +public: + static bool getInitialised() { return sInitialised; } + + static void initialise(MOBase::IOrganizer* organizer) + { + if (sInitialised) + return; + sPluginDataPath = getPluginFolder(); + System::AppDomain::CurrentDomain->AssemblyResolve += gcnew System::ResolveEventHandler(&OnAssemblyResolve); + sInitialised = true; + } + +private: + static System::Reflection::Assembly^ OnAssemblyResolve(System::Object^ sender, System::ResolveEventArgs^ args); + + static bool sInitialised; + static QDir sPluginDataPath; +}; + +bool AssemblyResolver::sInitialised = false; +QDir AssemblyResolver::sPluginDataPath; + +System::Reflection::Assembly^ AssemblyResolver::OnAssemblyResolve(System::Object^ sender, System::ResolveEventArgs^ args) +{ + QString name = toQString(args->Name).section(',', 0, 0) + ".dll"; + if (sPluginDataPath.exists(name)) + return System::Reflection::Assembly::LoadFrom(toDotNetString(sPluginDataPath.absoluteFilePath(name))); + return nullptr; +} + +OMODFrameworkWrapper::OMODFrameworkWrapper(MOBase::IOrganizer* organizer, QWidget* parentWidget) + : mMoInfo(organizer) + , mParentWidget(parentWidget) + , mWaitDialog(make_nullptr<QProgressDialog>()) +{ + try + { + AssemblyResolver::initialise(mMoInfo); + + constructorHelper(); + + connect(this, &OMODFrameworkWrapper::pickModName, this, &OMODFrameworkWrapper::pickModNameSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &OMODFrameworkWrapper::createMod, this, &OMODFrameworkWrapper::createModSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &OMODFrameworkWrapper::displayReadme, this, &OMODFrameworkWrapper::displayReadmeSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &OMODFrameworkWrapper::showWaitDialog, this, &OMODFrameworkWrapper::showWaitDialogSlot, Qt::ConnectionType::QueuedConnection); + connect(this, &OMODFrameworkWrapper::hideWaitDialog, this, &OMODFrameworkWrapper::hideWaitDialogSlot, Qt::ConnectionType::QueuedConnection); + + initFrameworkSettings(); + } + catch (const std::exception& e) + { + throw; + } + catch (System::Exception^ dotNetException) + { + throw toStdException(dotNetException); + } +} + +void OMODFrameworkWrapper::constructorHelper() +{ + // We can't call a function doing this before AssemblyResolver::initialise happens as the DLL needs to be available before its stack frame is created. + mTempPathStack.push(toQString(OMODFramework::Framework::Settings->TempPath)); +} + +ref class InstallInAnotherThreadHelper +{ +public: + InstallInAnotherThreadHelper(OMODFrameworkWrapper* owner, MOBase::GuessedValue<QString>& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) + : mModName(&modName) + , mGameName(&gameName) + , mArchiveName(&archiveName) + , mVersion(&version) + , mNexusID(nexusID) + , mOwner(owner) + , mExceptionPtr(new std::exception_ptr) + , mHasResult(false) + {} + + ~InstallInAnotherThreadHelper() + { + if (!mExceptionPtr) + return; + + this->!InstallInAnotherThreadHelper(); + } + + !InstallInAnotherThreadHelper() + { + delete mExceptionPtr; + mExceptionPtr = nullptr; + } + + void Run() + { + try + { + mResult = mOwner->install(*mModName, *mGameName, *mArchiveName, *mVersion, mNexusID); + mHasResult = true; + } + catch (...) + { + *mExceptionPtr = std::current_exception(); + } + } + + bool HasResult() + { + return mHasResult; + } + + OMODFrameworkWrapper::EInstallResult Result() + { + return mResult; + } + + std::exception_ptr ExceptionPtr() + { + return *mExceptionPtr; + } + +private: + // We own none of these pointers. They're pointers because managed objects can't have unmanaged members, but a pointer is just an integer of some form, which is the same. + MOBase::GuessedValue<QString>* mModName; + QString* mGameName; + const QString* mArchiveName; + const QString* mVersion; + int mNexusID; + + OMODFrameworkWrapper* mOwner; + + std::exception_ptr* mExceptionPtr; + OMODFrameworkWrapper::EInstallResult mResult; + bool mHasResult; +}; + +OMODFrameworkWrapper::EInstallResult OMODFrameworkWrapper::installInAnotherThread(MOBase::GuessedValue<QString>& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) +{ + QEventLoop eventLoop; + InstallInAnotherThreadHelper^ helper = gcnew InstallInAnotherThreadHelper(this, modName, gameName, archiveName, version, nexusID); + System::Threading::Tasks::Task^ installationTask = System::Threading::Tasks::Task::Run(gcnew System::Action(helper, &InstallInAnotherThreadHelper::Run)); + + // TODO: connect stuff to eventLoop.wakeUp. + // Installation manager does this with futureWatcher.finished and progressUpdate + while (!installationTask->IsCompleted) + eventLoop.processEvents(QEventLoop::AllEvents | QEventLoop::WaitForMoreEvents); + + if (helper->HasResult()) + return helper->Result(); + else if (helper->ExceptionPtr()) + std::rethrow_exception(helper->ExceptionPtr()); + else + throw std::runtime_error("Something went horribly wrong when asynchronously installing an OMOD. We don't even have the original exception."); +} + +// TODO: replace with std::scope_exit when it leaves std::experimental +template <typename T> +class scope_guard +{ +public: + scope_guard(T onExit) : mOnExit(onExit) {} + ~scope_guard() { mOnExit(); } +private: + T mOnExit; +}; + +OMODFrameworkWrapper::EInstallResult OMODFrameworkWrapper::install(MOBase::GuessedValue<QString>& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) +{ + try + { + QObject_unique_ptr<MessageBoxHelper> messageBoxHelper = make_unique<MessageBoxHelper>(); + + MOBase::log::debug("Installing {} as OMOD", archiveName); + + emit showWaitDialog("Initializing OMOD installer... "); + refreshFrameworkSettings(); + // Stack allocating should dispose like a `using` statement in C# + OMODFramework::OMOD omod(toDotNetString(archiveName)); + emit hideWaitDialog(); + + if (!System::String::IsNullOrEmpty(omod.ModName)) + modName.update(toQString(omod.ModName), MOBase::EGuessQuality::GUESS_META); + + bool nameNotCancelled; + emit pickModName(nameNotCancelled, modName); + if (!nameNotCancelled) + return EInstallResult::RESULT_CANCELED; + + MOBase::IModInterface* modInterface; + emit createMod(modInterface, modName); + if (!modInterface) + return EInstallResult::RESULT_CANCELED; + + { + QTemporaryDir tempPath(modInterface->absolutePath() + "/OMODInstallTempXXXXXX"); + pushTempPath(tempPath.path()); + scope_guard tempPathGuard([this]() { this->popTempPath(); }); + + if (omod.HasReadme) + emit displayReadme(toQString(omod.ModName), toQString(omod.GetReadme())); + + if (omod.HasScript) + { + MOBase::log::debug("Mod has script. Run it."); + OMODFramework::Scripting::IScriptFunctions^ scriptFunctions = gcnew ScriptFunctions(mParentWidget, mMoInfo); + + System::Collections::Generic::List<System::String^>^ activeBSAs = gcnew System::Collections::Generic::List<System::String^>(); + { + // Hopefully some other part of MO2 also knows this and this can be refactored + auto dataArchives = mMoInfo->gameFeatures()->gameFeature<MOBase::DataArchives>(); + // force-enabled by engine + auto bsas = dataArchives->vanillaArchives(); + // explicitly enabled in INI. For Oblivion, excludes the vanilla BSAs by default. + bsas.append(dataArchives->archives(mMoInfo->profile().get())); + for (const auto& bsa : bsas) + { + QString path = mMoInfo->resolvePath(bsa); + if (!path.isEmpty()) + activeBSAs->Add(toDotNetString(path)); + } + + std::map<int, QString> loadOrder; + for (const auto& plugin : mMoInfo->pluginList()->pluginNames()) + { + int loadIndex = mMoInfo->pluginList()->loadOrder(plugin); + if (loadIndex != -1) + loadOrder[loadIndex] = plugin; + } + for (const auto& [loadIndex, plugin] : loadOrder) + { + QString bsaPath = QFileInfo(plugin).completeBaseName() + ".bsa"; + bsaPath = mMoInfo->resolvePath(bsaPath); + if (!bsaPath.isEmpty()) + activeBSAs->Add(toDotNetString(bsaPath)); + } + } + OMODFramework::Framework::LoadBSAs(activeBSAs); + scope_guard bsaGuard([]() { OMODFramework::Framework::ClearBSAs(); }); + + System::String^ dataPath = omod.GetDataFiles(); + System::String^ pluginsPath = omod.GetPlugins(); + OMODFramework::ScriptReturnData^ scriptData = OMODFramework::Scripting::ScriptRunner::RunScript(%omod, scriptFunctions, dataPath, pluginsPath); + if (!scriptData) + throw std::runtime_error("OMOD script returned no result. This isn't supposed to happen."); + if (scriptData->CancelInstall) + return EInstallResult::RESULT_CANCELED; + + // inis first so that you don't need to wait for extraction before a second batch of questions appears + if (scriptData->INIEdits && scriptData->INIEdits->Count) + { + QMap<QString, QMap<QString, QString>> iniEdits; + for each (OMODFramework::INIEditInfo ^ edit in scriptData->INIEdits) + iniEdits[toQString(edit->Section)][toQString(edit->Name)] = toQString(edit->NewValue); + // This feels like something I shouldn't need to do manually + QVariantMap iniEditsVariants; + for (const auto& section : iniEdits.keys()) + { + QVariantMap innerMap; + for (const auto& setting : iniEdits[section]) + innerMap[setting] = iniEdits[section][setting]; + iniEditsVariants[section] = innerMap; + } + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".iniEdits", iniEditsVariants); + + QString oblivionIniPath = mMoInfo->profile()->absoluteIniFilePath("Oblivion.ini"); + bool yesToAll = false; + for each (OMODFramework::INIEditInfo ^ edit in scriptData->INIEdits) + { + QString section = toQString(edit->Section); + section = section.mid(1, section.size() - 2); + QString name = toQString(edit->Name); + QString newValue = toQString(edit->NewValue); + QString oldValue; + if (edit->OldValue) + oldValue = toQString(edit->OldValue); + else + { + // I'm pretty sure this is the maximum length for vanilla Oblivion. + wchar_t buffer[256]; + if (GetPrivateProfileString(section.toStdWString().data(), name.toStdWString().data(), nullptr, buffer, sizeof(buffer) / sizeof(buffer[0]), oblivionIniPath.toStdWString().data())) + oldValue = QString::fromWCharArray(buffer); + } + + MOBase::log::debug("OMOD wants to set [{}] {} to \"{}\", was \"{}\"", section, name, newValue, oldValue); + + if (oldValue == newValue) + { + MOBase::log::debug("Value is unchanged, not nagging user."); + continue; + } + + QMessageBox::StandardButton response; + if (!yesToAll) + { + QString message; + if (!oldValue.isEmpty()) + { + /*: %1 is the mod name + [%2] is the ini section name. + %3 is the ini setting name. + %4 is the value already in Oblivion.ini. + %5 is the value the mod wants to set. + */ + message = tr("%1 wants to change [%2] %3 from \"%4\" to \"%5\"").arg(modName).arg(section).arg(name).arg(oldValue).arg(newValue); + } + else + { + /*: %1 is the mod name + [%2] is the ini section name. + %3 is the ini setting name. + %5 is the value the mod wants to set. + */ + message = tr("%1 wants to set [%2] %3 to \"%4\"").arg(modName).arg(section).arg(name).arg(newValue); + } + + response = messageBoxHelper->question(mParentWidget, tr("Update INI?"), message, QMessageBox::Yes | QMessageBox::No | QMessageBox::YesToAll | QMessageBox::NoToAll); + if (response == QMessageBox::NoToAll) + { + MOBase::log::debug("User skipped all."); + break; + } + + yesToAll |= response == QMessageBox::YesToAll; + } + + if (yesToAll || response == QMessageBox::StandardButton::Yes) + { + MOBase::log::debug("Doing edit."); + MOBase::WriteRegistryValue(section.toStdWString().data(), name.toStdWString().data(), newValue.toStdWString().data(), oblivionIniPath.toStdWString().data()); + } + else + MOBase::log::debug("User skipped edit."); + } + } + + scriptData->Pretty(%omod, dataPath, pluginsPath); + // no compatability between auto and var makes me :angery: + System::Collections::Generic::HashSet<System::String^>^ installedPlugins = gcnew System::Collections::Generic::HashSet<System::String^>(System::StringComparer::InvariantCultureIgnoreCase); + for each (OMODFramework::InstallFile file in scriptData->InstallFiles) + { + System::String^ destinationPath = System::IO::Path::Combine(toDotNetString(modInterface->absolutePath()), file.InstallTo); + System::IO::Directory::CreateDirectory(System::IO::Path::GetDirectoryName(destinationPath)); + System::IO::File::Copy(file.InstallFrom, destinationPath, true); + System::String^ extension = System::IO::Path::GetExtension(file.InstallTo); + if (extension && (extension->Equals(".esm", System::StringComparison::InvariantCultureIgnoreCase) || extension->Equals(".esp", System::StringComparison::InvariantCultureIgnoreCase))) + installedPlugins->Add(file.InstallTo); + } + + if (scriptData->UncheckedPlugins) + installedPlugins->ExceptWith(scriptData->UncheckedPlugins); + + QStringList defaultActivePlugins; + for each (System::String ^ plugin in installedPlugins) + defaultActivePlugins.append(toQString(plugin)); + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultActivePlugins", defaultActivePlugins); + QStringList defaultInactivePlugins; + if (scriptData->UncheckedPlugins) + { + for each (System::String ^ plugin in scriptData->UncheckedPlugins) + defaultInactivePlugins.append(toQString(plugin)); + } + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultInactivePlugins", defaultInactivePlugins); + + QStringList registeredBSAs; + if (scriptData->RegisterBSASet) + { + for each (System::String ^ bsa in scriptData->RegisterBSASet) + registeredBSAs.append(toQString(bsa)); + } + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".registeredBSAs", registeredBSAs); + + if (scriptData->SDPEdits && scriptData->SDPEdits->Count) + { + for each (OMODFramework::SDPEditInfo ^ shaderEdit in scriptData->SDPEdits) + { + System::String^ destinationPath = System::IO::Path::Combine(toDotNetString(modInterface->absolutePath()), "Shaders", "OMOD", toDotNetString(QString::number(shaderEdit->Package))); + System::IO::Directory::CreateDirectory(destinationPath); + System::IO::File::Copy(shaderEdit->BinaryObject, System::IO::Path::Combine(destinationPath, shaderEdit->Shader), true); + } + } + + std::map<QString, int> unhandledScriptReturnDataCounts; + // This is a mapping from plugin name to an enum saying whether OBMM should allow the user to deactivate an ESP from the OMOD, disallow it, or just warn. By default, it'd warn. + unhandledScriptReturnDataCounts["ESPDeactivation"] = scriptData->ESPDeactivation ? scriptData->ESPDeactivation->Count : 0; + // There's nothing in the OBMM documentation claiming the function that sets this exists. + unhandledScriptReturnDataCounts["EarlyPlugins"] = scriptData->EarlyPlugins ? scriptData->EarlyPlugins->Count : 0; + // Sets load order a.esp, b.esp, true and b.esp, a.esp, false both mean the same thing. + unhandledScriptReturnDataCounts["LoadOrderSet"] = scriptData->LoadOrderSet ? scriptData->LoadOrderSet->Count : 0; + // Says this OMOD conflicts with another, potentially with a description. + unhandledScriptReturnDataCounts["ConflictsWith"] = scriptData->ConflictsWith ? scriptData->ConflictsWith->Count : 0; + // Says this OMOD depends on another, potentially with a description. + unhandledScriptReturnDataCounts["DependsOn"] = scriptData->DependsOn ? scriptData->DependsOn->Count : 0; + + // Contains a list of GMSTs and Globals to edit in the mod's ESPs. May a higher being have mercy on us if anyone ever used this. + unhandledScriptReturnDataCounts["ESPEdits"] = scriptData->ESPEdits ? scriptData->ESPEdits->Count : 0; + // OMODFramework is handling this for us, so don't sweat it. + unhandledScriptReturnDataCounts["PatchFiles"] = scriptData->PatchFiles ? scriptData->PatchFiles->Count : 0; + + for (const auto& [name, usageCount] : unhandledScriptReturnDataCounts) + { + if (usageCount) + { + /*: %1 is the mod name + %2 is the name of a field in the OMOD's return data + Hopefully this message will never be seen by anyone, but if it is, they need to know to tell the Mod Organizer 2 dev team. + */ + QString userMessage = tr("%1 has data for %2, but Mod Organizer 2 doesn't know what to do with it yet. Please report this to the Mod Organizer 2 development team (ideally by sending us your interface log) as we didn't find any OMODs that actually did this, and we need to know that they exist."); + userMessage = userMessage.arg(toQString(omod.ModName)); + userMessage = userMessage.arg(name); + messageBoxHelper->warning(mParentWidget, tr("Mod Organizer 2 can't completely install this OMOD."), userMessage); + MOBase::log::warn("{} ({}) contains {} entries for {}", toUTF8String(omod.ModName), archiveName, usageCount, name); + } + } + } + else + { + MOBase::log::debug("Mod has no script. Install contents directly."); + QString data = toQString(omod.GetDataFiles()); + QString plugins = toQString(omod.GetPlugins()); + if (!data.isNull()) + { + if (MOBase::shellMove(data + "/*.*", modInterface->absolutePath(), true, mParentWidget)) + MOBase::log::debug("Installed mod files."); + else + MOBase::log::error("Error while installing mod files."); + QFile::remove(data); + } + if (!plugins.isNull()) + { + QStringList defaultActivePlugins = QDir(plugins).entryList({ "*.esm", "*.esp" }, QDir::Files); + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultActivePlugins", defaultActivePlugins); + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultInactivePlugins", QStringList()); + if (MOBase::shellMove(plugins + "/*.*", modInterface->absolutePath(), true, mParentWidget)) + MOBase::log::debug("Installed mod plugins."); + else + MOBase::log::error("Error while installing mod plugins."); + QFile::remove(plugins); + } + } + + // on success, set mod info + MOBase::VersionInfo modVersion(std::max(int(omod.MajorVersion), 0), std::max(int(omod.MinorVersion), 0), std::max(int(omod.BuildVersion), 0)); + modInterface->setVersion(modVersion); + + // TODO: parse omod.Website. If it's Nexus, set the ID, otherwise set custom URL in meta.ini. We can't set the URL with the installation manager. + // TODO: maybe convert omod.Description to HTML and set it as nexusDescription + // TODO: maybe Holt will finish the proposed mod metadata API and there'll be a better, tidier option. + if (nexusID != -1) + modInterface->setNexusID(nexusID); + + modInterface->setInstallationFile(archiveName); + } + + OMODFramework::Framework::CleanTempDir(false); + + QStringList omodsPendingPostInstall = modInterface->pluginSetting("Omod Installer", "omodsPendingPostInstall", QStringList()).toStringList(); + if (!omodsPendingPostInstall.contains(toQString(omod.ModName))) + { + omodsPendingPostInstall.append(toQString(omod.ModName)); + modInterface->setPluginSetting("Omod Installer", "omodsPendingPostInstall", omodsPendingPostInstall); + } + return EInstallResult::RESULT_SUCCESS; + } + catch (const std::exception& e) + { + throw; + } + catch (System::Exception^ dotNetException) + { + throw toStdException(dotNetException); + } +} + +void OMODFrameworkWrapper::setParentWidget(QWidget* parentWidget) +{ + mParentWidget = parentWidget; + if (OMODFramework::Framework::Settings->CodeProgress) + static_cast<CodeProgress^>(OMODFramework::Framework::Settings->CodeProgress)->setParentWidget(mParentWidget); +} + +const std::array<QString, 3> pluginStateNames = { "STATE_MISSING", "STATE_INACTIVE", "STATE_ACTIVE" }; + +void OMODFrameworkWrapper::onInstallationEnd(EInstallResult status, MOBase::IModInterface* mod) +{ + if (status != EInstallResult::RESULT_SUCCESS || !mod) + return; + + QStringList omodsPendingPostInstall = mod->pluginSetting("Omod Installer", "omodsPendingPostInstall", QStringList()).toStringList(); + + if (!omodsPendingPostInstall.empty() && !(mMoInfo->modList()->state(mod->name()) & MOBase::IModList::STATE_ACTIVE)) + { + auto response = QMessageBox::question(mParentWidget, tr("Activate mod?"), + /*: %1 is the left-pane mod name. + %2 is the name from the metadata of an OMOD. + */ + tr("%1 contains the OMOD %2. OMODs may have post-installation actions like activating ESPs. Would you like to enable the mod so this can happen now?").arg(mod->name()).arg(omodsPendingPostInstall[0]), + QMessageBox::Yes | QMessageBox::No); + + if (response == QMessageBox::StandardButton::Yes) + mMoInfo->modList()->setActive(mod->name(), true); + else + return; + } + + for (const auto& omodName : omodsPendingPostInstall) + { + MOBase::log::debug("Running post-install for {}", omodName); + QStringList defaultActivePlugins = mod->pluginSetting("Omod Installer", omodName + ".defaultActivePlugins", QStringList()).toStringList(); + QStringList defaultInactivePlugins = mod->pluginSetting("Omod Installer", omodName + ".defaultInactivePlugins", QStringList()).toStringList(); + + bool yesToAll = false; + for (const auto& plugin : defaultActivePlugins) + { + MOBase::IPluginList::PluginStates oldState = mMoInfo->pluginList()->state(plugin); + MOBase::log::debug("OMOD wants to activate {}, was {}", plugin, pluginStateNames[oldState]); + + QMessageBox::StandardButton response; + if (!yesToAll) + { + QString message; + if (oldState == MOBase::IPluginList::STATE_INACTIVE) + { + /*: %1 is the mod name. + %2 is the plugin name. + */ + message = tr("%1 wants to activate %2. Do you want to do so?").arg(omodName).arg(plugin); + + response = QMessageBox::question(mParentWidget, tr("Activate plugin?"), message, QMessageBox::Yes | QMessageBox::No | QMessageBox::YesToAll | QMessageBox::NoToAll); + if (response == QMessageBox::NoToAll) + { + MOBase::log::debug("User skipped all."); + break; + } + + yesToAll |= response == QMessageBox::YesToAll; + } + else + { + if (oldState == MOBase::IPluginList::STATE_MISSING) + QMessageBox::warning(mParentWidget, tr("OMOD wants to activate missing plugin"), tr("An OMOD wants to activate a missing plugin. This shouldn't be possible. Please report this to a MO2 developer.")); + continue; + } + } + + if (yesToAll || response == QMessageBox::StandardButton::Yes) + { + MOBase::log::debug("Activating plugin."); + mMoInfo->pluginList()->setState(plugin, MOBase::IPluginList::STATE_ACTIVE); + } + else + MOBase::log::debug("User skipped plugin."); + } + + yesToAll = false; + for (const auto& plugin : defaultInactivePlugins) + { + MOBase::IPluginList::PluginStates oldState = mMoInfo->pluginList()->state(plugin); + MOBase::log::debug("OMOD installed {}, but didn't try and activate it. State was {}", plugin, pluginStateNames[oldState]); + + QMessageBox::StandardButton response; + if (!yesToAll) + { + QString message; + if (oldState == MOBase::IPluginList::STATE_INACTIVE) + { + /*: %1 is the mod name. + %2 is the plugin name. + */ + message = tr("%1 installed %2, but doesn't activate it by default. Do you want to activate it anyway?").arg(omodName).arg(plugin); + + response = QMessageBox::question(mParentWidget, tr("Activate plugin?"), message, QMessageBox::Yes | QMessageBox::No | QMessageBox::YesToAll | QMessageBox::NoToAll); + if (response == QMessageBox::NoToAll) + { + MOBase::log::debug("User skipped all."); + break; + } + + yesToAll |= response == QMessageBox::YesToAll; + } + else + { + if (oldState == MOBase::IPluginList::STATE_MISSING) + QMessageBox::warning(mParentWidget, tr("OMOD claimed to install missing plugin"), tr("An OMOD has activation settings for a missing plugin. This shouldn't be possible. Please report this to a MO2 developer.")); + continue; + } + } + + if (yesToAll || response == QMessageBox::StandardButton::Yes) + { + MOBase::log::debug("Activating plugin."); + mMoInfo->pluginList()->setState(plugin, MOBase::IPluginList::STATE_ACTIVE); + } + else + MOBase::log::debug("User skipped plugin."); + } + + // this is still ugly. + mMoInfo->gameFeatures()->gameFeature<MOBase::GamePlugins>()->writePluginLists(mMoInfo->pluginList()); + + + QStringList registeredBSAs = mod->pluginSetting("Omod Installer", omodName + ".registeredBSAs", QStringList()).toStringList(); + if (!registeredBSAs.empty()) + { + MOBase::log::debug("OMOD wants to register BSAs. We can't do that."); + QMessageBox::warning(mParentWidget, tr("Register BSAs"), + /*: %1 is the OMOD name + <ul><li>%2</li></ul> becomes a list of BSA files + */ + tr("%1 wants to register the following BSA archives, but Mod Organizer 2 can't do that yet due to technical limitations:<ul><li>%2</li></ul>For now, your options include adding the BSA names to <code>sResourceArchiveList</code> in the game INI, creating a dummy ESP with the same name, or extracting the BSA, all of which have drawbacks.") + .arg(omodName).arg(registeredBSAs.join("</li><li>")) + ); + } + } + + mod->setPluginSetting("Omod Installer", "omodsPendingPostInstall", QStringList()); +} + +void OMODFrameworkWrapper::initFrameworkSettings() +{ + OMODFramework::Framework::Settings->CodeProgress = gcnew CodeProgress(mParentWidget); + + // This is a hack to fix an OMOD framework bug and should be removed once it's fixed. + OMODFramework::Framework::Settings->DllPath = System::IO::Path::Combine(System::IO::Path::GetDirectoryName(OMODFramework::Framework::Settings->DllPath), "OMODFramework.Scripting.dll"); + + OMODFramework::LoggingSettings^ loggingSettings = OMODFramework::Framework::Settings->LoggingSettings; + loggingSettings->LogToFile = false; + loggingSettings->LowestLoggingLevel = Logger::OMODLoggingLevel(MOBase::log::getDefault().level()); + loggingSettings->Logger = gcnew Logger(); + loggingSettings->UseLogger = true; + + OMODFramework::ScriptExecutionSettings^ scriptSettings = gcnew OMODFramework::ScriptExecutionSettings(); + scriptSettings->EnableWarnings = true; + scriptSettings->ReadINIWithInterface = false; + scriptSettings->ReadRendererInfoWithInterface = false; + scriptSettings->HandleBSAsWithInterface = false; + scriptSettings->PatchWithInterface = false; + scriptSettings->UseSafePatching = true; + + OMODFramework::Framework::Settings->ScriptExecutionSettings = scriptSettings; +} + +void OMODFrameworkWrapper::refreshFrameworkSettings() +{ + OMODFramework::ScriptExecutionSettings^ scriptSettings = OMODFramework::Framework::Settings->ScriptExecutionSettings; + + if (scriptSettings && mMoInfo->managedGame()) + { + // the managed game isn't set during initFrameworkSettings, so only do this here + scriptSettings->OblivionGamePath = toDotNetString(mMoInfo->managedGame()->gameDirectory().path()); + scriptSettings->OblivionINIPath = toDotNetString(mMoInfo->profile()->absoluteIniFilePath("Oblivion.ini")); + scriptSettings->OblivionRendererInfoPath = System::IO::Path::Combine(toDotNetString(mMoInfo->managedGame()->documentsDirectory().path()), "RendererInfo.txt"); + } +} + +void OMODFrameworkWrapper::pushTempPath(const QString& tempPath) +{ + if (!tempPath.isEmpty()) + OMODFramework::Framework::Settings->TempPath = toDotNetString(tempPath); + mTempPathStack.push(tempPath); +} + +void OMODFrameworkWrapper::popTempPath() +{ + if (mTempPathStack.count() >= 2) + mTempPathStack.pop(); + OMODFramework::Framework::Settings->TempPath = toDotNetString(mTempPathStack.top()); +} + +void OMODFrameworkWrapper::pickModNameSlot(bool& successOut, MOBase::GuessedValue<QString>& modName) +{ + NameDialog nameDialog(modName, mParentWidget); + successOut = nameDialog.exec() == QDialog::Accepted; + if (successOut) + modName.update(nameDialog.getName(), MOBase::EGuessQuality::GUESS_USER); +} + +void OMODFrameworkWrapper::createModSlot(MOBase::IModInterface*& modInterfaceOut, MOBase::GuessedValue<QString>& modName) +{ + modInterfaceOut = mMoInfo->createMod(modName); +} + +void OMODFrameworkWrapper::displayReadmeSlot(const QString& modName, const QString& readme) +{ + if (QMessageBox::question(mParentWidget, tr("Display Readme?"), + //: <br> is a line break. Translators can remove it if it makes things clearer. + tr("The Readme may explain installation options. Display it?<br>It will remain visible until you close it.")) == QMessageBox::StandardButton::Yes) + { + // TODO: ideally this wouldn't be part of the same window heirarchy so that modal popups in the installer don't prevent it being moved/resized etc. + // DarNified UI's popups are modal for the whole process, so any fancy trick needs to be *here*. + RtfPopup* readmePopup = new RtfPopup(toDotNetString(readme), mParentWidget); + //: %1 is the mod name + readmePopup->setWindowTitle(tr("%1 Readme").arg(modName)); + readmePopup->show(); + readmePopup->setAttribute(Qt::WA_DeleteOnClose); + } +} + +void OMODFrameworkWrapper::showWaitDialogSlot(QString message) { + mWaitDialog.reset(new QProgressDialog(message, QString(), 0, 0, mParentWidget)); + mWaitDialog->setWindowFlags(mWaitDialog->windowFlags() & ~Qt::WindowContextHelpButtonHint & ~Qt::WindowCloseButtonHint); + mWaitDialog->setWindowModality(Qt::WindowModal); + mWaitDialog->show(); +} + +void OMODFrameworkWrapper::hideWaitDialogSlot() { + if (mWaitDialog) { + mWaitDialog->hide(); + mWaitDialog.reset(); + } +} diff --git a/libs/installer_omod/src/OMODFrameworkWrapper.h b/libs/installer_omod/src/OMODFrameworkWrapper.h new file mode 100644 index 0000000..50251f0 --- /dev/null +++ b/libs/installer_omod/src/OMODFrameworkWrapper.h @@ -0,0 +1,69 @@ +#pragma once + +#include <QCoreApplication> +#include <QProgressDialog> +#include <QStack> + +#include <uibase/iplugininstaller.h> + +#include "implementations/CodeProgress.h" +#include "QObject_unique_ptr.h" + +// define this here as it's going to be used a lot by things using this class' message box wrappers. +template<class T> +T& unused_out(T&& t) { return t; } + +class OMODFrameworkWrapper : public QObject +{ + Q_OBJECT + +public: + using EInstallResult = MOBase::IPluginInstaller::EInstallResult; + + OMODFrameworkWrapper(MOBase::IOrganizer* organizer, QWidget* parentWidget = nullptr); + + EInstallResult installInAnotherThread(MOBase::GuessedValue<QString>& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID); + + EInstallResult install(MOBase::GuessedValue<QString>& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID); + + void setParentWidget(QWidget* parentWidget); + + void onInstallationEnd(EInstallResult status, MOBase::IModInterface* mod); + +protected: + void initFrameworkSettings(); + + void refreshFrameworkSettings(); + + void pushTempPath(const QString& tempPath); + + void popTempPath(); + +signals: + void pickModName(bool& successOut, MOBase::GuessedValue<QString>& modName); + + void createMod(MOBase::IModInterface*& modInterfaceOut, MOBase::GuessedValue<QString>& modName); + + void displayReadme(const QString& modName, const QString& readme); + + void showWaitDialog(QString message); + void hideWaitDialog(); + +protected slots: + void pickModNameSlot(bool& successOut, MOBase::GuessedValue<QString>& modName); + void createModSlot(MOBase::IModInterface*& modInterfaceOut, MOBase::GuessedValue<QString>& modName); + void displayReadmeSlot(const QString& modName, const QString& readme); + + void showWaitDialogSlot(QString message); + void hideWaitDialogSlot(); + +private: + __declspec(noinline) void constructorHelper(); + + MOBase::IOrganizer* mMoInfo; + QWidget* mParentWidget; + + QStack<QString> mTempPathStack; + + QObject_unique_ptr<QProgressDialog> mWaitDialog; +}; diff --git a/libs/installer_omod/src/QObject_unique_ptr.h b/libs/installer_omod/src/QObject_unique_ptr.h new file mode 100644 index 0000000..e6f9395 --- /dev/null +++ b/libs/installer_omod/src/QObject_unique_ptr.h @@ -0,0 +1,20 @@ +#pragma once + +#include <QObject> + +static inline void deleteQObjectLater(QObject* qObject) { qObject->deleteLater(); } + +template <typename T> +using QObject_unique_ptr = std::unique_ptr<T, decltype(&deleteQObjectLater)>; + +template <typename T, typename... Args> +QObject_unique_ptr<T> make_unique(Args&&... args) +{ + return QObject_unique_ptr<T>(new T(std::forward<Args>(args)...), &deleteQObjectLater); +} + +template <typename T> +QObject_unique_ptr<T> make_nullptr() +{ + return QObject_unique_ptr<T>(nullptr, &deleteQObjectLater); +} diff --git a/libs/installer_omod/src/implementations/CodeProgress.cpp b/libs/installer_omod/src/implementations/CodeProgress.cpp new file mode 100644 index 0000000..7256592 --- /dev/null +++ b/libs/installer_omod/src/implementations/CodeProgress.cpp @@ -0,0 +1,85 @@ +#include <QApplication> + +#include "CodeProgress.h" + +CodeProgressHelper::CodeProgressHelper(QWidget* parentWidget) : mParentWidget{ parentWidget }, mProgressDialog(make_nullptr<QProgressDialog>()) { + moveToThread(QApplication::instance()->thread()); + connect(this, &CodeProgressHelper::ShowProgressDialogSignal, this, &CodeProgressHelper::ShowProgressDialogSlot, Qt::QueuedConnection); + connect(this, &CodeProgressHelper::UpdateProgressValueSignal, this, &CodeProgressHelper::UpdateProgressValueSlot, Qt::BlockingQueuedConnection); + connect(this, &CodeProgressHelper::HideProgressDialogSignal, this, &CodeProgressHelper::HideProgressDialogSlot, Qt::QueuedConnection); + +} + +void CodeProgressHelper::ShowProgressDialog() { + emit ShowProgressDialogSignal(); +} + +void CodeProgressHelper::UpdateProgressValue(int percentage) { + emit UpdateProgressValueSignal(percentage); +} + +void CodeProgressHelper::HideProgressDialog() { + emit HideProgressDialogSignal(); +} + +void CodeProgressHelper::ShowProgressDialogSlot() { + mProgressDialog.reset(new QProgressDialog(mParentWidget)); + mProgressDialog->setWindowFlags(mProgressDialog->windowFlags() & ~Qt::WindowContextHelpButtonHint & ~Qt::WindowCloseButtonHint); + mProgressDialog->setWindowModality(Qt::WindowModal); + mProgressDialog->setCancelButton(nullptr); + mProgressDialog->setMinimum(0); + mProgressDialog->setMaximum(100); + mProgressDialog->setAutoReset(false); + mProgressDialog->setAutoClose(false); + mProgressDialog->show(); +} + +void CodeProgressHelper::UpdateProgressValueSlot(int percentage) { + mProgressDialog->setValue(percentage); +} + +void CodeProgressHelper::HideProgressDialogSlot() { + mProgressDialog->hide(); + mProgressDialog.reset(); + } + +void CodeProgress::Init(__int64 totalSize, bool compressing) +{ + System::GC::ReRegisterForFinalize(this); + + mTotalSize = totalSize; + mCompressing = compressing; + mPercentage = 0; + + mHelper = new CodeProgressHelper(mParentWidget); + mHelper->ShowProgressDialog(); +} + +void CodeProgress::SetProgress(__int64 inSize, __int64 outSize) +{ + int newPercentage = (int)(100 * inSize / (double)mTotalSize); + if (newPercentage != mPercentage) + mHelper->UpdateProgressValue(newPercentage); + mPercentage = newPercentage; +} + +CodeProgress::~CodeProgress() +{ + if (!mHelper) + return; + + mHelper->HideProgressDialog(); + this->!CodeProgress(); +} + +CodeProgress::!CodeProgress() +{ + if (mHelper) + mHelper->deleteLater(); + mHelper = nullptr; +} + +void CodeProgress::setParentWidget(QWidget* parentWidget) +{ + mParentWidget = parentWidget; +} diff --git a/libs/installer_omod/src/implementations/CodeProgress.h b/libs/installer_omod/src/implementations/CodeProgress.h new file mode 100644 index 0000000..902fccd --- /dev/null +++ b/libs/installer_omod/src/implementations/CodeProgress.h @@ -0,0 +1,59 @@ +#pragma once + +#include <QObject> +#include <QWidget> +#include <QProgressDialog> + +#include "../QObject_unique_ptr.h" + +using namespace cli; + +class CodeProgressHelper : public QObject { + Q_OBJECT +public: + + CodeProgressHelper(QWidget* parentWidget); + + void ShowProgressDialog(); + void UpdateProgressValue(int percentage); + void HideProgressDialog(); + +public slots: + void ShowProgressDialogSlot(); + void UpdateProgressValueSlot(int percentage); + void HideProgressDialogSlot(); + +signals: + + void ShowProgressDialogSignal(); + void UpdateProgressValueSignal(int percentage); + void HideProgressDialogSignal(); + +private: + QWidget* mParentWidget; + QObject_unique_ptr<QProgressDialog> mProgressDialog; +}; + +ref class CodeProgress : OMODFramework::ICodeProgress +{ +public: + + CodeProgress(QWidget* parentWidget) : mParentWidget(parentWidget) { } + + virtual void Init(__int64 totalSize, bool compressing); + + virtual void SetProgress(__int64 inSize, __int64 outSize); + + ~CodeProgress(); + + !CodeProgress(); + + void setParentWidget(QWidget* parentWidget); + +private: + QWidget* mParentWidget; + CodeProgressHelper* mHelper; + __int64 mTotalSize; + int mPercentage; + bool mCompressing; +}; diff --git a/libs/installer_omod/src/implementations/Logger.cpp b/libs/installer_omod/src/implementations/Logger.cpp new file mode 100644 index 0000000..2b0a0c1 --- /dev/null +++ b/libs/installer_omod/src/implementations/Logger.cpp @@ -0,0 +1,45 @@ +#include "Logger.h" + +#include "../interop/StdDotNetConverters.h" + +OMODFramework::LoggingLevel Logger::OMODLoggingLevel(MOBase::log::Levels level) +{ + switch (level) + { + default: + case MOBase::log::Debug: + return OMODFramework::LoggingLevel::DEBUG; + case MOBase::log::Info: + return OMODFramework::LoggingLevel::INFO; + case MOBase::log::Warning: + return OMODFramework::LoggingLevel::WARNING; + case MOBase::log::Error: + return OMODFramework::LoggingLevel::ERROR; + } +} + +MOBase::log::Levels Logger::MOLoggingLevel(OMODFramework::LoggingLevel level) +{ + switch (level) + { + default: + case OMODFramework::LoggingLevel::DEBUG: + return MOBase::log::Debug; + case OMODFramework::LoggingLevel::INFO: + return MOBase::log::Info; + case OMODFramework::LoggingLevel::WARNING: + return MOBase::log::Warning; + case OMODFramework::LoggingLevel::ERROR: + return MOBase::log::Error; + } +} + +void Logger::Init() +{ + //no op +} + +void Logger::Log(OMODFramework::LoggingLevel level, System::String^ message, System::DateTime time) +{ + MOBase::log::getDefault().log(MOLoggingLevel(level), "{}", toUTF8String(message)); +} diff --git a/libs/installer_omod/src/implementations/Logger.h b/libs/installer_omod/src/implementations/Logger.h new file mode 100644 index 0000000..e3d1e7e --- /dev/null +++ b/libs/installer_omod/src/implementations/Logger.h @@ -0,0 +1,17 @@ +#pragma once + +using namespace cli; + +#include <uibase/log.h> + +ref class Logger : OMODFramework::ILogger +{ +public: + static OMODFramework::LoggingLevel OMODLoggingLevel(MOBase::log::Levels level); + + static MOBase::log::Levels MOLoggingLevel(OMODFramework::LoggingLevel level); + + virtual void Init(); + + virtual void Log(OMODFramework::LoggingLevel level, System::String^ message, System::DateTime time); +}; diff --git a/libs/installer_omod/src/implementations/ScriptFunctions.cpp b/libs/installer_omod/src/implementations/ScriptFunctions.cpp new file mode 100644 index 0000000..be4deb3 --- /dev/null +++ b/libs/installer_omod/src/implementations/ScriptFunctions.cpp @@ -0,0 +1,320 @@ +#include "ScriptFunctions.h" + +#include <QApplication> +#include <QDir> +#include <QGridLayout> +#include <QInputDialog> +#include <QImageReader> +#include <QLabel> +#include <QMessageBox> +#include <QScreen> + +#include <uibase/iplugingame.h> +#include <uibase/ipluginlist.h> +#include <uibase/log.h> + +#include "../interop/QtDotNetConverters.h" +#include "../newstuff/rtfPopup.h" +#include "../oldstuff/DialogSelect.h" + +ScriptFunctionsHelper::ScriptFunctionsHelper() : mMessageBoxHelper(make_unique<MessageBoxHelper>()) +{ + moveToThread(QApplication::instance()->thread()); + + connect(this, &ScriptFunctionsHelper::DialogSelectSignal, this, &ScriptFunctionsHelper::DialogSelectSlot, Qt::BlockingQueuedConnection); + connect(this, &ScriptFunctionsHelper::InputStringSignal, this, &ScriptFunctionsHelper::InputStringSlot, Qt::BlockingQueuedConnection); + connect(this, &ScriptFunctionsHelper::DisplayImageSignal, this, &ScriptFunctionsHelper::DisplayImageSlot, Qt::BlockingQueuedConnection); + connect(this, &ScriptFunctionsHelper::DisplayTextSignal, this, &ScriptFunctionsHelper::DisplayTextSlot, Qt::BlockingQueuedConnection); +} + +std::optional<QVector<int>> ScriptFunctionsHelper::DialogSelect(QWidget* parent, const QString& title, const QVector<QString>& items, const QVector<QString>& descriptions, const QVector<QString>& pixmaps, bool multiSelect) +{ + std::optional<QVector<int>> result; + emit DialogSelectSignal(result, parent, title, items, descriptions, pixmaps, multiSelect); + return result; +} + +QString ScriptFunctionsHelper::InputString(QWidget* parentWidget, const QString& title, const QString& initialText) +{ + QString text; + emit InputStringSignal(text, parentWidget, title, initialText); + return text; +} + +void ScriptFunctionsHelper::DisplayImage(QWidget* parentWidget, const QString& path, const QString& title) +{ + emit DisplayImageSignal(parentWidget, path, title); +} + +void ScriptFunctionsHelper::DisplayText(QWidget* parentWidget, const QString& path, const QString& title) +{ + emit DisplayTextSignal(parentWidget, path, title); +} + +void ScriptFunctionsHelper::InputStringSlot(QString& textOut, QWidget* parentWidget, const QString& title, const QString& initialText) +{ + textOut = QInputDialog::getText(parentWidget, title, title, QLineEdit::Normal, initialText); +} + +void ScriptFunctionsHelper::DisplayImageSlot(QWidget* parentWidget, const QString& path, const QString& title) +{ + QImageReader reader(path); + QImage image = reader.read(); + if (!image.isNull()) + { + QPixmap pixmap = QPixmap::fromImage(image); + MOBase::log::debug("image size {}, pixmap size {}", image.size(), pixmap.size()); + QDialog popup; + QLayout* layout = new QGridLayout(&popup); + popup.setLayout(layout); + FixedAspectRatioImageLabel* label = new FixedAspectRatioImageLabel(&popup); + label->setUnscaledPixmap(pixmap); + + QSize screenSize = parentWidget->screen()->availableSize(); + int maxHeight = static_cast<int>(screenSize.height() * 0.8f); + if (pixmap.size().height() > maxHeight) + // This is approximate due to borders, label can sort out details. + popup.resize(label->widthForHeight(maxHeight), maxHeight); + + layout->addWidget(label); + popup.setWindowTitle(title); + popup.exec(); + } + else + MOBase::log::error("Unable to display {}. Error was {}: {}", path, reader.error(), reader.errorString()); +} + +void ScriptFunctionsHelper::DisplayTextSlot(QWidget* parentWidget, const QString& path, const QString& title) +{ + RtfPopup popup(toDotNetString(path), parentWidget); + popup.setWindowTitle(title); + // the size readmes are becoming automatically + popup.resize(492, 366); + popup.exec(); +} + +void ScriptFunctionsHelper::DialogSelectSlot(std::optional<QVector<int>>& resultOut, QWidget* parent, const QString& title, const QVector<QString>& items, + const QVector<QString>& descriptions, const QVector<QString>& pixmaps, bool multiSelect) +{ + resultOut = ::DialogSelect(parent, title, items, descriptions, pixmaps, multiSelect); +} + +ScriptFunctions::ScriptFunctions(QWidget* parentWidget, MOBase::IOrganizer* moInfo) : mParentWidget(parentWidget), mMoInfo(moInfo), mHelper(new ScriptFunctionsHelper) {} + +ScriptFunctions::~ScriptFunctions() +{ + if (!mHelper) + return; + this->!ScriptFunctions(); +} + +ScriptFunctions::!ScriptFunctions() +{ + mHelper->deleteLater(); + mHelper = nullptr; +} + +void ScriptFunctions::Warn(System::String^ msg) +{ + mHelper->warning(mParentWidget, "Warning", toQString(msg)); +} + +void ScriptFunctions::Message(System::String^ msg) +{ + mHelper->information(mParentWidget, "Message", toQString(msg)); +} + +void ScriptFunctions::Message(System::String^ msg, System::String^ title) +{ + mHelper->information(mParentWidget, toQString(title), toQString(msg)); +} + +System::Collections::Generic::List<int>^ ScriptFunctions::Select(System::Collections::Generic::List<System::String^>^ items, + System::String^ title, + bool isMultiSelect, + System::Collections::Generic::List<System::String^>^ previews, + System::Collections::Generic::List<System::String^>^ descriptions) +{ + QVector<QString> qItems; + qItems.reserve(items ? items->Count : 0); + QVector<QString> qPreviews; + qPreviews.reserve(previews ? previews->Count : 0); + QVector<QString> qDescriptions; + qDescriptions.reserve(descriptions ? descriptions->Count : 0); + + // Expect red squiggles. No one told intellisense about this syntax, but it's the least ugly. + if (items) + { + for each (System::String ^ item in items) + qItems.push_back(toQString(item)); + } + + if (previews) + { + for each (System::String ^ preview in previews) + qPreviews.push_back(toQString(preview)); + } + + if (descriptions) + { + for each (System::String ^ description in descriptions) + qDescriptions.push_back(toQString(description)); + } + + std::optional<QVector<int>> qResponse = mHelper->DialogSelect(mParentWidget, toQString(title), qItems, qDescriptions, qPreviews, isMultiSelect); + if (!qResponse.has_value()) + return nullptr; + + System::Collections::Generic::List<int>^ response = gcnew System::Collections::Generic::List<int>(qResponse.value().length()); + for (const auto selection : qResponse.value()) + response->Add(selection); + return response; +} + +System::String^ ScriptFunctions::InputString(System::String^ title, System::String^ initialText) +{ + return toDotNetString(mHelper->InputString(mParentWidget, toQString(title), initialText ? toQString(initialText) : "")); +} + +int ScriptFunctions::DialogYesNo(System::String^ message) +{ + return mHelper->question(mParentWidget, "", toQString(message)) == QMessageBox::StandardButton::Yes; +} + +int ScriptFunctions::DialogYesNo(System::String^ message, System::String^ title) +{ + return mHelper->question(mParentWidget, toQString(title), toQString(message)) == QMessageBox::StandardButton::Yes; +} + +void ScriptFunctions::DisplayImage(System::String^ path, System::String^ title) +{ + mHelper->DisplayImage(mParentWidget, toQString(path), toQString(title)); +} + +void ScriptFunctions::DisplayText(System::String^ text, System::String^ title) +{ + mHelper->DisplayText(mParentWidget, toQString(text), toQString(title)); +} + +void ScriptFunctions::Patch(System::String^ from, System::String^ to) +{ + throw gcnew System::NotImplementedException(); +} + +System::String^ ScriptFunctions::ReadOblivionINI(System::String^ section, System::String^ name) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +System::String^ ScriptFunctions::ReadRendererInfo(System::String^ name) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +bool ScriptFunctions::DataFileExists(System::String^ path) +{ + return mMoInfo->resolvePath(toQString(path)) != ""; +} + +bool ScriptFunctions::HasScriptExtender() +{ + for (const auto& forcedLoad : mMoInfo->managedGame()->executableForcedLoads()) + { + if (forcedLoad.library().toLower().startsWith("obse")) + { + if (mMoInfo->managedGame()->gameDirectory().exists(forcedLoad.library())) + return true; + } + } + + return mMoInfo->managedGame()->gameDirectory().exists("obse_loader.exe"); +} + +bool ScriptFunctions::HasGraphicsExtender() +{ + return DataFileExists("obse\\plugins\\obge.dll"); +} + +System::Version^ ScriptFunctions::ScriptExtenderVersion() +{ + QString obsePath; + for (const auto& forcedLoad : mMoInfo->managedGame()->executableForcedLoads()) + { + if (forcedLoad.library().toLower().startsWith("obse")) + { + if (mMoInfo->managedGame()->gameDirectory().exists(forcedLoad.library())) + { + obsePath = mMoInfo->managedGame()->gameDirectory().filePath(forcedLoad.library()); + break; + } + } + } + + if (obsePath.isEmpty()) + obsePath = mMoInfo->managedGame()->gameDirectory().filePath("obse_loader.exe"); + + System::Diagnostics::FileVersionInfo^ info = System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(obsePath)); + return gcnew System::Version(info->FileMajorPart, info->FileMinorPart, info->FileBuildPart, info->FilePrivatePart); +} + +System::Version^ ScriptFunctions::GraphicsExtenderVersion() +{ + return gcnew System::Version(System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(mMoInfo->resolvePath("obse\\plugins\\obge.dll")))->FileVersion); +} + +System::Version^ ScriptFunctions::OblivionVersion() +{ + return gcnew System::Version(System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(mMoInfo->managedGame()->gameDirectory().filePath("oblivion.exe")))->FileVersion); +} + +System::Version^ ScriptFunctions::OBSEPluginVersion(System::String^ path) +{ + QString pluginPath = mMoInfo->resolvePath(toQString(System::IO::Path::Combine("obse", "plugins", System::IO::Path::ChangeExtension(path, ".dll")))); + if (pluginPath.isEmpty()) + return nullptr; + return gcnew System::Version(System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(pluginPath))->FileVersion); +} + +System::Collections::Generic::IEnumerable<OMODFramework::Scripting::ScriptESP>^ ScriptFunctions::GetESPs() +{ + QStringList plugins = mMoInfo->pluginList()->pluginNames(); + System::Collections::Generic::List<OMODFramework::Scripting::ScriptESP>^ pluginList = gcnew System::Collections::Generic::List<OMODFramework::Scripting::ScriptESP>(plugins.count()); + for (const auto& pluginName : plugins) + { + auto state = mMoInfo->pluginList()->state(pluginName); + if (state != MOBase::IPluginList::PluginState::STATE_MISSING) + { + OMODFramework::Scripting::ScriptESP plugin; + plugin.Name = toDotNetString(pluginName); + plugin.Active = state == MOBase::IPluginList::PluginState::STATE_ACTIVE; + pluginList->Add(plugin); + } + } + return pluginList; +} + +System::Collections::Generic::IEnumerable<System::String^>^ ScriptFunctions::GetActiveOMODNames() +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. No known OMODs seem to actually use this (which is irritating as this is one of OBMM's most powerful features). +} + +cli::array<unsigned char, 1>^ ScriptFunctions::ReadExistingDataFile(System::String^ file) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +cli::array<unsigned char, 1>^ ScriptFunctions::GetDataFileFromBSA(System::String^ file) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +cli::array<unsigned char, 1>^ ScriptFunctions::GetDataFileFromBSA(System::String^ bsa, System::String^ file) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} diff --git a/libs/installer_omod/src/implementations/ScriptFunctions.h b/libs/installer_omod/src/implementations/ScriptFunctions.h new file mode 100644 index 0000000..28f9dee --- /dev/null +++ b/libs/installer_omod/src/implementations/ScriptFunctions.h @@ -0,0 +1,122 @@ +#pragma once + +using namespace cli; + +#include <optional> + +#include <QWidget> + +#include <uibase/imoinfo.h> + +#include "../MessageBoxHelper.h" +#include "../QObject_unique_ptr.h" + +class ScriptFunctionsHelper : public QObject +{ + Q_OBJECT + +public: + ScriptFunctionsHelper(); + + // don't bother with std::forward and decltype(auto) as we know everything is fine being copied + // in fact, as some stuff lives on the managed heap, we can't take a reference anyway + template<typename... Args> auto critical(Args... args) { return mMessageBoxHelper->critical(args...); } + template<typename... Args> auto information(Args... args) { return mMessageBoxHelper->information(args...); } + template<typename... Args> auto question(Args... args) { return mMessageBoxHelper->question(args...); } + template<typename... Args> auto warning(Args... args) { return mMessageBoxHelper->warning(args...); } + + std::optional<QVector<int>> DialogSelect(QWidget* parent, const QString& title, const QVector<QString>& items, + const QVector<QString>& descriptions, const QVector<QString>& pixmaps, + bool multiSelect); + + QString InputString(QWidget* parentWidget, const QString& title, const QString& initialText); + + void DisplayImage(QWidget* parentWidget, const QString& path, const QString& title); + + void DisplayText(QWidget* parentWidget, const QString& path, const QString& title); + +signals: + void DialogSelectSignal(std::optional<QVector<int>>& resultOut, QWidget* parent, const QString& title, const QVector<QString>& items, + const QVector<QString>& descriptions, const QVector<QString>& pixmaps, bool multiSelect); + + void InputStringSignal(QString& textOut, QWidget* parentWidget, const QString& title, const QString& initialText); + + void DisplayImageSignal(QWidget* parentWidget, const QString& path, const QString& title); + + void DisplayTextSignal(QWidget* parentWidget, const QString& path, const QString& title); + +public slots: + void DialogSelectSlot(std::optional<QVector<int>>& resultOut, QWidget* parent, const QString& title, const QVector<QString>& items, + const QVector<QString>& descriptions, const QVector<QString>& pixmaps, bool multiSelect); + + void InputStringSlot(QString& textOut, QWidget* parentWidget, const QString& title, const QString& initialText); + + void DisplayImageSlot(QWidget* parentWidget, const QString& path, const QString& title); + + void DisplayTextSlot(QWidget* parentWidget, const QString& path, const QString& title); + +private: + QObject_unique_ptr<MessageBoxHelper> mMessageBoxHelper; +}; + +ref class ScriptFunctions : OMODFramework::Scripting::IScriptFunctions +{ +public: + ScriptFunctions(QWidget* parentWidget, MOBase::IOrganizer* moInfo); + ~ScriptFunctions(); + !ScriptFunctions(); + + // note: C++/CLI wants virtual for interface implementations, not override + virtual void Warn(System::String^ msg); + + virtual void Message(System::String^ msg); + + virtual void Message(System::String^ msg, System::String^ title); + + virtual System::Collections::Generic::List<int>^ Select(System::Collections::Generic::List<System::String^>^ items, System::String^ title, bool isMultiSelect, System::Collections::Generic::List<System::String^>^ previews, System::Collections::Generic::List<System::String^>^ descriptions); + + virtual System::String^ InputString(System::String^ title, System::String^ initialText); + + virtual int DialogYesNo(System::String^ message); + + virtual int DialogYesNo(System::String^ message, System::String^ title); + + virtual void DisplayImage(System::String^ path, System::String^ title); + + virtual void DisplayText(System::String^ text, System::String^ title); + + virtual void Patch(System::String^ from, System::String^ to); + + virtual System::String^ ReadOblivionINI(System::String^ section, System::String^ name); + + virtual System::String^ ReadRendererInfo(System::String^ name); + + virtual bool DataFileExists(System::String^ path); + + virtual bool HasScriptExtender(); + + virtual bool HasGraphicsExtender(); + + virtual System::Version^ ScriptExtenderVersion(); + + virtual System::Version^ GraphicsExtenderVersion(); + + virtual System::Version^ OblivionVersion(); + + virtual System::Version^ OBSEPluginVersion(System::String^ path); + + virtual System::Collections::Generic::IEnumerable<OMODFramework::Scripting::ScriptESP>^ GetESPs(); + + virtual System::Collections::Generic::IEnumerable<System::String^>^ GetActiveOMODNames(); + + virtual cli::array<unsigned char, 1>^ ReadExistingDataFile(System::String^ file); + + virtual cli::array<unsigned char, 1>^ GetDataFileFromBSA(System::String^ file); + + virtual cli::array<unsigned char, 1>^ GetDataFileFromBSA(System::String^ bsa, System::String^ file); + +private: + QWidget* mParentWidget; + MOBase::IOrganizer* mMoInfo; + ScriptFunctionsHelper* mHelper; +}; diff --git a/libs/installer_omod/src/installerOmod.cpp b/libs/installer_omod/src/installerOmod.cpp new file mode 100644 index 0000000..3e4c390 --- /dev/null +++ b/libs/installer_omod/src/installerOmod.cpp @@ -0,0 +1,204 @@ +#include "installerOmod.h" + +#include <QRegularExpression> +#include <QTemporaryFile> + +#include <uibase/iplugingame.h> +#include <uibase/log.h> + +#include "OMODFrameworkWrapper.h" + +InstallerOMOD::InstallerOMOD() : mMoInfo(nullptr), mOmodFrameworkWrapper(nullptr) +{ +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IPlugin + +bool InstallerOMOD::init(MOBase::IOrganizer* moInfo) +{ + mMoInfo = moInfo; + mOmodFrameworkWrapper = std::make_unique<OMODFrameworkWrapper>(mMoInfo); + mMoInfo->onAboutToRun([this](const QString&) { buildSDPs(); return true; }); + mMoInfo->onFinishedRun([this](const QString&, unsigned int) { clearSDPs(); }); + return true; +} + +QString InstallerOMOD::name() const +{ + return "Omod Installer"; +} + +QString InstallerOMOD::localizedName() const +{ + return tr("Omod Installer"); +} + +QString InstallerOMOD::author() const +{ + return "AnyOldName3 & erril120"; +} + +QString InstallerOMOD::description() const +{ + return tr("Installer for Omod files (including scripted ones)"); +} + +MOBase::VersionInfo InstallerOMOD::version() const +{ + return MOBase::VersionInfo(1, 1, 0, MOBase::VersionInfo::RELEASE_FINAL); +} + +std::vector<std::shared_ptr<const MOBase::IPluginRequirement>> InstallerOMOD::requirements() const +{ + return { Requirements::gameDependency("Oblivion") }; +} + +QList<MOBase::PluginSetting> InstallerOMOD::settings() const +{ + return {}; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IPluginInstaller + +unsigned int InstallerOMOD::priority() const +{ + // Some other installers have a use_any_file setting and then they'll try and claim OMODs as their own, so we want higher priority than them. + return 500; +} + +bool InstallerOMOD::isManualInstaller() const +{ + return false; +} + +void InstallerOMOD::onInstallationEnd(EInstallResult result, MOBase::IModInterface* newMod) +{ + mOmodFrameworkWrapper->onInstallationEnd(result, newMod); +} + +bool InstallerOMOD::isArchiveSupported(std::shared_ptr<const MOBase::IFileTree> tree) const +{ + for (const auto file : *tree) { + // config is the only file guaranteed to be there + if (file->isFile() && file->name() == "config") + return true; + } + return false; +} + +void InstallerOMOD::setParentWidget(QWidget* parent) +{ + IPluginInstaller::setParentWidget(parent); + mOmodFrameworkWrapper->setParentWidget(parent); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IPluginInstallerCustom + +bool InstallerOMOD::isArchiveSupported(const QString& archiveName) const +{ + return archiveName.endsWith(".omod", Qt::CaseInsensitive); +} + +std::set<QString> InstallerOMOD::supportedExtensions() const +{ + return { "omod" }; +} + +InstallerOMOD::EInstallResult InstallerOMOD::install(MOBase::GuessedValue<QString>& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) +{ + return mOmodFrameworkWrapper->installInAnotherThread(modName, gameName, archiveName, version, nexusID); +} + +MappingType InstallerOMOD::mappings() const +{ + MOBase::log::debug("Mapping virtual SDPs"); + + MappingType mappings; + + for (const auto& [realSDPPath, virtualSDP] : mVirtualSDPs) + mappings.push_back({ virtualSDP->fileName(), realSDPPath, false, false }); + + return mappings; +} + +void InstallerOMOD::buildSDPs() +{ + QStringList directories = mMoInfo->listDirectories("Shaders/OMOD"); + directories = directories.filter(QRegularExpression("\\d+")); + + for (const auto& directory : directories) + { + QString packagePath = mMoInfo->resolvePath(QString("Shaders/shaderpackage%1.sdp").arg(directory.toInt(), 3, 10, QChar('0'))); + if (packagePath.isEmpty()) + continue; + + QMap<QByteArray, QByteArray> shaders; + { + QFile packageFile(packagePath); + packageFile.open(QIODevice::ReadOnly); + QDataStream basePackageStream(&packageFile); + basePackageStream.setByteOrder(QDataStream::LittleEndian); + quint32 magic; + basePackageStream >> magic; + if (magic != 100) + { + MOBase::log::debug("SDP magic number mismatch in {}, got {}, but expected 100", packagePath, magic); + continue; + } + quint32 shaderCount, totalSize; + basePackageStream >> shaderCount >> totalSize; + for (quint32 i = 0; i < shaderCount && !basePackageStream.atEnd(); ++i) + { + QByteArray shaderName(256, '\0'); + basePackageStream.readRawData(shaderName.data(), 256); + quint32 shaderSize; + basePackageStream >> shaderSize; + QByteArray shaderBytecode(shaderSize, '\0'); + basePackageStream.readRawData(shaderBytecode.data(), shaderSize); + shaders[shaderName] = shaderBytecode; + } + } + + QStringList shaderFiles = mMoInfo->findFiles("Shaders/OMOD/" + directory, { "*.vso", "*.pso" }); + for (const auto& shaderFile : shaderFiles) + { + QFileInfo info(shaderFile); + QByteArray shaderName = (info.baseName().toUpper() + "." + info.suffix().toLower()).toLatin1(); + QFile file(shaderFile); + file.open(QIODevice::ReadOnly); + shaderName = shaderName.leftJustified(256, '\0', true); + shaders[shaderName] = file.readAll(); + MOBase::log::debug("Replacement for {} was {} bytes long", shaderFile, shaders[shaderName].size()); + } + + std::unique_ptr<QTemporaryFile> outputFile = std::make_unique<QTemporaryFile>(); + outputFile->open(); + QDataStream packageStream(outputFile.get()); + packageStream.setByteOrder(QDataStream::LittleEndian); + packageStream << quint32(100) << quint32(shaders.count()); + + quint32 totalSize = 0; + for (const auto& shader : shaders) + totalSize += 256 + 4 + shader.size(); + packageStream << totalSize; + + for (auto itr = shaders.cbegin(); itr != shaders.cend(); ++itr) + { + packageStream.writeRawData(itr.key().constData(), 256); + packageStream << quint32(itr.value().size()); + packageStream.writeRawData(itr.value().constData(), itr.value().size()); + } + + outputFile->close(); + + mVirtualSDPs.emplace(mMoInfo->managedGame()->dataDirectory().absoluteFilePath(QString("Shaders/shaderpackage%1.sdp").arg(directory.toInt(), 3, 10, QChar('0'))), std::move(outputFile)); + } +} + +void InstallerOMOD::clearSDPs() +{ + mVirtualSDPs.clear(); +} diff --git a/libs/installer_omod/src/installerOmod.h b/libs/installer_omod/src/installerOmod.h new file mode 100644 index 0000000..a122355 --- /dev/null +++ b/libs/installer_omod/src/installerOmod.h @@ -0,0 +1,75 @@ +#ifndef INSTALLEROMOD_H +#define INSTALLEROMOD_H + +#include <QTemporaryFile> + +#include <uibase/iplugininstallercustom.h> +#include <uibase/ipluginfilemapper.h> + +#include "OMODFrameworkWrapper.h" + +class InstallerOMOD : public MOBase::IPluginInstallerCustom, public MOBase::IPluginFileMapper +{ + Q_OBJECT; + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerCustom MOBase::IPluginFileMapper); +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + // We should probably buy a domain. + Q_PLUGIN_METADATA(IID "org.AnyOldName3.InstallerOmod"); +#endif + +public: + InstallerOMOD(); + + // IPlugin + bool init(MOBase::IOrganizer* moInfo) override; + + QString name() const override; + + QString localizedName() const override; + + QString author() const override; + + QString description() const override; + + MOBase::VersionInfo version() const override; + + std::vector<std::shared_ptr<const MOBase::IPluginRequirement>> requirements() const override; + + QList<MOBase::PluginSetting> settings() const override; + + // IPluginInstaller + + unsigned int priority() const override; + + bool isManualInstaller() const override; + + void onInstallationEnd(EInstallResult result, MOBase::IModInterface* newMod) override; + + bool isArchiveSupported(std::shared_ptr<const MOBase::IFileTree> tree) const override; + + void setParentWidget(QWidget* parent) override; + + // IPluginInstallerCustom + + bool isArchiveSupported(const QString& archiveName) const override; + + std::set<QString> supportedExtensions() const override; + + EInstallResult install(MOBase::GuessedValue<QString>& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) override; + + // IPluginFileMapper + + MappingType mappings() const override; + +protected: + void buildSDPs(); + + void clearSDPs(); + +private: + MOBase::IOrganizer* mMoInfo; + std::unique_ptr<OMODFrameworkWrapper> mOmodFrameworkWrapper; + std::map<QString, std::unique_ptr<QTemporaryFile>> mVirtualSDPs; +}; + +#endif // !INSTALLEROMOD_H diff --git a/libs/installer_omod/src/installer_omod_en.ts b/libs/installer_omod/src/installer_omod_en.ts new file mode 100644 index 0000000..a092611 --- /dev/null +++ b/libs/installer_omod/src/installer_omod_en.ts @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="en_US"> +<context> + <name>InstallerOMOD</name> + <message> + <location filename="installerOmod.cpp" line="34"/> + <source>Omod Installer</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="installerOmod.cpp" line="44"/> + <source>Installer for Omod files (including scripted ones)</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>NameDialog</name> + <message> + <location filename="newstuff/namedialog.ui" line="14"/> + <source>Pick mod name</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="newstuff/namedialog.ui" line="22"/> + <source>Name</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="newstuff/namedialog.ui" line="43"/> + <location filename="newstuff/namedialog.ui" line="46"/> + <source>Manual install is unavailable for OMODs.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="newstuff/namedialog.ui" line="49"/> + <source>Manual</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="newstuff/namedialog.ui" line="69"/> + <source>OK</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="newstuff/namedialog.ui" line="79"/> + <source>Cancel</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>OMODFrameworkWrapper</name> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="365"/> + <source>%1 wants to change [%2] %3 from "%4" to "%5"</source> + <extracomment>%1 is the mod name [%2] is the ini section name. %3 is the ini setting name. %4 is the value already in Oblivion.ini. %5 is the value the mod wants to set.</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="374"/> + <source>%1 wants to set [%2] %3 to "%4"</source> + <extracomment>%1 is the mod name [%2] is the ini section name. %3 is the ini setting name. %5 is the value the mod wants to set.</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="377"/> + <source>Update INI?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="468"/> + <source>%1 has data for %2, but Mod Organizer 2 doesn't know what to do with it yet. Please report this to the Mod Organizer 2 development team (ideally by sending us your interface log) as we didn't find any OMODs that actually did this, and we need to know that they exist.</source> + <extracomment>%1 is the mod name %2 is the name of a field in the OMOD's return data Hopefully this message will never be seen by anyone, but if it is, they need to know to tell the Mod Organizer 2 dev team.</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="471"/> + <source>Mod Organizer 2 can't completely install this OMOD.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="553"/> + <source>Activate mod?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="557"/> + <source>%1 contains the OMOD %2. OMODs may have post-installation actions like activating ESPs. Would you like to enable the mod so this can happen now?</source> + <extracomment>%1 is the left-pane mod name. %2 is the name from the metadata of an OMOD.</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="587"/> + <source>%1 wants to activate %2. Do you want to do so?</source> + <extracomment>%1 is the mod name. %2 is the plugin name.</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="589"/> + <location filename="OMODFrameworkWrapper.cpp" line="632"/> + <source>Activate plugin?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="601"/> + <source>OMOD wants to activate missing plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="601"/> + <source>An OMOD wants to activate a missing plugin. This shouldn't be possible. Please report this to a MO2 developer.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="630"/> + <source>%1 installed %2, but doesn't activate it by default. Do you want to activate it anyway?</source> + <extracomment>%1 is the mod name. %2 is the plugin name.</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="644"/> + <source>OMOD claimed to install missing plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="644"/> + <source>An OMOD has activation settings for a missing plugin. This shouldn't be possible. Please report this to a MO2 developer.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="666"/> + <source>Register BSAs</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="670"/> + <source>%1 wants to register the following BSA archives, but Mod Organizer 2 can't do that yet due to technical limitations:<ul><li>%2</li></ul>For now, your options include adding the BSA names to <code>sResourceArchiveList</code> in the game INI, creating a dummy ESP with the same name, or extracting the BSA, all of which have drawbacks.</source> + <extracomment>%1 is the OMOD name <ul><li>%2</li></ul> becomes a list of BSA files</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="745"/> + <source>Display Readme?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="747"/> + <source>The Readme may explain installation options. Display it?<br>It will remain visible until you close it.</source> + <extracomment><br> is a line break. Translators can remove it if it makes things clearer.</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="OMODFrameworkWrapper.cpp" line="753"/> + <source>%1 Readme</source> + <extracomment>%1 is the mod name</extracomment> + <translation type="unfinished"></translation> + </message> +</context> +</TS> diff --git a/libs/installer_omod/src/interop/QtDotNetConverters.cpp b/libs/installer_omod/src/interop/QtDotNetConverters.cpp new file mode 100644 index 0000000..430e8e5 --- /dev/null +++ b/libs/installer_omod/src/interop/QtDotNetConverters.cpp @@ -0,0 +1,17 @@ +#include "QtDotNetConverters.h" + +#include <string> + +#include <msclr\marshal_cppstd.h> + +QString toQString(System::String^ string) +{ + if (string) + return QString::fromStdWString(msclr::interop::marshal_as<std::wstring>(string)); + return QString(); +} + +System::String^ toDotNetString(const QString& qString) +{ + return qString.isNull() ? nullptr : msclr::interop::marshal_as<System::String^>(qString.toStdWString()); +} diff --git a/libs/installer_omod/src/interop/QtDotNetConverters.h b/libs/installer_omod/src/interop/QtDotNetConverters.h new file mode 100644 index 0000000..6cca053 --- /dev/null +++ b/libs/installer_omod/src/interop/QtDotNetConverters.h @@ -0,0 +1,9 @@ +#pragma once + +using namespace cli; + +#include <QString> + +QString toQString(System::String^ string); + +System::String^ toDotNetString(const QString& qString); diff --git a/libs/installer_omod/src/interop/StdDotNetConverters.cpp b/libs/installer_omod/src/interop/StdDotNetConverters.cpp new file mode 100644 index 0000000..be7497b --- /dev/null +++ b/libs/installer_omod/src/interop/StdDotNetConverters.cpp @@ -0,0 +1,35 @@ +#include "stdDotNetConverters.h" + +#include <stdexcept> + +#include <msclr\marshal_cppstd.h> + +std::wstring toWString(System::String^ string) +{ + if (string) + return msclr::interop::marshal_as<std::wstring>(string); + return L"null .NET string"; +} + +System::String^ toDotNetString(const std::wstring& wString) +{ + return msclr::interop::marshal_as<System::String^>(wString); +} + +std::string toUTF8String(System::String^ string) +{ + if (string) + { + array<unsigned char>^ utf8 = System::Text::Encoding::UTF8->GetBytes(string); + std::string stdstring; + stdstring.resize(utf8->Length); + System::Runtime::InteropServices::Marshal::Copy(utf8, 0, System::IntPtr(stdstring.data()), utf8->Length); + return stdstring; + } + return "null .NET string"; +} + +std::exception toStdException(System::Exception^ exception) +{ + return std::runtime_error("Unhanded .NET Exception: " + toUTF8String(exception->ToString())); +} diff --git a/libs/installer_omod/src/interop/StdDotNetConverters.h b/libs/installer_omod/src/interop/StdDotNetConverters.h new file mode 100644 index 0000000..04c2ada --- /dev/null +++ b/libs/installer_omod/src/interop/StdDotNetConverters.h @@ -0,0 +1,13 @@ +#pragma once + +using namespace cli; + +#include <string> + +std::wstring toWString(System::String^ string); + +std::string toUTF8String(System::String^ string); + +System::String^ toDotNetString(const std::wstring& wString); + +std::exception toStdException(System::Exception^ exception); diff --git a/libs/installer_omod/src/newstuff/namedialog.cpp b/libs/installer_omod/src/newstuff/namedialog.cpp new file mode 100644 index 0000000..4dd44d6 --- /dev/null +++ b/libs/installer_omod/src/newstuff/namedialog.cpp @@ -0,0 +1,37 @@ +#include "namedialog.h" + +#include <QCompleter> + +NameDialog::NameDialog(const MOBase::GuessedValue<QString>& suggestedNames, QWidget* parent) + : QDialog(parent), ui(), mName(suggestedNames) +{ + ui.setupUi(this); + + for (const auto& name : suggestedNames.variants()) + ui.nameCombo->addItem(name); + + ui.nameCombo->setCurrentIndex(ui.nameCombo->findText(suggestedNames)); + + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + ui.nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); +} + +QString NameDialog::getName() const +{ + return mName; +} + +void NameDialog::on_okBtn_clicked() +{ + accept(); +} + +void NameDialog::on_cancelBtn_clicked() +{ + reject(); +} + +void NameDialog::on_nameCombo_currentTextChanged(const QString& text) +{ + mName = text; +} diff --git a/libs/installer_omod/src/newstuff/namedialog.h b/libs/installer_omod/src/newstuff/namedialog.h new file mode 100644 index 0000000..72e62eb --- /dev/null +++ b/libs/installer_omod/src/newstuff/namedialog.h @@ -0,0 +1,28 @@ +#pragma once + +#include <QDialog> + +#include <uibase/guessedvalue.h> + +#include "ui_namedialog.h" + +class NameDialog : public QDialog +{ + Q_OBJECT + +public: + NameDialog(const MOBase::GuessedValue<QString>& suggestedNames, QWidget* parent = nullptr); + + QString getName() const; + +private slots: + void on_okBtn_clicked(); + + void on_cancelBtn_clicked(); + + void on_nameCombo_currentTextChanged(const QString& text); + +private: + Ui::NameDialog ui; + QString mName; +}; diff --git a/libs/installer_omod/src/newstuff/namedialog.ui b/libs/installer_omod/src/newstuff/namedialog.ui new file mode 100644 index 0000000..5727d1a --- /dev/null +++ b/libs/installer_omod/src/newstuff/namedialog.ui @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NameDialog</class> + <widget class="QDialog" name="NameDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>83</height> + </rect> + </property> + <property name="windowTitle"> + <string>Pick mod name</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Name</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="nameCombo"> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="manualBtn"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Manual install is unavailable for OMODs.</string> + </property> + <property name="whatsThis"> + <string>Manual install is unavailable for OMODs.</string> + </property> + <property name="text"> + <string>Manual</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="okBtn"> + <property name="text"> + <string>OK</string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="cancelBtn"> + <property name="text"> + <string>Cancel</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/libs/installer_omod/src/newstuff/rtfPopup.cpp b/libs/installer_omod/src/newstuff/rtfPopup.cpp new file mode 100644 index 0000000..476f078 --- /dev/null +++ b/libs/installer_omod/src/newstuff/rtfPopup.cpp @@ -0,0 +1,33 @@ +#include "rtfPopup.h" + +#include <QGridLayout> +#include <QLabel> +#include <QRegularExpression> +#include <QScrollArea> +#include <QTextDocument> + +#include "../interop/QtDotNetConverters.h" + +RtfPopup::RtfPopup(System::String^ rtfText, QWidget* parent, Qt::WindowFlags f) : QDialog(parent, f) +{ + QString text = rtfText->StartsWith("{\\rtf") ? toQString(RtfPipe::Rtf::ToHtml(rtfText)) : Qt::convertFromPlainText(toQString(rtfText), Qt::WhiteSpaceNormal); + QRegularExpression urlFinder(R"REGEX((?<!(?:href="))((?:(?:https?|ftp|file)://|www\.|ftp\.)(?:\([-A-Z0-9+@#/%=~_|$?!:,.]|(?:&)*\)|[-A-Z0-9+@#/%=~_|$?!:,.]|(?:&))*(?:\([-A-Z0-9+@#/%=~+|$?!:,.]|(?:&)*\)|[A-Z0-9+@#/%=~_|$]|(?:&))))REGEX", QRegularExpression::CaseInsensitiveOption | QRegularExpression::MultilineOption); + text.replace(urlFinder, R"(<a href="\1">\1</a>)"); + + QLayout* layout = new QGridLayout(this); + setLayout(layout); + QScrollArea* scrollArea = new QScrollArea(this); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + layout->addWidget(scrollArea); + + QLabel* label = new QLabel(text, scrollArea); + label->setWordWrap(true); + label->setTextFormat(Qt::RichText); + label->setOpenExternalLinks(true); + label->setTextInteractionFlags(Qt::TextBrowserInteraction); + scrollArea->setWidget(label); + scrollArea->setWidgetResizable(true); + + setSizeGripEnabled(true); +} diff --git a/libs/installer_omod/src/newstuff/rtfPopup.h b/libs/installer_omod/src/newstuff/rtfPopup.h new file mode 100644 index 0000000..17ca10c --- /dev/null +++ b/libs/installer_omod/src/newstuff/rtfPopup.h @@ -0,0 +1,11 @@ +using namespace cli; + +#include <QDialog> + +class RtfPopup : public QDialog +{ + Q_OBJECT + +public: + RtfPopup(System::String^ rtfText, QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); +}; diff --git a/libs/installer_omod/src/oldstuff/DialogSelect.cpp b/libs/installer_omod/src/oldstuff/DialogSelect.cpp new file mode 100644 index 0000000..9d387a8 --- /dev/null +++ b/libs/installer_omod/src/oldstuff/DialogSelect.cpp @@ -0,0 +1,301 @@ +#include "DialogSelect.h" + +#include <functional> + +#include <QCheckBox> +#include <QDialog> +#include <QDialogButtonBox> +#include <QDebug> +#include <QImageReader> +#include <QPlainTextEdit> +#include <QRadioButton> +#include <QScrollArea> +#include <QSplitter> +#include <QStackedWidget> +#include <QVBoxLayout> + +#include "MIT-licencedCodeToDoStuff/checkboxwordwrap.h" + +// there is no hover signal, the only way to know is to override enterEvent() +// +template <class T, typename... Args> +class HoverableWidget : public T +{ +public: + std::function<void ()> onHover; + + HoverableWidget(Args... args, std::function<void ()> h) + : T(args...), onHover(std::move(h)) + { + } + +protected: void enterEvent(QEnterEvent*) override + { + onHover(); + } +}; + + +FixedAspectRatioImageLabel::FixedAspectRatioImageLabel(QWidget* parent) : QLabel(parent) +{ +} + +void FixedAspectRatioImageLabel::setUnscaledPixmap(const QPixmap& pixmap) +{ + mUnscaledPixmap = pixmap; + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + rescalePixmap(size()); + + copySizeLabel.setPixmap(pixmap); +} + +const QPixmap& FixedAspectRatioImageLabel::unscaledPixmap() const +{ + return mUnscaledPixmap; +} + +QSize FixedAspectRatioImageLabel::sizeHint() const +{ + if (mUnscaledPixmap.isNull()) + { + return QLabel::sizeHint(); + } + else + { + // maybe should add frame border + return mUnscaledPixmap.size(); + //return copySizeLabel.sizeHint(); + } +} + +bool FixedAspectRatioImageLabel::hasHeightForWidth() const +{ + return true; +} + +int FixedAspectRatioImageLabel::heightForWidth(int width) const +{ + // this ignores the difference between size and contentsRect size + return mUnscaledPixmap.height() * width / (double)mUnscaledPixmap.width(); +} + +int FixedAspectRatioImageLabel::widthForHeight(int height) const +{ + return mUnscaledPixmap.width() * height / (double)mUnscaledPixmap.height(); +} + +void FixedAspectRatioImageLabel::resizeEvent(QResizeEvent* resizeEvent) +{ + QLabel::resizeEvent(resizeEvent); + rescalePixmap(contentsRect().size()); +} + +void FixedAspectRatioImageLabel::rescalePixmap(const QSize& size) +{ + setPixmap(mUnscaledPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} + + +std::optional<QVector<int>> DialogSelect( + QWidget* parent, const QString& title, const QVector<QString>& items, + const QVector<QString>& descriptions, const QVector<QString>& pixmaps, + bool multiSelect) +{ + QDialog d(parent); + d.setWindowTitle(title); + + auto* mainLayout = new QVBoxLayout(&d); + d.setLayout(mainLayout); + + auto* splitter = new QSplitter(&d); + mainLayout->addWidget(splitter); + + // button box + auto* buttons = new QDialogButtonBox( + QDialogButtonBox::Ok|QDialogButtonBox::Cancel, &d); + + /* QObject::connect: signal not found in QDialogButtonBox + even though this worked with my initial attempt. + Holt had the same issue: https://stackoverflow.com/questions/61879664/qobjectconnect-not-working-signal-not-found-with-function-syntax + The new syntax is much better, but as this code wants replacing, that's not a hill I'm going to die on. + QObject::connect(buttons, &QDialogButtonBox::accepted, [&] { + d.accept(); + }); + + QObject::connect(buttons, &QDialogButtonBox::rejected, [&] { + d.reject(); + }); + */ + QObject::connect(buttons, SIGNAL(accepted()), &d, SLOT(accept())); + QObject::connect(buttons, SIGNAL(rejected()), &d, SLOT(reject())); + + mainLayout->addWidget(buttons); + + + // left panel + auto* left = new QWidget(splitter); + auto* leftLayout = new QVBoxLayout(left); + leftLayout->setContentsMargins(0, 0, 0, 0); + + auto* stack = new QStackedWidget(left); + leftLayout->addWidget(stack); + + // don't put descriptions of pixmaps at all if there aren't any + const bool hasDescriptions = !descriptions.empty(); + const bool hasPixmaps = !pixmaps.empty(); + + // for each description/item + for (int i=0; i < std::max(descriptions.size(), pixmaps.size()); ++i) { + auto* panel = new QWidget(left); + auto* panelLayout = new QVBoxLayout(panel); + //panelLayout->setContentsMargins(0, 0, 0, 0); + + if (hasPixmaps) { + auto* pixmapLabel = new FixedAspectRatioImageLabel(panel); + + pixmapLabel->setFrameStyle(QFrame::StyledPanel); + pixmapLabel->setLineWidth(1); + + // make it resizable + //pixmapLabel->setMinimumSize(1, 150); + //pixmapLabel->setScaledContents(true); + //pixmapLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + if (i < pixmaps.size()) { + // this item has a pixmap + // Use QImageReader as (despite what the documentation says) QPixmap uses the extension, rather than the file header, to determine the format. + QImageReader reader(pixmaps[i]); + reader.setDecideFormatFromContent(true); + QImage image = reader.read(); + if (image.isNull()) { + pixmapLabel->setText(QString("failed to load '%1': %2 (code %3)").arg(pixmaps[i], reader.errorString(), QString::number(reader.error()))); + } else { + QPixmap pixmap = QPixmap::fromImage(image); + pixmapLabel->setUnscaledPixmap(std::move(pixmap)); + } + } + + panelLayout->addWidget(pixmapLabel); + } + + if (hasDescriptions) + { + // description under pixmap (if any) + auto* description = new QPlainTextEdit(descriptions[i], panel); + + // this puts the description on top if there's no pixmap + //description->setAlignment(Qt::AlignLeft|Qt::AlignTop); + + //description->setWordWrap(true); + + panelLayout->addWidget(description); + } + + // if the pixmap is first, it'll take as much space as it can; if the + // description is first, the AlignTop makes it work anyways + panelLayout->setStretch(0, 1); + + stack->addWidget(panel); + } + + + // right panel + auto* right = new QWidget(&d); + auto* rightOuterLayout = new QVBoxLayout(right); + rightOuterLayout->setContentsMargins(0, 0, 0, 0); + + // title + auto* titleLabel = new QLabel(title, right); + titleLabel->setWordWrap(true); + rightOuterLayout->addWidget(titleLabel); + + QWidget* rightInner; + QBoxLayout* rightInnerLayout; + + // Doing this unconditionally breaks HGEC lower body choices. + // Somehow the fixed aspect ratio image label forces the scroll area to be resized below its preferred size. + // Because the word wrapped radio buttons aren't good at telling Qt their size hint is only minimal for the current width, + // the layout ends up with their preferred width as its minimum and won't let itself be shrunk smaller than that even though you can't see the right hand side. + // This means that as they're in a widget with enough space, the text isn't wrapped. + // The exact same behaviour happens with non-wrappable radio buttons, but it's more expected. + // Making the fixed aspect ratio image label play nicely with being shrunk will probably fix this. + // It only seems to be MEAT that actually needs a scrollbar, though, and that looks fine, so fixing this mess is left as an exercise for the reader. + if (items.size() >= 10) + { + QScrollArea* rightScrollArea = new QScrollArea(right); + rightOuterLayout->addWidget(rightScrollArea); + rightScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + rightScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + + rightInner = new QWidget(right); + rightScrollArea->setWidget(rightInner); + rightScrollArea->setWidgetResizable(true); + + rightInnerLayout = new QVBoxLayout(rightInner); + } + else + { + rightInner = right; + rightInnerLayout = rightOuterLayout; + } + + // callback when hovering + auto onHover = [&](int i) { + stack->setCurrentIndex(i); + }; + + + // remember buttons to see which were checked + QVector<QAbstractButton*> itemButtons; + + // for each item + for (int i=0; i<items.size(); ++i) { + QAbstractButton* w = nullptr; + + QString labelText = items[i]; + bool checked = false; + + // THIS IS IMPORTANT WHEN REPLACING THIS WITH BETTER CODE. OMODS HAVE A PIPE AT THE START OF ANY OPTIONS SELECTED BY DEFAULT. + if (labelText.startsWith('|')) + { + labelText = labelText.remove(0, 1); + checked = true; + } + + if (multiSelect) { + w = new HoverableWidget<CheckBoxWordWrap, const QString&, QWidget*>(labelText, rightInner, [=]{ onHover(i); }); + } else { + w = new HoverableWidget<RadioButtonWordWrap, const QString&, QWidget*>(labelText, rightInner, [=]{ onHover(i); }); + checked |= i == 0; + } + + rightInnerLayout->addWidget(w); + itemButtons.push_back(w); + w->setChecked(checked); + } + + // push all the items to the top + rightInnerLayout->addStretch(1); + + splitter->addWidget(left); + splitter->addWidget(right); + + // decent initial size + d.resize(800, 500); + + if (d.exec() != QDialog::Accepted) { + return std::nullopt; + } + + + // go through every button, it doesn't matter whether they're checkboxes or + // radio buttons + QVector<int> selection; + for (int i=0; i<itemButtons.size(); ++i) { + if (itemButtons[i]->isChecked()) { + selection.push_back(i); + } + } + + return selection; +} diff --git a/libs/installer_omod/src/oldstuff/DialogSelect.h b/libs/installer_omod/src/oldstuff/DialogSelect.h new file mode 100644 index 0000000..0e0f1bb --- /dev/null +++ b/libs/installer_omod/src/oldstuff/DialogSelect.h @@ -0,0 +1,46 @@ +#pragma once + +#include <optional> + +#include <QLabel> +#include <QVector> +#include <QWidget> + +std::optional<QVector<int>> DialogSelect(QWidget* parent, const QString& title, const QVector<QString>& items, + const QVector<QString>& descriptions, const QVector<QString>& pixmaps, + bool multiSelect); + + +// For some reason, this can be resized bigger but not smaller. +class FixedAspectRatioImageLabel : public QLabel +{ + Q_OBJECT + + Q_PROPERTY(QPixmap unscaledPixmap READ unscaledPixmap WRITE setUnscaledPixmap) + +public: + FixedAspectRatioImageLabel() = default; + + FixedAspectRatioImageLabel(QWidget* parent); + + void setUnscaledPixmap(const QPixmap& pixmap); + + const QPixmap& unscaledPixmap() const; + + QSize sizeHint() const override; + + bool hasHeightForWidth() const override; + + int heightForWidth(int width) const override; + + int widthForHeight(int height) const; + +protected: + void resizeEvent(QResizeEvent* resizeEvent) override; + +private: + void rescalePixmap(const QSize& size); + + QPixmap mUnscaledPixmap; + QLabel copySizeLabel; +}; diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE new file mode 100644 index 0000000..e2874c2 --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) unknown thibdev + +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. diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp new file mode 100644 index 0000000..d686c58 --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp @@ -0,0 +1,165 @@ +#include "checkboxwordwrap.h" + +#include <QStyle> +#include <QStyleOptionButton> + +CheckBoxWordWrap::CheckBoxWordWrap(QWidget *parent) + : QCheckBox (parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(this)) +{ + init(); +} + +CheckBoxWordWrap::CheckBoxWordWrap(const QString &text, QWidget *parent) + : QCheckBox (parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(text, this)) +{ + init(); +} + +CheckBoxWordWrap::~CheckBoxWordWrap() +{ + delete m_label; + delete m_hMainLayout; +} + +bool CheckBoxWordWrap::isWordWrap() const +{ + return m_label->wordWrap(); +} + +void CheckBoxWordWrap::setWordWrap(bool wordwrap) +{ + m_label->setWordWrap(wordwrap); +} + +QString CheckBoxWordWrap::text() const +{ + return m_label->text(); +} + +void CheckBoxWordWrap::setText(const QString &text) +{ + m_label->setText(text); +} + +QSize CheckBoxWordWrap::sizeHint() const +{ + QFontMetrics fm(m_label->font()); + QRect r = m_label->rect(); + r.setLeft(r.left()+m_label->indent()+separation); + QRect bRect = fm.boundingRect(r, int(Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap), m_label->text()); + QSize ret = QSize(QWidget::sizeHint().width(), bRect.height()); + return ret; +} + +void CheckBoxWordWrap::labelIsClicked() +{ + setChecked(!isChecked()); +} + +void CheckBoxWordWrap::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + updateGeometry(); +} + +void CheckBoxWordWrap::init() +{ + setLayout(m_hMainLayout); + QStyleOptionButton opt; + initStyleOption(&opt); + int indicatorW = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorWidth, &opt, this); + // Useless in our case, we only need the indicator width + //int indicatorH = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorHeight, &opt, this); + m_hMainLayout->setContentsMargins(0, 0, 0, 0); + m_hMainLayout->addWidget(m_label); + m_label->setIndent(indicatorW+separation); + m_label->setWordWrap(true); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + connect(m_label, SIGNAL(clicked()), this, SLOT(labelIsClicked())); +} + + +RadioButtonWordWrap::RadioButtonWordWrap(QWidget* parent) + : QRadioButton(parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(this)) +{ + init(); +} + +RadioButtonWordWrap::RadioButtonWordWrap(const QString& text, QWidget* parent) + : QRadioButton(parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(text, this)) +{ + init(); +} + +RadioButtonWordWrap::~RadioButtonWordWrap() +{ + delete m_label; + delete m_hMainLayout; +} + +bool RadioButtonWordWrap::isWordWrap() const +{ + return m_label->wordWrap(); +} + +void RadioButtonWordWrap::setWordWrap(bool wordwrap) +{ + m_label->setWordWrap(wordwrap); +} + +QString RadioButtonWordWrap::text() const +{ + return m_label->text(); +} + +void RadioButtonWordWrap::setText(const QString& text) +{ + m_label->setText(text); +} + +QSize RadioButtonWordWrap::sizeHint() const +{ + QFontMetrics fm(m_label->font()); + QRect r = m_label->rect(); + r.setLeft(r.left() + m_label->indent() + separation); + QRect bRect = fm.boundingRect(r, int(Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap), m_label->text()); + QSize ret = QSize(QWidget::sizeHint().width(), bRect.height()); + return ret; +} + +void RadioButtonWordWrap::labelIsClicked() +{ + setChecked(!isChecked()); +} + +void RadioButtonWordWrap::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + updateGeometry(); +} + +void RadioButtonWordWrap::init() +{ + setLayout(m_hMainLayout); + QStyleOptionButton opt; + initStyleOption(&opt); + int indicatorW = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorWidth, &opt, this); + // Useless in our case, we only need the indicator width + //int indicatorH = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorHeight, &opt, this); + m_hMainLayout->setContentsMargins(0, 0, 0, 0); + m_hMainLayout->addWidget(m_label); + m_label->setIndent(indicatorW + separation); + m_label->setWordWrap(true); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + connect(m_label, SIGNAL(clicked()), this, SLOT(labelIsClicked())); +} diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h new file mode 100644 index 0000000..dc4d30c --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h @@ -0,0 +1,75 @@ +#ifndef CHECKBOXWORDWRAP_H +#define CHECKBOXWORDWRAP_H + +#include <QCheckBox> +#include <QHBoxLayout> +#include <QRadioButton> + +#include "clickablelabel.h" + +/** + * @author thibdev + */ +class CheckBoxWordWrap : public QCheckBox +{ + Q_OBJECT + + Q_PROPERTY(bool wordwrap READ isWordWrap WRITE setWordWrap) + Q_PROPERTY(QString text READ text WRITE setText) + +public: + CheckBoxWordWrap(QWidget *parent = Q_NULLPTR); + CheckBoxWordWrap(const QString &text, QWidget *parent = Q_NULLPTR); + ~CheckBoxWordWrap(); + bool isWordWrap() const; + void setWordWrap(bool wordwrap); + QString text() const; + void setText(const QString &text); + QSize sizeHint() const override; + +private slots: + void labelIsClicked(); + +protected: + void resizeEvent(QResizeEvent *event) override; + +private: + void init(); + const int separation = 5; + QHBoxLayout *m_hMainLayout; + ClickableLabel *m_label; + +}; + +class RadioButtonWordWrap : public QRadioButton +{ + Q_OBJECT + + Q_PROPERTY(bool wordwrap READ isWordWrap WRITE setWordWrap) + Q_PROPERTY(QString text READ text WRITE setText) + +public: + RadioButtonWordWrap(QWidget* parent = Q_NULLPTR); + RadioButtonWordWrap(const QString& text, QWidget* parent = Q_NULLPTR); + ~RadioButtonWordWrap(); + bool isWordWrap() const; + void setWordWrap(bool wordwrap); + QString text() const; + void setText(const QString& text); + QSize sizeHint() const override; + +private slots: + void labelIsClicked(); + +protected: + void resizeEvent(QResizeEvent* event) override; + +private: + void init(); + const int separation = 5; + QHBoxLayout* m_hMainLayout; + ClickableLabel* m_label; + +}; + +#endif diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp new file mode 100644 index 0000000..6f8d47f --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp @@ -0,0 +1,24 @@ +#include "clickablelabel.h" + +ClickableLabel::ClickableLabel(QWidget *parent, Qt::WindowFlags f) + : QLabel(parent, f) +{ + +} + +ClickableLabel::ClickableLabel(const QString &text, QWidget *parent, Qt::WindowFlags f) + : QLabel(text, parent, f) +{ + +} + +ClickableLabel::~ClickableLabel() +{ + +} + +void ClickableLabel::mouseReleaseEvent(QMouseEvent *event) +{ + Q_UNUSED(event); + emit clicked(); +} diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h new file mode 100644 index 0000000..c3ee22b --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h @@ -0,0 +1,27 @@ +#ifndef CLICKABLELABEL_H +#define CLICKABLELABEL_H + +#include <QLabel> +#include <QMouseEvent> + +/** + * @brief The ClickableLabel class + * https://wiki.qt.io/Clickable_QLabel + */ +class ClickableLabel : public QLabel +{ + Q_OBJECT +public: + explicit ClickableLabel(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); + explicit ClickableLabel(const QString &text, QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); + ~ClickableLabel() override; + +signals: + void clicked(); + +protected: + void mouseReleaseEvent(QMouseEvent *event) override; + +}; + +#endif // CLICKABLELABEL_H diff --git a/libs/installer_omod/src/oldstuff/folder.md b/libs/installer_omod/src/oldstuff/folder.md new file mode 100644 index 0000000..3b5009a --- /dev/null +++ b/libs/installer_omod/src/oldstuff/folder.md @@ -0,0 +1 @@ +This is stuff I'm just lifting from my original attempt at an OMOD installer. It's probably terrible and I'm barely going to fix it. I expect and want other MO2 devs to rework/replace it with something better. diff --git a/libs/installer_omod/vcpkg.json b/libs/installer_omod/vcpkg.json new file mode 100644 index 0000000..28051ad --- /dev/null +++ b/libs/installer_omod/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673" + } + } +} |
