Compare commits

..

96 Commits

Author SHA1 Message Date
Petr Mrázek
3ad9ea507e NOISSUE update version number, changelog and credits in about dialog 2020-03-29 03:12:57 +02:00
Petr Mrázek
c9e851f12f GH-2544 enable Forge install button for >= 1.13 2020-03-28 15:55:12 +01:00
Petr Mrázek
0281845fc8 GH-2544 allow adding files to libraries without affecting classpath
This is done by adding library-like objects into the `mavenFiles`
list in version JSONs.
2020-03-27 02:23:15 +01:00
Petr Mrázek
e6cc65cf69 NOISSUE no means no, #2 2020-03-26 10:38:13 +01:00
Petr Mrázek
ebb17cb5f8 NOISSUE no means no. 2020-03-26 03:51:14 +01:00
Petr Mrázek
69490535d0 Merge pull request #2979 from Brottweiler/desktop-entry-tweak
Tweak GenericName for Linux desktop entry
2020-02-25 18:02:09 +01:00
Brottweiler
95a09ba099 GH-2979 Tweak GenericName 2020-02-24 19:39:22 +01:00
Petr Mrázek
6cb956b45b NOISSUE Nice. 2020-02-24 18:59:36 +01:00
OverMighty
47fa7b3f8c GH-2988 add --import command-line option
When specified, opens the "Import from zip" dialog as soon as the main
window is shown, with the URL field prefilled with the argument given to
the option.

Closes #2998
2020-02-24 18:52:26 +01:00
Petr Mrázek
d58481e0de NOISSUE fix some changelog wording 2020-02-09 00:03:20 +01:00
Petr Mrázek
08f85f1a93 Update changelog and set version to 0.6.8 2020-02-08 15:00:20 +01:00
Petr Mrázek
bc98181ec2 GH-2769 add an option to not copy play time when copying instances 2020-01-09 15:31:32 +01:00
Petr Mrázek
6a095deea6 GH-2832 add .minecraft and libraries buttons to version page 2020-01-09 13:45:46 +01:00
Petr Mrázek
355e5e24da GH-2819 mod list filter now also looks at descriptions and authors 2020-01-08 21:12:45 +01:00
Petr Mrázek
8bdff97ac0 GH-2839 remove username wording from login and account dialogs 2020-01-08 13:37:05 +01:00
Petr Mrázek
6288805f37 GH-2853 fix collapsing state being sticky in group view
Now it resets properly and collapsing a group doesn't make
all subsequent clicks collapse groups.
2020-01-08 09:00:54 +01:00
Petr Mrázek
0d157d86b5 NOISSUE add wget as a dependency for deb package 2020-01-08 06:28:42 +01:00
Petr Mrázek
f413e61cd8 NOISSUE Do not crash when dependencies are customized and conflict 2020-01-08 04:41:47 +01:00
Petr Mrázek
3581f5384f GH-2880 make services status widget open a valid page
https://help.mojang.com/ no longer has API status
2020-01-07 08:56:00 +01:00
Petr Mrázek
480b298635 NOISSUE Attempt to add build status and test teamcity build reporting 2019-11-27 00:35:42 +01:00
Petr Mrázek
b54b25231e Remove travis-ci and github actions leftovers 2019-11-26 21:09:37 +01:00
Petr Mrázek
43628556ed NOISSUE fix comment in Commandline.h 2019-11-26 19:06:45 +01:00
Petr Mrázek
b5adff14ab NOISSUE remove dead reference to TwitchPage.ui 2019-11-18 02:38:08 +01:00
Petr Mrázek
af5120c828 GH-2859 remove twitch page and modpack import from URL
The functionality was broken, beyond repair and an ongoing maintenance
nightmare.
2019-11-18 00:38:36 +01:00
Petr Mrázek
47ed2f48d4 NOISSUE put legacy FTB support in a namespace, fix its base URL 2019-11-03 23:48:12 +01:00
Petr Mrázek
0c9340a3d2 NOISSUE fix translation string for Twitch drop area 2019-10-14 23:51:36 +02:00
Petr Mrázek
9cc5ebcdd1 GH-2859 improve UI for twitch pack import with drag&drop 2019-10-14 02:31:53 +02:00
Petr Mrázek
c60647523e NOISSUE remove remains of what could have been technic integration 2019-10-14 01:05:38 +02:00
Petr Mrázek
9165232ba4 NOISSUE remove unused 'PackagesPage' 2019-10-13 22:31:14 +02:00
Petr Mrázek
31d0507e07 NOISSUE make the main window title only 'MultiMC' for screen readers 2019-10-05 22:51:55 +02:00
Petr Mrázek
cca766cfd9 Merge pull request #2871 from kb-1000/fix-accessibility-fix
Remove include of qtguiglobal.h
2019-10-01 16:15:45 +02:00
kb1000
0d41bc5e13 Remove include of qtguiglobal.h 2019-10-01 15:55:40 +02:00
Petr Mrázek
e27309d08a Merge pull request #2870 from kb-1000/fix-accessibility
Add checks for QT_NO_ACCESSIBILITY to prevent build issues with Qt without accessibility
2019-10-01 15:40:39 +02:00
kb1000
dec6759e61 Add checks for QT_NO_ACCESSIBILITY to prevent build issues with Qt without accessibility 2019-10-01 14:28:06 +02:00
Petr Mrázek
ce7917048a NOISSUE remove more html rom About dialog 2019-09-30 23:50:32 +02:00
Petr Mrázek
e6936212d6 NOISSUE remove some ugly html strings to make translating slightly nicer 2019-09-27 00:23:30 +02:00
Petr Mrázek
1210d3abf1 NOISSUE fix display of european portuguese in language lists 2019-09-27 00:23:03 +02:00
Petr Mrázek
ffe84d6ec7 NOISSUE remove some dead things 2019-09-26 20:08:47 +02:00
Petr Mrázek
19015de258 NOISSUE correct some parts of the README, one more translation string change 2019-09-26 09:03:25 +02:00
Petr Mrázek
5c0c26cd25 TRANSLATIONS-82 fix typos: resove -> resolve 2019-09-25 22:31:09 +02:00
Petr Mrázek
4cc7427eb4 TRANSLATIONS-73 remove some strings that don't show up in the UI 2019-09-25 22:29:50 +02:00
Petr Mrázek
6531aaa7bc Try using github actions
Let's see how badly this goes
2019-09-19 10:56:11 +02:00
Petr Mrázek
a35a2e877e NOISSUE remove nonsensical logic related to 'MultiMC.app/' prefixes in update manifests 2019-09-19 01:13:02 +02:00
Petr Mrázek
4e93c4d012 NOISSUE escape tcversion more 2019-09-17 01:26:37 +02:00
Petr Mrázek
7bb23b4142 NOISSUE add some escaping to make the tc version print actually do something 2019-09-17 01:22:00 +02:00
Petr Mrázek
b420f4bafb Merge pull request #2849 from AshleighTheCutie/develop
Message cleanup
2019-09-17 01:12:45 +02:00
Petr Mrázek
0e0a017175 NOISSUE add a way to extract the version into a TC variable 2019-09-17 00:53:30 +02:00
Ashleigh
137fe7e3c0 Cleaned up messages, made some more descriptive, made some nicer. 2019-09-14 22:31:13 -05:00
Ashleigh
9689e5cea7 Added text notifying that password change may cause logout 2019-09-14 22:12:23 -05:00
Petr Mrázek
1ede75aa79 Merge pull request #2848 from AshleighTheCutie/develop
Window Size to 800x600
2019-09-15 04:34:03 +02:00
Ashleigh
9ee3a84817 Window Size to 800x600 2019-09-14 21:15:51 -05:00
Petr Mrázek
8750ca8b36 NOISSUE add bee icon 2019-08-26 02:25:42 +02:00
Petr Mrázek
1747f413b9 GH-851 save, load and use group expansion status 2019-08-20 02:58:27 +02:00
Petr Mrázek
6d975748c0 Merge pull request #2796 from QuImUfu/bug_fix
GH-2787 fix "Download All" button
2019-08-14 22:12:58 +02:00
Petr Mrázek
b050c7c075 Merge pull request #2803 from kb-1000/escape-semicolon
Escape ; too in instance folder names
2019-08-14 22:12:39 +02:00
Kaeptm Blaubaer
84e0cb1daa Escape ; too in instance folder names 2019-08-13 07:39:00 +02:00
Petr Mrázek
5074a97cb3 NOISSUE tweak toolbar and custom widget titles for better translations 2019-08-10 20:12:42 +02:00
Petr Mrázek
441e8980b8 NOISSUE fix small memory leaks 2019-08-10 19:58:58 +02:00
Johannes Michael Andreas Merl
84c53273ce GH-2787 fix "Download All" button 2019-08-09 00:11:42 +02:00
Petr Mrázek
c291946d2a NOISSUE do not lose selection on mod enable/disable toggle 2019-08-05 00:46:59 +02:00
Petr Mrázek
dfb30d9139 NOISSUE warn users about MS Edge being bad 2019-08-04 21:14:59 +02:00
Petr Mrázek
4ed67413ac GH-988 add ability to toggle mods with keyboard 2019-08-04 21:13:50 +02:00
Petr Mrázek
d31184f9a4 GH-2738 check for being in the temp folder better 2019-08-04 18:11:55 +02:00
Petr Mrázek
b75ba53d4b GH-2785 fix crash caused by starting multiple mod folder update tasks 2019-08-04 11:12:19 +02:00
Petr Mrázek
9037873928 NOISSUE update changelog 2019-08-04 06:26:09 +02:00
Petr Mrázek
ce4a55bc3b NOISSUE fix listing of mods in log, improve display with unicode 2019-08-04 05:08:40 +02:00
Petr Mrázek
6b82e942d0 NOISSUE fix build on linux 2019-08-04 03:39:25 +02:00
Petr Mrázek
a3ffa3d665 NOISSUE asynchronous, parallel mod folder listing and mod resolving 2019-08-04 03:27:53 +02:00
Petr Mrázek
7d13e31198 NOISSUE refactor Mod a bunch, get rid of dead code 2019-08-03 05:30:46 +02:00
Petr Mrázek
40c9af1a8b NOISSUE remove dependency of legacy mod list on the Mod class 2019-08-03 03:12:48 +02:00
Petr Mrázek
f5f3149dcf NOISSUE update changelog and version 2019-08-03 00:48:34 +02:00
Petr Mrázek
7b00d47fe0 NOISSUE tweak UI geometry and remove old language selection 2019-08-02 23:52:19 +02:00
Petr Mrázek
930d39b5f2 GH-2550 soring of mods by enabled status, cascade sorting to name and version 2019-07-31 01:28:55 +02:00
Petr Mrázek
bafcf93eb1 NOISSUE fix bug with drag & drop not working with empty mod list 2019-07-31 01:27:35 +02:00
Petr Mrázek
bd93c3b4e0 NOISSUE fix build 2019-07-30 01:25:37 +02:00
Petr Mrázek
09f7a426ab GH-2722 GH-2762 Improve mod list sorting
Sorting by version understands version numbers
Sorting by name removes 'The' prefixes before sorting
2019-07-30 01:16:56 +02:00
Petr Mrázek
e4fd50e210 NOISSUE make the language translation prompt translateable 2019-07-30 01:16:02 +02:00
Petr Mrázek
3ee5a63c5c NOISSUE make notes page focusable with tab key 2019-07-25 01:13:47 +02:00
Petr Mrázek
7dfe73df0c NOISSUE add context menus to pages with toolbars 2019-07-25 01:02:30 +02:00
Petr Mrázek
c3e61536a3 NOISSUE automatically open the log page when starting the instance 2019-07-24 00:24:02 +02:00
Petr Mrázek
a0e45c5d1d NOISSUE fix build 2019-07-23 01:05:23 +02:00
Petr Mrázek
bf38021937 NOISSUE improve toolbars 2019-07-23 00:48:14 +02:00
Petr Mrázek
1e5b595923 NOISSUE fix build failures 2019-07-22 01:44:19 +02:00
Petr Mrázek
d6c6653872 NOISSUE Add basic accessibility support to GroupView 2019-07-22 01:40:52 +02:00
Petr Mrázek
3b32730526 Merge pull request #2758 from telans/patch-1
Fix translation repository link
2019-07-21 01:33:31 +02:00
telans
1703dbeb57 Fix translation repository link 2019-07-21 10:50:30 +12:00
Petr Mrázek
81fdde6fdd NOISSUE convert accounts page to use a toolbar for the side menu 2019-07-19 08:29:31 +02:00
Petr Mrázek
3d5869e1cf NOISSUE fix overly large margins in the instance settings page 2019-07-17 02:17:09 +02:00
Petr Mrázek
edc5378333 NOISSUE add spacer to the screenshot page toolbar 2019-07-17 02:09:54 +02:00
Petr Mrázek
95febe5436 NOISSUE convert rest of the instance pages to use toolbars for side menus 2019-07-17 02:01:29 +02:00
Petr Mrázek
5b153a5165 GH-2748 disable the version page toolbar labels 2019-07-16 08:58:00 +02:00
Petr Mrázek
decd4ae7ab NOISSUE Make mod folder pages use toolbars instead of button layouts 2019-07-16 01:30:53 +02:00
Petr Mrázek
2eec1df1a0 NOISSUE hide main toolbar toggle action instead of working around it 2019-07-16 01:30:09 +02:00
Petr Mrázek
6fde775b90 NOISSUE Show Version page while the instancer is running.
All controls are disabled.
2019-07-15 23:16:34 +02:00
Petr Mrázek
80b3efff11 NOISSUE Do not hide mods list pages when the instance is running.
Instead, disable (most of) the controls.
2019-07-15 01:07:21 +02:00
Petr Mrázek
e4273d6a17 GH-358 Make version page use a toolbar for all the actions
This should make it possible to make it fit on small screens again.
2019-07-14 05:37:10 +02:00
156 changed files with 4584 additions and 4103 deletions

View File

@@ -1,5 +0,0 @@
{
"project_id": "MultiMC5",
"conduit_uri": "http://ph.multimc.org"
}

View File

@@ -1,25 +0,0 @@
UseTab: false
IndentWidth: 4
TabWidth: 4
ConstructorInitializerIndentWidth: 4
AccessModifierOffset: -4
IndentCaseLabels: false
IndentFunctionDeclarationAfterType: false
NamespaceIndentation: None
BreakBeforeBraces: Allman
AllowShortIfStatementsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
ColumnLimit: 160
MaxEmptyLinesToKeep: 1
Standard: Cpp11
Cpp11BracedListStyle: true
SpacesInParentheses: false
SpaceInEmptyParentheses: false
SpacesInCStyleCastParentheses: false
SpaceAfterControlStatementKeyword: true
AlignTrailingComments: true
SpacesBeforeTrailingComments: 1

5
.gitignore vendored
View File

@@ -1,5 +1,5 @@
Thumbs.db
.kdev4
*.kdev4
.user
.directory
resources/CMakeFiles
@@ -9,8 +9,7 @@ resources/MultiMCLauncher.jar
html/
# Project Files
MultiMC5.kdev4
MultiMC.pro.user
*.pro.user
CMakeLists.txt.user
CMakeLists.txt.user.*
/.project

View File

@@ -1,38 +0,0 @@
# General set up
language: cpp
cache: apt
matrix:
include:
- os: linux
dist: precise
sudo: required
compiler: gcc
env: TRAVIS_DIST=precise QT_VERSION=5.4.2
- os: linux
dist: precise
sudo: required
compiler: gcc
env: TRAVIS_DIST=precise QT_VERSION=5.6.2
- os: linux
dist: trusty
sudo: required
compiler: gcc
env: TRAVIS_DIST=trusty QT_VERSION=5.4.2
- os: linux
dist: trusty
sudo: required
compiler: gcc
env: TRAVIS_DIST=trusty QT_VERSION=5.6.2
# Install dependencies
install:
- source travis/prepare.sh # installs qt and cmake. need to source because some env vars are set from there
# Actual work
before_script:
- mkdir build
- cd build
- cmake -DCMAKE_PREFIX_PATH=$CMAKE_PREFIX_PATH ..
script:
- make -j4 && make test ARGS="-V"

View File

@@ -46,7 +46,7 @@ set(MultiMC_NEWS_RSS_URL "https://multimc.org/rss.xml" CACHE STRING "URL to fetc
######## Set version numbers ########
set(MultiMC_VERSION_MAJOR 0)
set(MultiMC_VERSION_MINOR 6)
set(MultiMC_VERSION_HOTFIX 6)
set(MultiMC_VERSION_HOTFIX 11)
# Build number
set(MultiMC_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")
@@ -77,6 +77,7 @@ set(MultiMC_RELEASE_VERSION_NAME "${MultiMC_VERSION_MAJOR}.${MultiMC_VERSION_MIN
#### Custom target to just print the version.
add_custom_target(version echo "Version: ${MultiMC_RELEASE_VERSION_NAME}")
add_custom_target(tcversion echo "\\#\\#teamcity[setParameter name=\\'env.MULTIMC_VERSION\\' value=\\'${MultiMC_RELEASE_VERSION_NAME}\\']")
################################ 3rd Party Libs ################################

View File

@@ -13,21 +13,22 @@ The project uses C++ and Qt5 as the language and base framework. This might seem
We can do more, with less, on worse hardware and leave more resources for the game while keeping the launcher running and providing extra features.
If you want to contribute, either talk to us on [Discord](https://discord.gg/0k2zsXGNHs0fE4Wm), [IRC](http://webchat.esper.net/?nick=&channels=MultiMC)(esper.net/#MultiMC) or pick up some item from [workflowy](https://workflowy.com/s/2EyDMcp7CU) - there are many.
If you want to contribute, either talk to us on [Discord](https://discord.gg/0k2zsXGNHs0fE4Wm), [IRC](http://webchat.esper.net/?nick=&channels=MultiMC)(esper.net/#MultiMC) or pick up some item from the github issues [workflowy](https://github.com/MultiMC/MultiMC5/issues) - there is always plenty of ideas around.
### Building
If you want to build MultiMC yourself, check [BUILD.md](BUILD.md) for build instructions.
The ci server is running at [ci.multimc.org](http://ci.multimc.org), where you can watch the builds happen in (or very close to) real time. It can also serve as a nice reference on how to set up the build environment on your end.
According to travis.ci, the builds are currently [![Build Status](https://travis-ci.org/MultiMC/MultiMC5.svg?branch=develop)](https://travis-ci.org/MultiMC/MultiMC5)
### Code formatting
We use [Clang Format](http://clang.llvm.org/docs/ClangFormat.html) to format the project. We highly recommend setting it up so the project stays well formatted.
Just follow the existing formatting.
In general:
* Indent with 4 space unless it's in a submodule
* Keep lists (of arguments, parameters, initializators...) as lists, not paragraphs.
* Prefer readability over dogma.
## Translations
Translations can be done either directly in the [translations repository](https://github.com/MultiMC/MultiMC5). For more details, see: [Translating-MultiMC](https://github.com/MultiMC/MultiMC5/wiki/Translating-MultiMC).
Translations can be done [on crowdin](https://translate.multimc.org).
## Forking/Redistributing
We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license.
@@ -43,3 +44,36 @@ Copyright © 2013-2019 MultiMC Contributors
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this program except in compliance with the License. You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
## Build status
### Linux (Intel32)
<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=MultiMC_Launcher_Linux32_Build&guest=1">
Build: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:MultiMC_Launcher_Linux32_Build)/statusIcon"/>
</a>
<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=MultiMC_Launcher_Linux32_Deploy&guest=1">
Deploy: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:MultiMC_Launcher_Linux32_Deploy)/statusIcon"/>
</a>
### Linux (AMD64)
<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=MultiMC_Launcher_Linux64_Build&guest=1">
Build: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:MultiMC_Launcher_Linux64_Build)/statusIcon"/>
</a>
<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=MultiMC_Launcher_Linux64_Deploy&guest=1">
Deploy: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:MultiMC_Launcher_Linux64_Deploy)/statusIcon"/>
</a>
### macOS (AMD64)
<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=MultiMC_Launcher_MacOS_Build&guest=1">
Build: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:MultiMC_Launcher_MacOS_Build)/statusIcon"/>
</a>
<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=MultiMC_Launcher_MacOS_Deploy&guest=1">
Deploy: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:MultiMC_Launcher_MacOS_Deploy)/statusIcon"/>
</a>
### Windows (Intel32)
<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=MultiMC_Launcher_Windows_Build&guest=1">
Build: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:MultiMC_Launcher_Windows_Build)/statusIcon"/>
</a>
<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=MultiMC_Launcher_Windows_Deploy&guest=1">
Deploy: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:MultiMC_Launcher_Windows_Deploy)/statusIcon"/>
</a>

View File

@@ -213,8 +213,10 @@ set(MINECRAFT_SOURCES
minecraft/auth/flows/RefreshTask.cpp
minecraft/auth/flows/ValidateTask.h
minecraft/auth/flows/ValidateTask.cpp
minecraft/gameoptions/GameOptions.h
minecraft/gameoptions/GameOptions.cpp
minecraft/update/AssetUpdateTask.h
minecraft/update/AssetUpdateTask.cpp
minecraft/update/FMLLibrariesTask.cpp
@@ -223,6 +225,7 @@ set(MINECRAFT_SOURCES
minecraft/update/FoldersTask.h
minecraft/update/LibrariesTask.cpp
minecraft/update/LibrariesTask.h
minecraft/launch/ClaimAccount.cpp
minecraft/launch/ClaimAccount.h
minecraft/launch/CreateServerResourcePacksFolder.cpp
@@ -239,12 +242,16 @@ set(MINECRAFT_SOURCES
minecraft/launch/PrintInstanceInfo.h
minecraft/launch/ReconstructAssets.cpp
minecraft/launch/ReconstructAssets.h
minecraft/launch/ScanModFolders.cpp
minecraft/launch/ScanModFolders.h
minecraft/legacy/LegacyModList.h
minecraft/legacy/LegacyModList.cpp
minecraft/legacy/LegacyInstance.h
minecraft/legacy/LegacyInstance.cpp
minecraft/legacy/LegacyUpgradeTask.h
minecraft/legacy/LegacyUpgradeTask.cpp
minecraft/GradleSpecifier.h
minecraft/MinecraftInstance.cpp
minecraft/MinecraftInstance.h
@@ -279,15 +286,21 @@ set(MINECRAFT_SOURCES
minecraft/VersionFile.h
minecraft/VersionFilterData.h
minecraft/VersionFilterData.cpp
minecraft/Mod.h
minecraft/Mod.cpp
minecraft/SimpleModList.h
minecraft/SimpleModList.cpp
minecraft/World.h
minecraft/World.cpp
minecraft/WorldList.h
minecraft/WorldList.cpp
minecraft/mod/Mod.h
minecraft/mod/Mod.cpp
minecraft/mod/ModDetails.h
minecraft/mod/ModFolderModel.h
minecraft/mod/ModFolderModel.cpp
minecraft/mod/ModFolderLoadTask.h
minecraft/mod/ModFolderLoadTask.cpp
minecraft/mod/LocalModParseTask.h
minecraft/mod/LocalModParseTask.cpp
# Assets
minecraft/AssetsUtils.h
minecraft/AssetsUtils.cpp
@@ -318,8 +331,8 @@ add_unit_test(Library
)
# FIXME: shares data with FileSystem test
add_unit_test(SimpleModList
SOURCES minecraft/SimpleModList_test.cpp
add_unit_test(ModFolderModel
SOURCES minecraft/mod/ModFolderModel_test.cpp
DATA testdata
LIBS MultiMC_logic
)
@@ -426,15 +439,14 @@ set(META_SOURCES
)
set(FTB_SOURCES
modplatform/ftb/FtbPackFetchTask.h
modplatform/ftb/FtbPackFetchTask.cpp
modplatform/ftb/FtbPackInstallTask.h
modplatform/ftb/FtbPackInstallTask.cpp
modplatform/legacy_ftb/PackFetchTask.h
modplatform/legacy_ftb/PackFetchTask.cpp
modplatform/legacy_ftb/PackInstallTask.h
modplatform/legacy_ftb/PackInstallTask.cpp
modplatform/legacy_ftb/PrivatePackManager.h
modplatform/legacy_ftb/PrivatePackManager.cpp
modplatform/ftb/FtbPrivatePackManager.h
modplatform/ftb/FtbPrivatePackManager.cpp
modplatform/ftb/PackHelpers.h
modplatform/legacy_ftb/PackHelpers.h
)
set(FLAME_SOURCES
@@ -443,8 +455,6 @@ set(FLAME_SOURCES
modplatform/flame/PackManifest.cpp
modplatform/flame/FileResolvingTask.h
modplatform/flame/FileResolvingTask.cpp
modplatform/flame/UrlResolvingTask.h
modplatform/flame/UrlResolvingTask.cpp
)
add_unit_test(Index
@@ -479,8 +489,6 @@ set(LOGIC_SOURCES
${FLAME_SOURCES}
)
message(STATUS "FOO! ${LOGIC_SOURCES}")
add_library(MultiMC_logic SHARED ${LOGIC_SOURCES})
set_target_properties(MultiMC_logic PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1)

View File

@@ -69,8 +69,8 @@ namespace ArgumentStyle
{
enum Enum
{
Space, /**< --option=value */
Equals, /**< --option value */
Space, /**< --option value */
Equals, /**< --option=value */
SpaceAndEquals, /**< --option[= ]value */
#ifdef Q_OS_WIN32
Default = Equals

View File

@@ -174,6 +174,11 @@ bool copy::operator()(const QString &offset)
bool deletePath(QString path)
{
bool OK = true;
QFileInfo finfo(path);
if(finfo.isFile()) {
return QFile::remove(path);
}
QDir dir(path);
if (!dir.exists())
@@ -294,7 +299,7 @@ QString NormalizePath(QString path)
}
}
QString badFilenameChars = "\"\\/?<>:*|!+\r\n";
QString badFilenameChars = "\"\\/?<>:;*|!+\r\n";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{

View File

@@ -5,9 +5,10 @@
#include "pathmatcher/RegexpMatcher.h"
#include <QtConcurrentRun>
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, bool copySaves)
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime)
{
m_origInstance = origInstance;
m_keepPlaytime = keepPlaytime;
if(!copySaves)
{
@@ -46,6 +47,9 @@ void InstanceCopyTask::copyFinished()
InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath));
inst->setName(m_instName);
inst->setIconKey(m_instIcon);
if(!m_keepPlaytime) {
inst->resetTimePlayed();
}
emitSucceeded();
}

View File

@@ -15,7 +15,7 @@ class MULTIMC_LOGIC_EXPORT InstanceCopyTask : public InstanceTask
{
Q_OBJECT
public:
explicit InstanceCopyTask(InstancePtr origInstance, bool copySaves);
explicit InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime);
protected:
//! Entry point for tasks.
@@ -28,4 +28,5 @@ private: /* data */
QFuture<bool> m_copyFuture;
QFutureWatcher<bool> m_copyFutureWatcher;
std::unique_ptr<IPathMatcher> m_matcher;
bool m_keepPlaytime;
};

View File

@@ -100,6 +100,10 @@ QVariant InstanceList::data(const QModelIndex &index, int role) const
{
return pdata->name();
}
case Qt::AccessibleTextRole:
{
return tr("%1 Instance").arg(pdata->name());
}
case Qt::ToolTipRole:
{
return pdata->instanceRoot();
@@ -156,8 +160,8 @@ GroupId InstanceList::getInstanceGroup(const InstanceId& id) const
{
return GroupId();
}
auto iter = m_groupMap.find(inst->id());
if(iter != m_groupMap.end())
auto iter = m_instanceGroupIndex.find(inst->id());
if(iter != m_instanceGroupIndex.end())
{
return *iter;
}
@@ -174,8 +178,8 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name)
}
bool changed = false;
auto iter = m_groupMap.find(inst->id());
if(iter != m_groupMap.end())
auto iter = m_instanceGroupIndex.find(inst->id());
if(iter != m_instanceGroupIndex.end())
{
if(*iter != name)
{
@@ -186,12 +190,12 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name)
else
{
changed = true;
m_groupMap[id] = name;
m_instanceGroupIndex[id] = name;
}
if(changed)
{
m_groups.insert(name);
m_groupNameCache.insert(name);
auto idx = getInstIndex(inst.get());
emit dataChanged(index(idx), index(idx), {GroupRole});
saveGroupList();
@@ -200,7 +204,7 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name)
QStringList InstanceList::getGroups()
{
return m_groups.toList();
return m_groupNameCache.toList();
}
void InstanceList::deleteGroup(const QString& name)
@@ -213,7 +217,7 @@ void InstanceList::deleteGroup(const QString& name)
auto instGroupName = getInstanceGroup(instID);
if(instGroupName == name)
{
m_groupMap.remove(instID);
m_instanceGroupIndex.remove(instID);
qDebug() << "Remove" << instID << "from group" << name;
removed = true;
auto idx = getInstIndex(instance.get());
@@ -229,6 +233,11 @@ void InstanceList::deleteGroup(const QString& name)
}
}
bool InstanceList::isGroupCollapsed(const QString& group)
{
return m_collapsedGroups.contains(group);
}
void InstanceList::deleteInstance(const InstanceId& id)
{
auto inst = getInstanceById(id);
@@ -238,7 +247,7 @@ void InstanceList::deleteInstance(const InstanceId& id)
return;
}
if(m_groupMap.remove(id))
if(m_instanceGroupIndex.remove(id))
{
saveGroupList();
}
@@ -511,7 +520,7 @@ void InstanceList::saveGroupList()
WatchLock foo(m_watcher, m_instDir);
QString groupFileName = m_instDir + "/instgroups.json";
QMap<QString, QSet<QString>> reverseGroupMap;
for (auto iter = m_groupMap.begin(); iter != m_groupMap.end(); iter++)
for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++)
{
QString id = iter.key();
QString group = iter.value();
@@ -544,7 +553,7 @@ void InstanceList::saveGroupList()
auto name = iter.key();
QJsonObject groupObj;
QJsonArray instanceArr;
groupObj.insert("hidden", QJsonValue(QString("false")));
groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name)));
for (auto item : list)
{
instanceArr.append(QJsonValue(item));
@@ -568,7 +577,6 @@ void InstanceList::saveGroupList()
void InstanceList::loadGroupList()
{
qDebug() << "Will load group list now.";
QSet<QString> groupSet;
QString groupFileName = m_instDir + "/instgroups.json";
@@ -619,7 +627,8 @@ void InstanceList::loadGroupList()
return;
}
m_groupMap.clear();
QSet<QString> groupSet;
m_instanceGroupIndex.clear();
// Iterate through all the groups.
QJsonObject groupMapping = rootObj.value("groups").toObject();
@@ -630,37 +639,35 @@ void InstanceList::loadGroupList()
// If not an object, complain and skip to the next one.
if (!iter.value().isObject())
{
qWarning() << QString("Group '%1' in the group list should "
"be an object.")
.arg(groupName)
.toUtf8();
qWarning() << QString("Group '%1' in the group list should be an object.").arg(groupName).toUtf8();
continue;
}
QJsonObject groupObj = iter.value().toObject();
if (!groupObj.value("instances").isArray())
{
qWarning() << QString("Group '%1' in the group list is invalid. "
"It should contain an array "
"called 'instances'.")
.arg(groupName)
.toUtf8();
qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.").arg(groupName).toUtf8();
continue;
}
// keep a list/set of groups for choosing
groupSet.insert(groupName);
auto hidden = groupObj.value("hidden").toBool(false);
if(hidden) {
m_collapsedGroups.insert(groupName);
}
// Iterate through the list of instances in the group.
QJsonArray instancesArray = groupObj.value("instances").toArray();
for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++)
{
m_groupMap[(*iter2).toString()] = groupName;
m_instanceGroupIndex[(*iter2).toString()] = groupName;
}
}
m_groupsLoaded = true;
m_groups.unite(groupSet);
m_groupNameCache.unite(groupSet);
qDebug() << "Group list loaded.";
}
@@ -685,6 +692,17 @@ void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value)
}
}
void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed)
{
qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded");
if(collapsed) {
m_collapsedGroups.insert(group);
} else {
m_collapsedGroups.remove(group);
}
saveGroupList();
}
class InstanceStaging : public Task
{
Q_OBJECT
@@ -815,9 +833,9 @@ bool InstanceList::commitStagedInstance(const QString& path, const QString& inst
qWarning() << "Failed to move" << path << "to" << destination;
return false;
}
m_groupMap[instID] = groupName;
m_instanceGroupIndex[instID] = groupName;
instanceSet.insert(instID);
m_groups.insert(groupName);
m_groupNameCache.insert(groupName);
emit instancesChanged();
emit instanceSelectRequest(instID);
}

View File

@@ -99,6 +99,8 @@ public:
InstancePtr getInstanceById(QString id) const;
QModelIndex getInstanceIndexById(const QString &id) const;
QStringList getGroups();
bool isGroupCollapsed(const QString &groupName);
GroupId getInstanceGroup(const InstanceId & id) const;
void setInstanceGroup(const InstanceId & id, const GroupId& name);
@@ -134,6 +136,7 @@ signals:
public slots:
void on_InstFolderChanged(const Setting &setting, QVariant value);
void on_GroupStateChanged(const QString &group, bool collapsed);
private slots:
void propertiesChanged(BaseInstance *inst);
@@ -154,12 +157,14 @@ private:
int m_watchLevel = 0;
bool m_dirty = false;
QList<InstancePtr> m_instances;
QSet<QString> m_groups;
QSet<QString> m_groupNameCache;
SettingsObjectPtr m_globalSettings;
QString m_instDir;
QFileSystemWatcher * m_watcher;
QMap<InstanceId, GroupId> m_groupMap;
// FIXME: this is so inefficient that looking at it is almost painful.
QSet<QString> m_collapsedGroups;
QMap<InstanceId, GroupId> m_instanceGroupIndex;
QSet<InstanceId> instanceSet;
bool m_groupsLoaded = false;
bool m_instancesProbed = false;

View File

@@ -18,7 +18,7 @@
#include <QString>
#include <QFileInfo>
#include <QSet>
#include "minecraft/Mod.h"
#include "minecraft/mod/Mod.h"
#include <functional>
#include "multimc_logic_export.h"

View File

@@ -43,6 +43,8 @@ ComponentList::ComponentList(MinecraftInstance * instance)
d->m_instance = instance;
d->m_saveTimer.setSingleShot(true);
d->m_saveTimer.setInterval(5000);
d->interactionDisabled = instance->isRunning();
connect(d->m_instance, &BaseInstance::runningStatusChanged, this, &ComponentList::disableInteraction);
connect(&d->m_saveTimer, &QTimer::timeout, this, &ComponentList::save_internal);
}
@@ -765,8 +767,9 @@ QVariant ComponentList::data(const QModelIndex &index, int role) const
{
switch (column)
{
case NameColumn:
return d->components.at(row)->isEnabled() ? Qt::Checked : Qt::Unchecked;
case NameColumn: {
return patch->isEnabled() ? Qt::Checked : Qt::Unchecked;
}
default:
return QVariant();
}
@@ -776,7 +779,7 @@ QVariant ComponentList::data(const QModelIndex &index, int role) const
switch (column)
{
case NameColumn:
return d->components.at(row)->getName();
return patch->getName();
case VersionColumn:
{
if(patch->isCustom())
@@ -856,21 +859,25 @@ QVariant ComponentList::headerData(int section, Qt::Orientation orientation, int
}
return QVariant();
}
// FIXME: zero precision mess
Qt::ItemFlags ComponentList::flags(const QModelIndex &index) const
{
if (!index.isValid())
if (!index.isValid()) {
return Qt::NoItemFlags;
}
Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
int row = index.row();
if (row < 0 || row >= d->components.size())
if (row < 0 || row >= d->components.size()) {
return Qt::NoItemFlags;
}
auto patch = d->components.at(row);
// TODO: this will need fine-tuning later...
if(patch->canBeDisabled())
if(patch->canBeDisabled() && !d->interactionDisabled)
{
outFlags |= Qt::ItemIsUserCheckable;
}
@@ -1205,3 +1212,14 @@ QString ComponentList::getComponentVersion(const QString& uid) const
}
return QString();
}
void ComponentList::disableInteraction(bool disable)
{
if(d->interactionDisabled != disable) {
d->interactionDisabled = disable;
auto size = d->components.size();
if(size) {
emit dataChanged(index(0), index(size - 1));
}
}
}

View File

@@ -134,6 +134,7 @@ private slots:
void updateSucceeded();
void updateFailed(const QString & error);
void componentDataChanged();
void disableInteraction(bool disable);
private:
bool load();

View File

@@ -38,5 +38,6 @@ struct ComponentListData
QTimer m_saveTimer;
shared_qobject_ptr<Task> m_updateTask;
bool loaded = false;
bool interactionDisabled = true;
};

View File

@@ -451,13 +451,17 @@ static bool getTrivialComponentChanges(const ComponentIndex & index, const Requi
auto & comp = (*compIter);
if(comp->getVersion() != req.equalsVersion)
{
if(comp->m_dependencyOnly)
{
decision = Decision::VersionNotSame;
}
else
{
if(comp->isCustom()) {
decision = Decision::LockedVersionNotSame;
} else {
if(comp->m_dependencyOnly)
{
decision = Decision::VersionNotSame;
}
else
{
decision = Decision::LockedVersionNotSame;
}
}
break;
}

View File

@@ -11,6 +11,7 @@ void LaunchProfile::clear()
m_mainClass.clear();
m_appletClass.clear();
m_libraries.clear();
m_mavenFiles.clear();
m_traits.clear();
m_jarMods.clear();
m_mainJar.reset();
@@ -157,6 +158,22 @@ void LaunchProfile::applyLibrary(LibraryPtr library)
}
}
void LaunchProfile::applyMavenFile(LibraryPtr mavenFile)
{
if(!mavenFile->isActive())
{
return;
}
if(mavenFile->isNative())
{
return;
}
// unlike libraries, we do not keep only one version or try to dedupe them
m_mavenFiles.append(Library::limitedCopy(mavenFile));
}
const LibraryPtr LaunchProfile::getMainJar() const
{
return m_mainJar;
@@ -253,6 +270,11 @@ const QList<LibraryPtr> & LaunchProfile::getNativeLibraries() const
return m_nativeLibraries;
}
const QList<LibraryPtr> & LaunchProfile::getMavenFiles() const
{
return m_mavenFiles;
}
void LaunchProfile::getLibraryFiles(
const QString& architecture,
QStringList& jars,

View File

@@ -20,6 +20,7 @@ public: /* application of profile variables from patches */
void applyJarMods(const QList<LibraryPtr> &jarMods);
void applyMods(const QList<LibraryPtr> &jarMods);
void applyLibrary(LibraryPtr library);
void applyMavenFile(LibraryPtr library);
void applyMainJar(LibraryPtr jar);
void applyProblemSeverity(ProblemSeverity severity);
/// clear the profile
@@ -37,6 +38,7 @@ public: /* getters for profile variables */
const QList<LibraryPtr> & getJarMods() const;
const QList<LibraryPtr> & getLibraries() const;
const QList<LibraryPtr> & getNativeLibraries() const;
const QList<LibraryPtr> & getMavenFiles() const;
const LibraryPtr getMainJar() const;
void getLibraryFiles(
const QString & architecture,
@@ -79,10 +81,13 @@ private:
/// the list of libraries
QList<LibraryPtr> m_libraries;
/// the list of maven files to be placed in the libraries folder, but not acted upon
QList<LibraryPtr> m_mavenFiles;
/// the main jar
LibraryPtr m_mainJar;
/// the list of libraries
/// the list of native libraries
QList<LibraryPtr> m_nativeLibraries;
/// traits, collected from all the version files (version files can only add)

View File

@@ -21,12 +21,13 @@
#include "minecraft/launch/ModMinecraftJar.h"
#include "minecraft/launch/ClaimAccount.h"
#include "minecraft/launch/ReconstructAssets.h"
#include "minecraft/launch/ScanModFolders.h"
#include "java/launch/CheckJava.h"
#include "java/JavaUtils.h"
#include "meta/Index.h"
#include "meta/VersionList.h"
#include "SimpleModList.h"
#include "mod/ModFolderModel.h"
#include "WorldList.h"
#include "icons/IIconList.h"
@@ -550,37 +551,38 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session)
out << "";
}
if(loaderModList()->size())
{
out << "Mods:";
for(auto & mod: loaderModList()->allMods())
auto printModList = [&](const QString & label, ModFolderModel & model) {
if(model.size())
{
if(!mod.enabled())
continue;
if(mod.type() == Mod::MOD_FOLDER)
continue;
// TODO: proper implementation would need to descend into folders.
out << QString("%1:").arg(label);
auto modList = model.allMods();
std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) {
auto aName = a.filename().completeBaseName();
auto bName = b.filename().completeBaseName();
return aName.localeAwareCompare(bName) < 0;
});
for(auto & mod: modList)
{
if(mod.type() == Mod::MOD_FOLDER)
{
out << u8" [📁] " + mod.filename().completeBaseName() + " (folder)";
continue;
}
out << " " + mod.filename().completeBaseName();
if(mod.enabled()) {
out << u8" [✔️] " + mod.filename().completeBaseName();
}
else {
out << u8" [❌] " + mod.filename().completeBaseName() + " (disabled)";
}
}
out << "";
}
out << "";
}
};
if(coreModList()->size())
{
out << "Core Mods:";
for(auto & coremod: coreModList()->allMods())
{
if(!coremod.enabled())
continue;
if(coremod.type() == Mod::MOD_FOLDER)
continue;
// TODO: proper implementation would need to descend into folders.
out << " " + coremod.filename().completeBaseName();
}
out << "";
}
printModList("Mods", *(loaderModList().get()));
printModList("Core Mods", *(coreModList().get()));
auto & jarMods = profile->getJarMods();
if(jarMods.size())
@@ -827,6 +829,11 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(new ModMinecraftJar(pptr));
}
// if there are any jar mods
{
process->appendStep(new ScanModFolders(pptr));
}
// print some instance info here...
{
process->appendStep(new PrintInstanceInfo(pptr, session));
@@ -892,43 +899,47 @@ JavaVersion MinecraftInstance::getJavaVersion() const
return JavaVersion(settings()->get("JavaVersion").toString());
}
std::shared_ptr<SimpleModList> MinecraftInstance::loaderModList() const
std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const
{
if (!m_loader_mod_list)
{
m_loader_mod_list.reset(new SimpleModList(loaderModsDir()));
m_loader_mod_list.reset(new ModFolderModel(loaderModsDir()));
m_loader_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction);
}
m_loader_mod_list->update();
return m_loader_mod_list;
}
std::shared_ptr<SimpleModList> MinecraftInstance::coreModList() const
std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
{
if (!m_core_mod_list)
{
m_core_mod_list.reset(new SimpleModList(coreModsDir()));
m_core_mod_list.reset(new ModFolderModel(coreModsDir()));
m_core_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction);
}
m_core_mod_list->update();
return m_core_mod_list;
}
std::shared_ptr<SimpleModList> MinecraftInstance::resourcePackList() const
std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const
{
if (!m_resource_pack_list)
{
m_resource_pack_list.reset(new SimpleModList(resourcePacksDir()));
m_resource_pack_list.reset(new ModFolderModel(resourcePacksDir()));
m_resource_pack_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction);
}
m_resource_pack_list->update();
return m_resource_pack_list;
}
std::shared_ptr<SimpleModList> MinecraftInstance::texturePackList() const
std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
{
if (!m_texture_pack_list)
{
m_texture_pack_list.reset(new SimpleModList(texturePacksDir()));
m_texture_pack_list.reset(new ModFolderModel(texturePacksDir()));
m_texture_pack_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_texture_pack_list.get(), &ModFolderModel::disableInteraction);
}
m_texture_pack_list->update();
return m_texture_pack_list;
}

View File

@@ -1,13 +1,12 @@
#pragma once
#include "BaseInstance.h"
#include <java/JavaVersion.h>
#include "minecraft/Mod.h"
#include "minecraft/mod/Mod.h"
#include <QProcess>
#include <QDir>
#include "multimc_logic_export.h"
class ModsModel;
class SimpleModList;
class ModFolderModel;
class WorldList;
class GameOptions;
class LaunchStep;
@@ -68,11 +67,10 @@ public:
std::shared_ptr<ComponentList> getComponentList() const;
////// Mod Lists //////
std::shared_ptr<ModsModel> modsModel() const;
std::shared_ptr<SimpleModList> loaderModList() const;
std::shared_ptr<SimpleModList> coreModList() const;
std::shared_ptr<SimpleModList> resourcePackList() const;
std::shared_ptr<SimpleModList> texturePackList() const;
std::shared_ptr<ModFolderModel> loaderModList() const;
std::shared_ptr<ModFolderModel> coreModList() const;
std::shared_ptr<ModFolderModel> resourcePackList() const;
std::shared_ptr<ModFolderModel> texturePackList() const;
std::shared_ptr<WorldList> worldList() const;
std::shared_ptr<GameOptions> gameOptionsModel() const;
@@ -123,11 +121,10 @@ private:
protected: // data
std::shared_ptr<ComponentList> m_components;
mutable std::shared_ptr<ModsModel> m_mods_model;
mutable std::shared_ptr<SimpleModList> m_loader_mod_list;
mutable std::shared_ptr<SimpleModList> m_core_mod_list;
mutable std::shared_ptr<SimpleModList> m_resource_pack_list;
mutable std::shared_ptr<SimpleModList> m_texture_pack_list;
mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
mutable std::shared_ptr<ModFolderModel> m_resource_pack_list;
mutable std::shared_ptr<ModFolderModel> m_texture_pack_list;
mutable std::shared_ptr<WorldList> m_world_list;
mutable std::shared_ptr<GameOptions> m_game_options;
};

View File

@@ -1,433 +0,0 @@
/* Copyright 2013-2019 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <QDir>
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <quazip.h>
#include <quazipfile.h>
#include "Mod.h"
#include "settings/INIFile.h"
#include <FileSystem.h>
#include <QDebug>
Mod::Mod(const QFileInfo &file)
{
repath(file);
m_changedDateTime = file.lastModified();
}
void Mod::repath(const QFileInfo &file)
{
m_file = file;
QString name_base = file.fileName();
m_type = Mod::MOD_UNKNOWN;
if (m_file.isDir())
{
m_type = MOD_FOLDER;
m_name = name_base;
m_mmc_id = name_base;
}
else if (m_file.isFile())
{
if (name_base.endsWith(".disabled"))
{
m_enabled = false;
name_base.chop(9);
}
else
{
m_enabled = true;
}
m_mmc_id = name_base;
if (name_base.endsWith(".zip") || name_base.endsWith(".jar"))
{
m_type = MOD_ZIPFILE;
name_base.chop(4);
}
else if (name_base.endsWith(".litemod"))
{
m_type = MOD_LITEMOD;
name_base.chop(8);
}
else
{
m_type = MOD_SINGLEFILE;
}
m_name = name_base;
}
if (m_type == MOD_ZIPFILE)
{
QuaZip zip(m_file.filePath());
if (!zip.open(QuaZip::mdUnzip))
return;
QuaZipFile file(&zip);
if (zip.setCurrentFile("mcmod.info"))
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close();
return;
}
ReadMCModInfo(file.readAll());
file.close();
zip.close();
return;
}
else if (zip.setCurrentFile("fabric.mod.json"))
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close();
return;
}
ReadFabricModInfo(file.readAll());
file.close();
zip.close();
return;
}
else if (zip.setCurrentFile("forgeversion.properties"))
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close();
return;
}
ReadForgeInfo(file.readAll());
file.close();
zip.close();
return;
}
zip.close();
}
else if (m_type == MOD_FOLDER)
{
QFileInfo mcmod_info(FS::PathCombine(m_file.filePath(), "mcmod.info"));
if (mcmod_info.isFile())
{
QFile mcmod(mcmod_info.filePath());
if (!mcmod.open(QIODevice::ReadOnly))
return;
auto data = mcmod.readAll();
if (data.isEmpty() || data.isNull())
return;
ReadMCModInfo(data);
}
}
else if (m_type == MOD_LITEMOD)
{
QuaZip zip(m_file.filePath());
if (!zip.open(QuaZip::mdUnzip))
return;
QuaZipFile file(&zip);
if (zip.setCurrentFile("litemod.json"))
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close();
return;
}
ReadLiteModInfo(file.readAll());
file.close();
}
zip.close();
}
}
// NEW format
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3
// OLD format:
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
void Mod::ReadMCModInfo(QByteArray contents)
{
auto getInfoFromArray = [&](QJsonArray arr)->void
{
if (!arr.at(0).isObject())
return;
auto firstObj = arr.at(0).toObject();
m_mod_id = firstObj.value("modid").toString();
m_name = firstObj.value("name").toString();
m_version = firstObj.value("version").toString();
m_homeurl = firstObj.value("url").toString();
m_updateurl = firstObj.value("updateUrl").toString();
m_homeurl = m_homeurl.trimmed();
if(!m_homeurl.isEmpty())
{
// fix up url.
if (!m_homeurl.startsWith("http://") && !m_homeurl.startsWith("https://") &&
!m_homeurl.startsWith("ftp://"))
{
m_homeurl.prepend("http://");
}
}
m_description = firstObj.value("description").toString();
QJsonArray authors = firstObj.value("authorList").toArray();
if (authors.size() == 0)
authors = firstObj.value("authors").toArray();
if (authors.size() == 0)
m_authors = "";
else if (authors.size() >= 1)
{
m_authors = authors.at(0).toString();
for (int i = 1; i < authors.size(); i++)
{
m_authors += ", " + authors.at(i).toString();
}
}
m_credits = firstObj.value("credits").toString();
return;
}
;
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
// this is the very old format that had just the array
if (jsonDoc.isArray())
{
getInfoFromArray(jsonDoc.array());
}
else if (jsonDoc.isObject())
{
auto val = jsonDoc.object().value("modinfoversion");
if(val.isUndefined())
val = jsonDoc.object().value("modListVersion");
int version = val.toDouble();
if (version != 2)
{
qCritical() << "BAD stuff happened to mod json:";
qCritical() << contents;
return;
}
auto arrVal = jsonDoc.object().value("modlist");
if(arrVal.isUndefined())
arrVal = jsonDoc.object().value("modList");
if (arrVal.isArray())
{
getInfoFromArray(arrVal.toArray());
}
}
}
// https://fabricmc.net/wiki/documentation:fabric_mod_json
void Mod::ReadFabricModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0;
m_mod_id = object.value("id").toString();
m_version = object.value("version").toString();
m_name = object.contains("name") ? object.value("name").toString() : m_mod_id;
m_description = object.value("description").toString();
if (schemaVersion >= 1)
{
QJsonArray authors = object.value("authors").toArray();
m_authors = "";
for (int i = 0; i < authors.size(); i++)
{
QString author_name = authors.at(i).isObject()
? authors.at(i).toObject().value("name").toString()
: authors.at(i).toString();
if (i > 0)
m_authors += ", " + author_name;
else {
m_authors += author_name;
}
}
if (object.contains("contact"))
{
QJsonObject contact = object.value("contact").toObject();
if (contact.contains("homepage"))
m_homeurl = contact.value("homepage").toString();
}
}
}
void Mod::ReadForgeInfo(QByteArray contents)
{
// Read the data
m_name = "Minecraft Forge";
m_mod_id = "Forge";
m_homeurl = "http://www.minecraftforge.net/forum/";
INIFile ini;
if (!ini.loadFile(contents))
return;
QString major = ini.get("forge.major.number", "0").toString();
QString minor = ini.get("forge.minor.number", "0").toString();
QString revision = ini.get("forge.revision.number", "0").toString();
QString build = ini.get("forge.build.number", "0").toString();
m_version = major + "." + minor + "." + revision + "." + build;
}
void Mod::ReadLiteModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
if (object.contains("name"))
{
m_mod_id = m_name = object.value("name").toString();
}
if (object.contains("version"))
{
m_version = object.value("version").toString("");
}
else
{
m_version = object.value("revision").toString("");
}
m_mcversion = object.value("mcversion").toString();
m_authors = object.value("author").toString();
m_description = object.value("description").toString();
m_homeurl = object.value("url").toString();
}
bool Mod::replace(Mod &with)
{
if (!destroy())
return false;
bool success = false;
auto t = with.type();
if (t == MOD_ZIPFILE || t == MOD_SINGLEFILE || t == MOD_LITEMOD)
{
qDebug() << "Copy: " << with.m_file.filePath() << " to " << m_file.filePath();
success = QFile::copy(with.m_file.filePath(), m_file.filePath());
}
if (t == MOD_FOLDER)
{
success = FS::copy(with.m_file.filePath(), m_file.path())();
}
if (success)
{
m_name = with.m_name;
m_mmc_id = with.m_mmc_id;
m_mod_id = with.m_mod_id;
m_version = with.m_version;
m_mcversion = with.m_mcversion;
m_description = with.m_description;
m_authors = with.m_authors;
m_credits = with.m_credits;
m_homeurl = with.m_homeurl;
m_type = with.m_type;
m_file.refresh();
}
return success;
}
bool Mod::destroy()
{
if (m_type == MOD_FOLDER)
{
QDir d(m_file.filePath());
if (d.removeRecursively())
{
m_type = MOD_UNKNOWN;
return true;
}
return false;
}
else if (m_type == MOD_SINGLEFILE || m_type == MOD_ZIPFILE || m_type == MOD_LITEMOD)
{
QFile f(m_file.filePath());
if (f.remove())
{
m_type = MOD_UNKNOWN;
return true;
}
return false;
}
return true;
}
QString Mod::version() const
{
switch (type())
{
case MOD_ZIPFILE:
case MOD_LITEMOD:
return m_version;
case MOD_FOLDER:
return "Folder";
case MOD_SINGLEFILE:
return "File";
default:
return "VOID";
}
}
bool Mod::enable(bool value)
{
if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER)
return false;
if (m_enabled == value)
return false;
QString path = m_file.absoluteFilePath();
if (value)
{
QFile foo(path);
if (!path.endsWith(".disabled"))
return false;
path.chop(9);
if (!foo.rename(path))
return false;
}
else
{
QFile foo(path);
path += ".disabled";
if (!foo.rename(path))
return false;
}
m_file = QFileInfo(path);
m_enabled = value;
return true;
}
bool Mod::operator==(const Mod &other) const
{
return mmc_id() == other.mmc_id();
}
bool Mod::strongCompare(const Mod &other) const
{
return mmc_id() == other.mmc_id() && version() == other.version() && type() == other.type();
}

View File

@@ -144,14 +144,14 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
}
}
auto readLibs = [&](const char * which)
auto readLibs = [&](const char * which, QList<LibraryPtr> & out)
{
for (auto libVal : requireArray(root.value(which)))
{
QJsonObject libObj = requireObject(libVal);
// parse the library
auto lib = libraryFromJson(libObj, filename);
out->libraries.append(lib);
out.append(lib);
}
};
bool hasPlusLibs = root.contains("+libraries");
@@ -160,16 +160,20 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
{
out->addProblem(ProblemSeverity::Warning,
QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported."));
readLibs("libraries");
readLibs("+libraries");
readLibs("libraries", out->libraries);
readLibs("+libraries", out->libraries);
}
else if (hasLibs)
{
readLibs("libraries");
readLibs("libraries", out->libraries);
}
else if(hasPlusLibs)
{
readLibs("+libraries");
readLibs("+libraries", out->libraries);
}
if(root.contains("mavenFiles")) {
readLibs("mavenFiles", out->mavenFiles);
}
// if we have mainJar, just use it
@@ -276,6 +280,15 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch
}
root.insert("libraries", array);
}
if (!patch->mavenFiles.isEmpty())
{
QJsonArray array;
for (auto value: patch->mavenFiles)
{
array.append(OneSixVersionFormat::libraryToJson(value.get()));
}
root.insert("mavenFiles", array);
}
if (!patch->jarMods.isEmpty())
{
QJsonArray array;

View File

@@ -33,7 +33,7 @@ slots:
auto time_parsed = timeFromS3Time(timestamp);
auto time_serialized = timeToS3Time(time_parsed);
QCOMPARE(time_serialized, timestamp);
}

View File

@@ -41,6 +41,10 @@ void VersionFile::applyTo(LaunchProfile *profile)
{
profile->applyLibrary(library);
}
for (auto mavenFile : mavenFiles)
{
profile->applyMavenFile(mavenFile);
}
profile->applyProblemSeverity(getProblemSeverity());
}
@@ -53,4 +57,4 @@ void VersionFile::applyTo(LaunchProfile *profile)
throw MinecraftVersionMismatch(uid, dependsOnMinecraftVersion, theirVersion);
}
}
*/
*/

View File

@@ -75,6 +75,9 @@ public: /* data */
/// Mojang: list of libraries to add to the version
QList<LibraryPtr> libraries;
/// MultiMC: list of maven files to put in the libraries folder, but not in classpath
QList<LibraryPtr> mavenFiles;
/// The main jar (Minecraft version library, normally)
LibraryPtr mainJar;

View File

@@ -429,7 +429,3 @@ bool World::operator==(const World &other) const
{
return is_valid == other.is_valid && folderName() == other.folderName();
}
bool World::strongCompare(const World &other) const
{
return is_valid == other.is_valid && folderName() == other.folderName();
}

View File

@@ -76,7 +76,6 @@ public:
// WEAK compare operator - used for replacing worlds
bool operator==(const World &other) const;
bool strongCompare(const World &other) const;
private:
void readFromZip(const QFileInfo &file);

View File

@@ -0,0 +1,54 @@
/* Copyright 2013-2019 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ScanModFolders.h"
#include "launch/LaunchTask.h"
#include "MMCZip.h"
#include "minecraft/OpSys.h"
#include "FileSystem.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/mod/ModFolderModel.h"
void ScanModFolders::executeTask()
{
auto m_inst = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance());
auto loaders = m_inst->loaderModList();
connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone);
loaders->update();
auto cores = m_inst->coreModList();
connect(cores.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone);
cores->update();
}
void ScanModFolders::modsDone()
{
m_modsDone = true;
checkDone();
}
void ScanModFolders::coreModsDone()
{
m_coreModsDone = true;
checkDone();
}
void ScanModFolders::checkDone()
{
if(m_modsDone && m_coreModsDone) {
emitSucceeded();
}
}

View File

@@ -0,0 +1,42 @@
/* Copyright 2013-2019 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <launch/LaunchStep.h>
#include <memory>
class ScanModFolders: public LaunchStep
{
Q_OBJECT
public:
explicit ScanModFolders(LaunchTask *parent) : LaunchStep(parent) {};
virtual ~ScanModFolders(){};
virtual void executeTask() override;
virtual bool canAbort() const override
{
return false;
}
private slots:
void coreModsDone();
void modsDone();
private:
void checkDone();
private: // DATA
bool m_modsDone = false;
bool m_coreModsDone = false;
};

View File

@@ -21,7 +21,6 @@
#include "LegacyInstance.h"
#include "minecraft/legacy/LegacyModList.h"
#include "minecraft/SimpleModList.h"
#include "minecraft/WorldList.h"
#include <MMCZip.h>
#include <FileSystem.h>
@@ -107,11 +106,6 @@ std::shared_ptr<LegacyModList> LegacyInstance::jarModList() const
return jar_mod_list;
}
QList<Mod> LegacyInstance::getJarMods() const
{
return jarModList()->allMods();
}
QString LegacyInstance::gameRoot() const
{
QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft"));

View File

@@ -16,12 +16,11 @@
#pragma once
#include "BaseInstance.h"
#include "minecraft/Mod.h"
#include "launch/LaunchTask.h"
#include "multimc_logic_export.h"
class SimpleModList;
class ModFolderModel;
class LegacyModList;
class WorldList;
class Task;
@@ -78,7 +77,6 @@ public:
QString customBaseJar() const;
std::shared_ptr<LegacyModList> jarModList() const;
QList<Mod> getJarMods() const;
std::shared_ptr<WorldList> worldList() const;
/*!

View File

@@ -22,8 +22,7 @@ LegacyModList::LegacyModList(const QString &dir, const QString &list_file)
: m_dir(dir), m_list_file(list_file)
{
FS::ensureFolderPathExists(m_dir.absolutePath());
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs |
QDir::NoSymLinks);
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
}
@@ -34,15 +33,11 @@ LegacyModList::LegacyModList(const QString &dir, const QString &list_file)
};
typedef QList<OrderItem> OrderList;
static void internalSort(QList<Mod> &what)
static void internalSort(QList<LegacyModList::Mod> &what)
{
auto predicate = [](const Mod &left, const Mod &right)
auto predicate = [](const LegacyModList::Mod &left, const LegacyModList::Mod &right)
{
if (left.name() == right.name())
{
return left.mmc_id().localeAwareCompare(right.mmc_id()) < 0;
}
return left.name().localeAwareCompare(right.name()) < 0;
return left.fileName().localeAwareCompare(right.fileName()) < 0;
};
std::sort(what.begin(), what.end(), predicate);
}
@@ -90,7 +85,6 @@ bool LegacyModList::update()
QList<Mod> newMods;
m_dir.refresh();
auto folderContents = m_dir.entryInfoList();
bool orderOrStateChanged = false;
// first, process the ordered items (if any)
OrderList listOrder = readListFile(m_list_file);
@@ -124,48 +118,19 @@ bool LegacyModList::update()
// remove from the actual folder contents list
folderContents.takeAt(idx);
// append the new mod
orderedMods.append(Mod(info));
if (isEnabled != item.enabled)
orderOrStateChanged = true;
}
else
{
orderOrStateChanged = true;
orderedMods.append(info);
}
}
// if there are any untracked files...
// if there are any untracked files... append them sorted at the end
if (folderContents.size())
{
// the order surely changed!
for (auto entry : folderContents)
{
newMods.append(Mod(entry));
newMods.append(entry);
}
internalSort(newMods);
orderedMods.append(newMods);
orderOrStateChanged = true;
}
// otherwise, if we were already tracking some mods
else if (mods.size())
{
// if the number doesn't match, order changed.
if (mods.size() != orderedMods.size())
orderOrStateChanged = true;
// if it does match, compare the mods themselves
else
for (int i = 0; i < mods.size(); i++)
{
if (!mods[i].strongCompare(orderedMods[i]))
{
orderOrStateChanged = true;
break;
}
}
}
mods.swap(orderedMods);
if (orderOrStateChanged && !m_list_file.isEmpty())
{
qDebug() << "Mod list " << m_list_file << " changed!";
}
return true;
}

View File

@@ -19,21 +19,14 @@
#include <QString>
#include <QDir>
#include "minecraft/Mod.h"
#include "multimc_logic_export.h"
class LegacyInstance;
class BaseInstance;
/**
* A legacy mod list.
* Backed by a folder.
*/
class MULTIMC_LOGIC_EXPORT LegacyModList
{
public:
using Mod = QFileInfo;
LegacyModList(const QString &dir, const QString &list_file = QString());
/// Reloads the mod list and returns true if the list changed.

View File

@@ -7,6 +7,7 @@
#include "LegacyInstance.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/ComponentList.h"
#include "LegacyModList.h"
#include "classparser.h"
LegacyUpgradeTask::LegacyUpgradeTask(InstancePtr origInstance)
@@ -96,10 +97,10 @@ void LegacyUpgradeTask::copyFinished()
components->installCustomJar(jarPath);
}
auto jarMods = legacyInst->getJarMods();
auto jarMods = legacyInst->jarModList()->allMods();
for(auto & jarMod: jarMods)
{
QString modPath = jarMod.filename().absoluteFilePath();
QString modPath = jarMod.absoluteFilePath();
qDebug() << "jarMod: " << modPath;
components->installJarMods({modPath});
}

View File

@@ -0,0 +1,298 @@
#include "LocalModParseTask.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <quazip.h>
#include <quazipfile.h>
#include "settings/INIFile.h"
#include "FileSystem.h"
namespace {
// NEW format
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3
// OLD format:
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
{
auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr<ModDetails>
{
if (!arr.at(0).isObject()) {
return nullptr;
}
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
auto firstObj = arr.at(0).toObject();
details->mod_id = firstObj.value("modid").toString();
auto name = firstObj.value("name").toString();
// NOTE: ignore stupid example mods copies where the author didn't even bother to change the name
if(name != "Example Mod") {
details->name = name;
}
details->version = firstObj.value("version").toString();
details->updateurl = firstObj.value("updateUrl").toString();
auto homeurl = firstObj.value("url").toString().trimmed();
if(!homeurl.isEmpty())
{
// fix up url.
if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://"))
{
homeurl.prepend("http://");
}
}
details->homeurl = homeurl;
details->description = firstObj.value("description").toString();
QJsonArray authors = firstObj.value("authorList").toArray();
if (authors.size() == 0) {
// FIXME: what is the format of this? is there any?
authors = firstObj.value("authors").toArray();
}
for (auto author: authors)
{
details->authors.append(author.toString());
}
details->credits = firstObj.value("credits").toString();
return details;
};
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
// this is the very old format that had just the array
if (jsonDoc.isArray())
{
return getInfoFromArray(jsonDoc.array());
}
else if (jsonDoc.isObject())
{
auto val = jsonDoc.object().value("modinfoversion");
if(val.isUndefined()) {
val = jsonDoc.object().value("modListVersion");
}
int version = val.toDouble();
if (version != 2)
{
qCritical() << "BAD stuff happened to mod json:";
qCritical() << contents;
return nullptr;
}
auto arrVal = jsonDoc.object().value("modlist");
if(arrVal.isUndefined()) {
arrVal = jsonDoc.object().value("modList");
}
if (arrVal.isArray())
{
return getInfoFromArray(arrVal.toArray());
}
}
return nullptr;
}
// https://fabricmc.net/wiki/documentation:fabric_mod_json
std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0;
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
details->mod_id = object.value("id").toString();
details->version = object.value("version").toString();
details->name = object.contains("name") ? object.value("name").toString() : details->mod_id;
details->description = object.value("description").toString();
if (schemaVersion >= 1)
{
QJsonArray authors = object.value("authors").toArray();
for (auto author: authors)
{
if(author.isObject()) {
details->authors.append(author.toObject().value("name").toString());
}
else {
details->authors.append(author.toString());
}
}
if (object.contains("contact"))
{
QJsonObject contact = object.value("contact").toObject();
if (contact.contains("homepage"))
{
details->homeurl = contact.value("homepage").toString();
}
}
}
return details;
}
std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
{
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
// Read the data
details->name = "Minecraft Forge";
details->mod_id = "Forge";
details->homeurl = "http://www.minecraftforge.net/forum/";
INIFile ini;
if (!ini.loadFile(contents))
return details;
QString major = ini.get("forge.major.number", "0").toString();
QString minor = ini.get("forge.minor.number", "0").toString();
QString revision = ini.get("forge.revision.number", "0").toString();
QString build = ini.get("forge.build.number", "0").toString();
details->version = major + "." + minor + "." + revision + "." + build;
return details;
}
std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents)
{
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
if (object.contains("name"))
{
details->mod_id = details->name = object.value("name").toString();
}
if (object.contains("version"))
{
details->version = object.value("version").toString("");
}
else
{
details->version = object.value("revision").toString("");
}
details->mcversion = object.value("mcversion").toString();
auto author = object.value("author").toString();
if(!author.isEmpty()) {
details->authors.append(author);
}
details->description = object.value("description").toString();
details->homeurl = object.value("url").toString();
return details;
}
}
LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile):
m_token(token),
m_type(type),
m_modFile(modFile),
m_result(new Result())
{
}
void LocalModParseTask::processAsZip()
{
QuaZip zip(m_modFile.filePath());
if (!zip.open(QuaZip::mdUnzip))
return;
QuaZipFile file(&zip);
if (zip.setCurrentFile("mcmod.info"))
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close();
return;
}
m_result->details = ReadMCModInfo(file.readAll());
file.close();
zip.close();
return;
}
else if (zip.setCurrentFile("fabric.mod.json"))
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close();
return;
}
m_result->details = ReadFabricModInfo(file.readAll());
file.close();
zip.close();
return;
}
else if (zip.setCurrentFile("forgeversion.properties"))
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close();
return;
}
m_result->details = ReadForgeInfo(file.readAll());
file.close();
zip.close();
return;
}
zip.close();
}
void LocalModParseTask::processAsFolder()
{
QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info"));
if (mcmod_info.isFile())
{
QFile mcmod(mcmod_info.filePath());
if (!mcmod.open(QIODevice::ReadOnly))
return;
auto data = mcmod.readAll();
if (data.isEmpty() || data.isNull())
return;
m_result->details = ReadMCModInfo(data);
}
}
void LocalModParseTask::processAsLitemod()
{
QuaZip zip(m_modFile.filePath());
if (!zip.open(QuaZip::mdUnzip))
return;
QuaZipFile file(&zip);
if (zip.setCurrentFile("litemod.json"))
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close();
return;
}
m_result->details = ReadLiteModInfo(file.readAll());
file.close();
}
zip.close();
}
void LocalModParseTask::run()
{
switch(m_type)
{
case Mod::MOD_ZIPFILE:
processAsZip();
break;
case Mod::MOD_FOLDER:
processAsFolder();
break;
case Mod::MOD_LITEMOD:
processAsLitemod();
break;
default:
break;
}
emit finished(m_token);
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include <QRunnable>
#include <QDebug>
#include <QObject>
#include "Mod.h"
#include "ModDetails.h"
class LocalModParseTask : public QObject, public QRunnable
{
Q_OBJECT
public:
struct Result {
QString id;
std::shared_ptr<ModDetails> details;
};
using ResultPtr = std::shared_ptr<Result>;
ResultPtr result() const {
return m_result;
}
LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile);
void run();
signals:
void finished(int token);
private:
void processAsZip();
void processAsFolder();
void processAsLitemod();
private:
int m_token;
Mod::ModType m_type;
QFileInfo m_modFile;
ResultPtr m_result;
};

View File

@@ -0,0 +1,151 @@
/* Copyright 2013-2019 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <QDir>
#include <QString>
#include "Mod.h"
#include <QDebug>
#include <FileSystem.h>
namespace {
ModDetails invalidDetails;
}
Mod::Mod(const QFileInfo &file)
{
repath(file);
m_changedDateTime = file.lastModified();
}
void Mod::repath(const QFileInfo &file)
{
m_file = file;
QString name_base = file.fileName();
m_type = Mod::MOD_UNKNOWN;
m_mmc_id = name_base;
if (m_file.isDir())
{
m_type = MOD_FOLDER;
m_name = name_base;
}
else if (m_file.isFile())
{
if (name_base.endsWith(".disabled"))
{
m_enabled = false;
name_base.chop(9);
}
else
{
m_enabled = true;
}
if (name_base.endsWith(".zip") || name_base.endsWith(".jar"))
{
m_type = MOD_ZIPFILE;
name_base.chop(4);
}
else if (name_base.endsWith(".litemod"))
{
m_type = MOD_LITEMOD;
name_base.chop(8);
}
else
{
m_type = MOD_SINGLEFILE;
}
m_name = name_base;
}
}
bool Mod::enable(bool value)
{
if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER)
return false;
if (m_enabled == value)
return false;
QString path = m_file.absoluteFilePath();
if (value)
{
QFile foo(path);
if (!path.endsWith(".disabled"))
return false;
path.chop(9);
if (!foo.rename(path))
return false;
}
else
{
QFile foo(path);
path += ".disabled";
if (!foo.rename(path))
return false;
}
repath(QFileInfo(path));
m_enabled = value;
return true;
}
bool Mod::destroy()
{
m_type = MOD_UNKNOWN;
return FS::deletePath(m_file.filePath());
}
const ModDetails & Mod::details() const
{
if(!m_localDetails)
return invalidDetails;
return *m_localDetails;
}
QString Mod::version() const
{
return details().version;
}
QString Mod::name() const
{
auto & d = details();
if(!d.name.isEmpty()) {
return d.name;
}
return m_name;
}
QString Mod::homeurl() const
{
return details().homeurl;
}
QString Mod::description() const
{
return details().description;
}
QStringList Mod::authors() const
{
return details().authors;
}

View File

@@ -16,8 +16,16 @@
#pragma once
#include <QFileInfo>
#include <QDateTime>
#include <QList>
#include <memory>
class Mod
#include "multimc_logic_export.h"
#include "ModDetails.h"
class MULTIMC_LOGIC_EXPORT Mod
{
public:
enum ModType
@@ -29,6 +37,7 @@ public:
MOD_LITEMOD, //!< The mod is a litemod
};
Mod() = default;
Mod(const QFileInfo &file);
QFileInfo filename() const
@@ -39,54 +48,14 @@ public:
{
return m_mmc_id;
}
QString mod_id() const
{
return m_mod_id;
}
ModType type() const
{
return m_type;
}
QString mcversion() const
{
return m_mcversion;
}
;
bool valid()
{
return m_type != MOD_UNKNOWN;
}
QString name() const
{
QString name = m_name.trimmed();
if(name.isEmpty() || name == "Example Mod")
{
return m_mmc_id;
}
return m_name;
}
QString version() const;
QString homeurl() const
{
return m_homeurl;
}
QString description() const
{
return m_description;
}
QString authors() const
{
return m_authors;
}
QString credits() const
{
return m_credits;
}
QDateTime dateTimeChanged() const
{
@@ -98,46 +67,51 @@ public:
return m_enabled;
}
const ModDetails &details() const;
QString name() const;
QString version() const;
QString homeurl() const;
QString description() const;
QStringList authors() const;
bool enable(bool value);
// delete all the files of this mod
bool destroy();
// replace this mod with a copy of the other
bool replace(Mod &with);
// change the mod's filesystem path (used by mod lists for *MAGIC* purposes)
void repath(const QFileInfo &file);
// WEAK compare operator - used for replacing mods
bool operator==(const Mod &other) const;
bool strongCompare(const Mod &other) const;
private:
void ReadMCModInfo(QByteArray contents);
void ReadFabricModInfo(QByteArray contents);
void ReadForgeInfo(QByteArray contents);
void ReadLiteModInfo(QByteArray contents);
bool shouldResolve() {
return !m_resolving && !m_resolved;
}
bool isResolving() {
return m_resolving;
}
int resolutionTicket()
{
return m_resolutionTicket;
}
void setResolving(bool resolving, int resolutionTicket) {
m_resolving = resolving;
m_resolutionTicket = resolutionTicket;
}
void finishResolvingWithDetails(std::shared_ptr<ModDetails> details){
m_resolving = false;
m_resolved = true;
m_localDetails = details;
}
protected:
// FIXME: what do do with those? HMM...
/*
void ReadModInfoData(QString info);
void ReadForgeInfoData(QString infoFileData);
*/
QFileInfo m_file;
QDateTime m_changedDateTime;
QString m_mmc_id;
QString m_mod_id;
bool m_enabled = true;
QString m_name;
QString m_version;
QString m_mcversion;
QString m_homeurl;
QString m_updateurl;
QString m_description;
QString m_authors;
QString m_credits;
ModType m_type;
bool m_enabled = true;
bool m_resolving = false;
bool m_resolved = false;
int m_resolutionTicket = 0;
ModType m_type = MOD_UNKNOWN;
std::shared_ptr<ModDetails> m_localDetails;
};

View File

@@ -0,0 +1,17 @@
#pragma once
#include <QString>
#include <QStringList>
struct ModDetails
{
QString mod_id;
QString name;
QString version;
QString mcversion;
QString homeurl;
QString updateurl;
QString description;
QStringList authors;
QString credits;
};

View File

@@ -0,0 +1,18 @@
#include "ModFolderLoadTask.h"
#include <QDebug>
ModFolderLoadTask::ModFolderLoadTask(QDir dir) :
m_dir(dir), m_result(new Result())
{
}
void ModFolderLoadTask::run()
{
m_dir.refresh();
for (auto entry : m_dir.entryInfoList())
{
Mod m(entry);
m_result->mods[m.mmc_id()] = m;
}
emit succeeded();
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <QRunnable>
#include <QObject>
#include <QDir>
#include <QMap>
#include "Mod.h"
#include <memory>
class ModFolderLoadTask : public QObject, public QRunnable
{
Q_OBJECT
public:
struct Result {
QMap<QString, Mod> mods;
};
using ResultPtr = std::shared_ptr<Result>;
ResultPtr result() const {
return m_result;
}
public:
ModFolderLoadTask(QDir dir);
void run();
signals:
void succeeded();
private:
QDir m_dir;
ResultPtr m_result;
};

View File

@@ -13,7 +13,7 @@
* limitations under the License.
*/
#include "SimpleModList.h"
#include "ModFolderModel.h"
#include <FileSystem.h>
#include <QMimeData>
#include <QUrl>
@@ -21,18 +21,21 @@
#include <QString>
#include <QFileSystemWatcher>
#include <QDebug>
#include "ModFolderLoadTask.h"
#include <QThreadPool>
#include <algorithm>
#include "LocalModParseTask.h"
SimpleModList::SimpleModList(const QString &dir) : QAbstractListModel(), m_dir(dir)
ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir)
{
FS::ensureFolderPathExists(m_dir.absolutePath());
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs |
QDir::NoSymLinks);
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
m_watcher = new QFileSystemWatcher(this);
connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
}
void SimpleModList::startWatching()
void ModFolderModel::startWatching()
{
if(is_watching)
return;
@@ -50,7 +53,7 @@ void SimpleModList::startWatching()
}
}
void SimpleModList::stopWatching()
void ModFolderModel::stopWatching()
{
if(!is_watching)
return;
@@ -66,39 +69,164 @@ void SimpleModList::stopWatching()
}
}
bool SimpleModList::update()
bool ModFolderModel::update()
{
if (!isValid())
if (!isValid()) {
return false;
QList<Mod> newMods;
m_dir.refresh();
for (auto entry : m_dir.entryInfoList())
{
newMods.append(Mod(entry));
}
if(m_update) {
scheduled_update = true;
return true;
}
beginResetModel();
mods.swap(newMods);
endResetModel();
emit changed();
auto task = new ModFolderLoadTask(m_dir);
m_update = task->result();
QThreadPool *threadPool = QThreadPool::globalInstance();
connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate);
threadPool->start(task);
return true;
}
void SimpleModList::directoryChanged(QString path)
void ModFolderModel::finishUpdate()
{
QSet<QString> currentSet = modsIndex.keys().toSet();
auto & newMods = m_update->mods;
QSet<QString> newSet = newMods.keys().toSet();
// see if the kept mods changed in some way
{
QSet<QString> kept = currentSet;
kept.intersect(newSet);
for(auto & keptMod: kept) {
auto & newMod = newMods[keptMod];
auto row = modsIndex[keptMod];
auto & currentMod = mods[row];
if(newMod.dateTimeChanged() == currentMod.dateTimeChanged()) {
// no significant change, ignore...
continue;
}
auto & oldMod = mods[row];
if(oldMod.isResolving()) {
activeTickets.remove(oldMod.resolutionTicket());
}
oldMod = newMod;
resolveMod(mods[row]);
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
}
}
// remove mods no longer present
{
QSet<QString> removed = currentSet;
QList<int> removedRows;
removed.subtract(newSet);
for(auto & removedMod: removed) {
removedRows.append(modsIndex[removedMod]);
}
std::sort(removedRows.begin(), removedRows.end(), std::greater<int>());
for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) {
int removedIndex = *iter;
beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
auto removedIter = mods.begin() + removedIndex;
if(removedIter->isResolving()) {
activeTickets.remove(removedIter->resolutionTicket());
}
mods.erase(removedIter);
endRemoveRows();
}
}
// add new mods to the end
{
QSet<QString> added = newSet;
added.subtract(currentSet);
beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1);
for(auto & addedMod: added) {
mods.append(newMods[addedMod]);
resolveMod(mods.last());
}
endInsertRows();
}
// update index
{
modsIndex.clear();
int idx = 0;
for(auto & mod: mods) {
modsIndex[mod.mmc_id()] = idx;
idx++;
}
}
m_update.reset();
emit updateFinished();
if(scheduled_update) {
scheduled_update = false;
update();
}
}
void ModFolderModel::resolveMod(Mod& m)
{
if(!m.shouldResolve()) {
return;
}
auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.filename());
auto result = task->result();
result->id = m.mmc_id();
activeTickets.insert(nextResolutionTicket, result);
m.setResolving(true, nextResolutionTicket);
nextResolutionTicket++;
QThreadPool *threadPool = QThreadPool::globalInstance();
connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse);
threadPool->start(task);
}
void ModFolderModel::finishModParse(int token)
{
auto iter = activeTickets.find(token);
if(iter == activeTickets.end()) {
return;
}
auto result = *iter;
activeTickets.remove(token);
int row = modsIndex[result->id];
auto & mod = mods[row];
mod.finishResolvingWithDetails(result->details);
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}
void ModFolderModel::disableInteraction(bool disabled)
{
if (interaction_disabled == disabled) {
return;
}
interaction_disabled = disabled;
if(size()) {
emit dataChanged(index(0), index(size() - 1));
}
}
void ModFolderModel::directoryChanged(QString path)
{
update();
}
bool SimpleModList::isValid()
bool ModFolderModel::isValid()
{
return m_dir.exists() && m_dir.isReadable();
}
// FIXME: this does not take disabled mod (with extra .disable extension) into account...
bool SimpleModList::installMod(const QString &filename)
bool ModFolderModel::installMod(const QString &filename)
{
if(interaction_disabled) {
return false;
}
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
auto originalPath = FS::NormalizePath(filename);
QFileInfo fileinfo(originalPath);
@@ -175,23 +303,31 @@ bool SimpleModList::installMod(const QString &filename)
return false;
}
bool SimpleModList::enableMods(const QModelIndexList& indexes, bool enable)
bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable)
{
if(interaction_disabled) {
return false;
}
if(indexes.isEmpty())
return true;
for (auto i: indexes)
for (auto index: indexes)
{
Mod &m = mods[i.row()];
m.enable(enable);
emit dataChanged(i, i);
if(index.column() != 0) {
continue;
}
setModStatus(index.row(), enable);
}
emit changed();
return true;
}
bool SimpleModList::deleteMods(const QModelIndexList& indexes)
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{
if(interaction_disabled) {
return false;
}
if(indexes.isEmpty())
return true;
@@ -200,16 +336,15 @@ bool SimpleModList::deleteMods(const QModelIndexList& indexes)
Mod &m = mods[i.row()];
m.destroy();
}
emit changed();
return true;
}
int SimpleModList::columnCount(const QModelIndex &parent) const
int ModFolderModel::columnCount(const QModelIndex &parent) const
{
return NUM_COLUMNS;
}
QVariant SimpleModList::data(const QModelIndex &index, int role) const
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
@@ -227,8 +362,17 @@ QVariant SimpleModList::data(const QModelIndex &index, int role) const
{
case NameColumn:
return mods[row].name();
case VersionColumn:
case VersionColumn: {
switch(mods[row].type()) {
case Mod::MOD_FOLDER:
return tr("Folder");
case Mod::MOD_SINGLEFILE:
return tr("File");
default:
break;
}
return mods[row].version();
}
case DateColumn:
return mods[row].dateTimeChanged();
@@ -252,7 +396,7 @@ QVariant SimpleModList::data(const QModelIndex &index, int role) const
}
}
bool SimpleModList::setData(const QModelIndex &index, const QVariant &value, int role)
bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
{
@@ -261,17 +405,53 @@ bool SimpleModList::setData(const QModelIndex &index, const QVariant &value, int
if (role == Qt::CheckStateRole)
{
auto &mod = mods[index.row()];
if (mod.enable(!mod.enabled()))
{
emit dataChanged(index, index);
return true;
}
return setModStatus(index.row(), Toggle);
}
return false;
}
QVariant SimpleModList::headerData(int section, Qt::Orientation orientation, int role) const
bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action)
{
if(row < 0 || row >= mods.size()) {
return false;
}
auto &mod = mods[row];
bool desiredStatus;
switch(action) {
case Enable:
desiredStatus = true;
break;
case Disable:
desiredStatus = false;
break;
case Toggle:
default:
desiredStatus = !mod.enabled();
break;
}
if(desiredStatus == mod.enabled()) {
return true;
}
// preserve the row, but change its ID
auto oldId = mod.mmc_id();
if(!mod.enable(!mod.enabled())) {
return false;
}
auto newId = mod.mmc_id();
if(modsIndex.contains(newId)) {
// NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled
// But is it necessary?
}
modsIndex.remove(oldId);
modsIndex[newId] = row;
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
return true;
}
QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
{
switch (role)
{
@@ -310,30 +490,37 @@ QVariant SimpleModList::headerData(int section, Qt::Orientation orientation, int
return QVariant();
}
Qt::ItemFlags SimpleModList::flags(const QModelIndex &index) const
Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
if (index.isValid())
return Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled |
defaultFlags;
auto flags = defaultFlags;
if(interaction_disabled) {
flags &= ~Qt::ItemIsDropEnabled;
}
else
return Qt::ItemIsDropEnabled | defaultFlags;
{
flags |= Qt::ItemIsDropEnabled;
if(index.isValid()) {
flags |= Qt::ItemIsUserCheckable;
}
}
return flags;
}
Qt::DropActions SimpleModList::supportedDropActions() const
Qt::DropActions ModFolderModel::supportedDropActions() const
{
// copy from outside, move from within and other mod lists
return Qt::CopyAction | Qt::MoveAction;
}
QStringList SimpleModList::mimeTypes() const
QStringList ModFolderModel::mimeTypes() const
{
QStringList types;
types << "text/uri-list";
return types;
}
bool SimpleModList::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
{
if (action == Qt::IgnoreAction)
{

View File

@@ -16,13 +16,17 @@
#pragma once
#include <QList>
#include <QMap>
#include <QSet>
#include <QString>
#include <QDir>
#include <QAbstractListModel>
#include "minecraft/Mod.h"
#include "Mod.h"
#include "multimc_logic_export.h"
#include "ModFolderLoadTask.h"
#include "LocalModParseTask.h"
class LegacyInstance;
class BaseInstance;
@@ -32,7 +36,7 @@ class QFileSystemWatcher;
* A legacy mod list.
* Backed by a folder.
*/
class MULTIMC_LOGIC_EXPORT SimpleModList : public QAbstractListModel
class MULTIMC_LOGIC_EXPORT ModFolderModel : public QAbstractListModel
{
Q_OBJECT
public:
@@ -40,11 +44,16 @@ public:
{
ActiveColumn = 0,
NameColumn,
DateColumn,
VersionColumn,
DateColumn,
NUM_COLUMNS
};
SimpleModList(const QString &dir);
enum ModStatusAction {
Disable,
Enable,
Toggle
};
ModFolderModel(const QString &dir);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
@@ -76,9 +85,13 @@ public:
{
return mods[index];
}
const Mod &at(size_t index) const
{
return mods.at(index);
}
/// Reloads the mod list and returns true if the list changed.
virtual bool update();
bool update();
/**
* Adds the given mod to the list at the given index - if the list supports custom ordering
@@ -86,15 +99,15 @@ public:
bool installMod(const QString& filename);
/// Deletes all the selected mods
virtual bool deleteMods(const QModelIndexList &indexes);
bool deleteMods(const QModelIndexList &indexes);
/// Enable or disable listed mods
virtual bool enableMods(const QModelIndexList &indexes, bool enable = true);
bool setModStatus(const QModelIndexList &indexes, ModStatusAction action);
void startWatching();
void stopWatching();
virtual bool isValid();
bool isValid();
QDir dir()
{
@@ -106,16 +119,31 @@ public:
return mods;
}
public slots:
void disableInteraction(bool disabled);
private
slots:
void directoryChanged(QString path);
void finishUpdate();
void finishModParse(int token);
signals:
void changed();
void updateFinished();
private:
void resolveMod(Mod& m);
bool setModStatus(int index, ModStatusAction action);
protected:
QFileSystemWatcher *m_watcher;
bool is_watching = false;
ModFolderLoadTask::ResultPtr m_update;
bool scheduled_update = false;
bool interaction_disabled = false;
QDir m_dir;
QMap<QString, int> modsIndex;
QMap<int, LocalModParseTask::ResultPtr> activeTickets;
int nextResolutionTicket = 0;
QList<Mod> mods;
};

View File

@@ -4,9 +4,9 @@
#include "TestUtil.h"
#include "FileSystem.h"
#include "minecraft/SimpleModList.h"
#include "minecraft/mod/ModFolderModel.h"
class SimpleModListTest : public QObject
class ModFolderModelTest : public QObject
{
Q_OBJECT
@@ -32,7 +32,7 @@ slots:
{
QString folder = source;
QTemporaryDir tempDir;
SimpleModList m(tempDir.path());
ModFolderModel m(tempDir.path());
m.installMod(folder);
verify(tempDir.path());
}
@@ -41,13 +41,13 @@ slots:
{
QString folder = source + '/';
QTemporaryDir tempDir;
SimpleModList m(tempDir.path());
ModFolderModel m(tempDir.path());
m.installMod(folder);
verify(tempDir.path());
}
}
};
QTEST_GUILESS_MAIN(SimpleModListTest)
QTEST_GUILESS_MAIN(ModFolderModelTest)
#include "SimpleModList_test.moc"
#include "ModFolderModel_test.moc"

View File

@@ -45,6 +45,7 @@ void LibrariesTask::executeTask()
QList<LibraryPtr> libArtifactPool;
libArtifactPool.append(profile->getLibraries());
libArtifactPool.append(profile->getNativeLibraries());
libArtifactPool.append(profile->getMavenFiles());
libArtifactPool.append(profile->getMainJar());
processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath());

View File

@@ -1,175 +0,0 @@
#include "UrlResolvingTask.h"
#include <QtXml>
#include <Json.h>
namespace {
const char * metabase = "https://cursemeta.dries007.net";
}
Flame::UrlResolvingTask::UrlResolvingTask(const QString& toProcess)
: m_url(toProcess)
{
}
void Flame::UrlResolvingTask::executeTask()
{
resolveUrl();
}
void Flame::UrlResolvingTask::resolveUrl()
{
setStatus(tr("Resolving URL..."));
setProgress(0, 1);
QUrl actualUrl(m_url);
if(actualUrl.host() != "www.curseforge.com") {
emitFailed(tr("Not a Twitch URL."));
return;
}
m_dljob.reset(new NetJob("URL resolver"));
bool weAreDigging = false;
needle = QString();
if(m_url.startsWith("https://")) {
if(m_url.endsWith("?client=y")) {
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download?client=y
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download/2697088?client=y
m_url.chop(9);
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download/2697088
}
if(m_url.endsWith("/download")) {
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download -> need to dig inside html...
weAreDigging = true;
needle = m_url;
needle.replace("https://", "twitch://");
needle.replace("/download", "/download-client/");
m_url.append("?client=y");
} else if (m_url.contains("/download/")) {
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download/2697088
m_url.replace("/download/", "/download-client/");
}
}
else if(m_url.startsWith("twitch://")) {
// twitch://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download-client/2697088
m_url.replace(0, 9, "https://");
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download-client/2697088
}
auto dl = Net::Download::makeByteArray(QUrl(m_url), &results);
m_dljob->addNetAction(dl);
if(weAreDigging) {
connect(m_dljob.get(), &NetJob::finished, this, &Flame::UrlResolvingTask::processHTML);
} else {
connect(m_dljob.get(), &NetJob::finished, this, &Flame::UrlResolvingTask::processCCIP);
}
m_dljob->start();
}
void Flame::UrlResolvingTask::processHTML()
{
QString htmlDoc = QString::fromUtf8(results);
auto index = htmlDoc.indexOf(needle);
if(index < 0) {
emitFailed(tr("Couldn't find the needle in the haystack..."));
return;
}
auto indexStart = index;
int indexEnd = -1;
while((index + 1) < htmlDoc.size() && htmlDoc[index] != '"') {
index ++;
if(htmlDoc[index] == '"') {
indexEnd = index;
break;
}
}
if(indexEnd > 0) {
QString found = htmlDoc.mid(indexStart, indexEnd - indexStart);
qDebug() << "Found needle: " << found;
// twitch://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download-client/2697088
m_url = found;
resolveUrl();
return;
}
emitFailed(tr("Couldn't find the end of the needle in the haystack..."));
return;
}
void Flame::UrlResolvingTask::processCCIP()
{
QDomDocument doc;
if (!doc.setContent(results)) {
qDebug() << results;
emitFailed(tr("Resolving failed."));
return;
}
auto packageNode = doc.namedItem("package");
if(!packageNode.isElement()) {
emitFailed(tr("Resolving failed: missing package root element."));
return;
}
auto projectNode = packageNode.namedItem("project");
if(!projectNode.isElement()) {
emitFailed(tr("Resolving failed: missing project element."));
return;
}
auto attribs = projectNode.attributes();
auto projectIdNode = attribs.namedItem("id");
if(!projectIdNode.isAttr()) {
emitFailed(tr("Resolving failed: missing id attribute."));
return;
}
auto fileIdNode = attribs.namedItem("file");
if(!fileIdNode.isAttr()) {
emitFailed(tr("Resolving failed: missing file attribute."));
return;
}
auto projectId = projectIdNode.nodeValue();
auto fileId = fileIdNode.nodeValue();
bool success = true;
m_result.projectId = projectId.toInt(&success);
if(!success) {
emitFailed(tr("Failed to resove projectId as a number."));
return;
}
m_result.fileId = fileId.toInt(&success);
if(!success) {
emitFailed(tr("Failed to resove fileId as a number."));
return;
}
qDebug() << "Resolved" << m_url << "as" << m_result.projectId << "/" << m_result.fileId;
resolveIDs();
}
void Flame::UrlResolvingTask::resolveIDs()
{
setStatus(tr("Resolving mod IDs..."));
m_dljob.reset(new NetJob("Mod id resolver"));
auto projectIdStr = QString::number(m_result.projectId);
auto fileIdStr = QString::number(m_result.fileId);
QString metaurl = QString("%1/%2/%3.json").arg(metabase, projectIdStr, fileIdStr);
auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results);
m_dljob->addNetAction(dl);
connect(m_dljob.get(), &NetJob::finished, this, &Flame::UrlResolvingTask::processCursemeta);
m_dljob->start();
}
void Flame::UrlResolvingTask::processCursemeta()
{
try {
if(m_result.parseFromBytes(results)) {
emitSucceeded();
qDebug() << results;
return;
}
} catch (const JSONValidationError &e) {
qCritical() << "Resolving of" << m_result.projectId << m_result.fileId << "failed because of a parsing error:";
qCritical() << e.cause();
qCritical() << "JSON:";
qCritical() << results;
}
emitFailed(tr("Failed to resolve the modpack file."));
}

View File

@@ -1,43 +0,0 @@
#pragma once
#include "tasks/Task.h"
#include "net/NetJob.h"
#include "PackManifest.h"
#include "multimc_logic_export.h"
namespace Flame
{
class MULTIMC_LOGIC_EXPORT UrlResolvingTask : public Task
{
Q_OBJECT
public:
explicit UrlResolvingTask(const QString &toProcess);
virtual ~UrlResolvingTask() {};
const Flame::File &getResults() const
{
return m_result;
}
protected:
virtual void executeTask() override;
protected slots:
void processCCIP();
void processHTML();
void processCursemeta();
private:
void resolveUrl();
void resolveIDs();
private: /* data */
QString m_url;
QString needle;
Flame::File m_result;
QByteArray results;
NetJobPtr m_dljob;
};
}

View File

@@ -1,34 +1,36 @@
#include "FtbPackFetchTask.h"
#include <QDomDocument>
#include "FtbPrivatePackManager.h"
#include "PackFetchTask.h"
#include "PrivatePackManager.h"
#include <QDomDocument>
#include "net/URLConstants.h"
void FtbPackFetchTask::fetch()
namespace LegacyFTB {
void PackFetchTask::fetch()
{
publicPacks.clear();
thirdPartyPacks.clear();
NetJob *netJob = new NetJob("FtbModpackFetch");
NetJob *netJob = new NetJob("LegacyFTB::ModpackFetch");
QUrl publicPacksUrl = QUrl(URLConstants::FTB_CDN_BASE_URL + "static/modpacks.xml");
QUrl publicPacksUrl = QUrl(URLConstants::LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml");
qDebug() << "Downloading public version info from" << publicPacksUrl.toString();
netJob->addNetAction(Net::Download::makeByteArray(publicPacksUrl, &publicModpacksXmlFileData));
QUrl thirdPartyUrl = QUrl(URLConstants::FTB_CDN_BASE_URL + "static/thirdparty.xml");
QUrl thirdPartyUrl = QUrl(URLConstants::LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml");
qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString();
netJob->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, &thirdPartyModpacksXmlFileData));
QObject::connect(netJob, &NetJob::succeeded, this, &FtbPackFetchTask::fileDownloadFinished);
QObject::connect(netJob, &NetJob::failed, this, &FtbPackFetchTask::fileDownloadFailed);
QObject::connect(netJob, &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished);
QObject::connect(netJob, &NetJob::failed, this, &PackFetchTask::fileDownloadFailed);
jobPtr.reset(netJob);
netJob->start();
}
void FtbPackFetchTask::fetchPrivate(const QStringList & toFetch)
void PackFetchTask::fetchPrivate(const QStringList & toFetch)
{
QString privatePackBaseUrl = URLConstants::FTB_CDN_BASE_URL + "static/%1.xml";
QString privatePackBaseUrl = URLConstants::LEGACY_FTB_CDN_BASE_URL + "static/%1.xml";
for (auto &packCode: toFetch)
{
@@ -38,9 +40,9 @@ void FtbPackFetchTask::fetchPrivate(const QStringList & toFetch)
QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode]
{
FtbModpackList packs;
parseAndAddPacks(*data, FtbPackType::Private, packs);
foreach(FtbModpack currentPack, packs)
ModpackList packs;
parseAndAddPacks(*data, PackType::Private, packs);
foreach(Modpack currentPack, packs)
{
currentPack.packCode = packCode;
emit privateFileDownloadFinished(currentPack);
@@ -65,18 +67,18 @@ void FtbPackFetchTask::fetchPrivate(const QStringList & toFetch)
}
}
void FtbPackFetchTask::fileDownloadFinished()
void PackFetchTask::fileDownloadFinished()
{
jobPtr.reset();
QStringList failedLists;
if(!parseAndAddPacks(publicModpacksXmlFileData, FtbPackType::Public, publicPacks))
if(!parseAndAddPacks(publicModpacksXmlFileData, PackType::Public, publicPacks))
{
failedLists.append(tr("Public Packs"));
}
if(!parseAndAddPacks(thirdPartyModpacksXmlFileData, FtbPackType::ThirdParty, thirdPartyPacks))
if(!parseAndAddPacks(thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks))
{
failedLists.append(tr("Third Party Packs"));
}
@@ -91,7 +93,7 @@ void FtbPackFetchTask::fileDownloadFinished()
}
}
bool FtbPackFetchTask::parseAndAddPacks(QByteArray &data, FtbPackType packType, FtbModpackList &list)
bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list)
{
QDomDocument doc;
@@ -112,7 +114,7 @@ bool FtbPackFetchTask::parseAndAddPacks(QByteArray &data, FtbPackType packType,
{
QDomElement element = nodes.at(i).toElement();
FtbModpack modpack;
Modpack modpack;
modpack.name = element.attribute("name");
modpack.currentVersion = element.attribute("version");
modpack.mcVersion = element.attribute("mcVersion");
@@ -161,8 +163,10 @@ bool FtbPackFetchTask::parseAndAddPacks(QByteArray &data, FtbPackType packType,
return true;
}
void FtbPackFetchTask::fileDownloadFailed(QString reason)
void PackFetchTask::fileDownloadFailed(QString reason)
{
qWarning() << "Fetching FtbPacks failed:" << reason;
qWarning() << "Fetching FTBPacks failed:" << reason;
emit failed(reason);
}
}

View File

@@ -6,13 +6,15 @@
#include <QObject>
#include "PackHelpers.h"
class MULTIMC_LOGIC_EXPORT FtbPackFetchTask : public QObject {
namespace LegacyFTB {
class MULTIMC_LOGIC_EXPORT PackFetchTask : public QObject {
Q_OBJECT
public:
FtbPackFetchTask() = default;
virtual ~FtbPackFetchTask() = default;
PackFetchTask() = default;
virtual ~PackFetchTask() = default;
void fetch();
void fetchPrivate(const QStringList &toFetch);
@@ -23,18 +25,20 @@ private:
QByteArray publicModpacksXmlFileData;
QByteArray thirdPartyModpacksXmlFileData;
bool parseAndAddPacks(QByteArray &data, FtbPackType packType, FtbModpackList &list);
FtbModpackList publicPacks;
FtbModpackList thirdPartyPacks;
bool parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list);
ModpackList publicPacks;
ModpackList thirdPartyPacks;
protected slots:
void fileDownloadFinished();
void fileDownloadFailed(QString reason);
signals:
void finished(FtbModpackList publicPacks, FtbModpackList thirdPartyPacks);
void finished(ModpackList publicPacks, ModpackList thirdPartyPacks);
void failed(QString reason);
void privateFileDownloadFinished(FtbModpack modpack);
void privateFileDownloadFinished(Modpack modpack);
void privateFileDownloadFailed(QString reason, QString packCode);
};
}

View File

@@ -5,15 +5,17 @@
#include <QStringList>
#include <QMetaType>
namespace LegacyFTB {
//Header for structs etc...
enum class FtbPackType
enum class PackType
{
Public,
ThirdParty,
Private
};
struct FtbModpack
struct Modpack
{
QString name;
QString description;
@@ -31,11 +33,13 @@ struct FtbModpack
bool bugged = false;
bool broken = false;
FtbPackType type;
PackType type;
QString packCode;
};
//We need it for the proxy model
Q_DECLARE_METATYPE(FtbModpack)
typedef QList<Modpack> ModpackList;
typedef QList<FtbModpack> FtbModpackList;
}
//We need it for the proxy model
Q_DECLARE_METATYPE(LegacyFTB::Modpack)

View File

@@ -1,28 +1,32 @@
#include "FtbPackInstallTask.h"
#include "PackInstallTask.h"
#include "Env.h"
#include "MMCZip.h"
#include "QtConcurrent"
#include "BaseInstance.h"
#include "FileSystem.h"
#include "settings/INISettingsObject.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/ComponentList.h"
#include "minecraft/GradleSpecifier.h"
#include "net/URLConstants.h"
FtbPackInstallTask::FtbPackInstallTask(FtbModpack pack, QString version)
#include <QtConcurrent>
namespace LegacyFTB {
PackInstallTask::PackInstallTask(Modpack pack, QString version)
{
m_pack = pack;
m_version = version;
}
void FtbPackInstallTask::executeTask()
void PackInstallTask::executeTask()
{
downloadPack();
}
void FtbPackInstallTask::downloadPack()
void PackInstallTask::downloadPack()
{
setStatus(tr("Downloading zip for %1").arg(m_pack.name));
@@ -32,46 +36,46 @@ void FtbPackInstallTask::downloadPack()
entry->setStale(true);
QString url;
if(m_pack.type == FtbPackType::Private)
if(m_pack.type == PackType::Private)
{
url = QString(URLConstants::FTB_CDN_BASE_URL + "privatepacks/%1").arg(packoffset);
url = QString(URLConstants::LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(packoffset);
}
else
{
url = QString(URLConstants::FTB_CDN_BASE_URL + "modpacks/%1").arg(packoffset);
url = QString(URLConstants::LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(packoffset);
}
job->addNetAction(Net::Download::makeCached(url, entry));
archivePath = entry->getFullPath();
netJobContainer.reset(job);
connect(job, &NetJob::succeeded, this, &FtbPackInstallTask::onDownloadSucceeded);
connect(job, &NetJob::failed, this, &FtbPackInstallTask::onDownloadFailed);
connect(job, &NetJob::progress, this, &FtbPackInstallTask::onDownloadProgress);
connect(job, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
connect(job, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
connect(job, &NetJob::progress, this, &PackInstallTask::onDownloadProgress);
job->start();
progress(1, 4);
}
void FtbPackInstallTask::onDownloadSucceeded()
void PackInstallTask::onDownloadSucceeded()
{
abortable = false;
unzip();
}
void FtbPackInstallTask::onDownloadFailed(QString reason)
void PackInstallTask::onDownloadFailed(QString reason)
{
abortable = false;
emitFailed(reason);
}
void FtbPackInstallTask::onDownloadProgress(qint64 current, qint64 total)
void PackInstallTask::onDownloadProgress(qint64 current, qint64 total)
{
abortable = true;
progress(current, total * 4);
setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10));
}
void FtbPackInstallTask::unzip()
void PackInstallTask::unzip()
{
progress(2, 4);
setStatus(tr("Extracting modpack"));
@@ -85,22 +89,22 @@ void FtbPackInstallTask::unzip()
}
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &FtbPackInstallTask::onUnzipFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &FtbPackInstallTask::onUnzipCanceled);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled);
m_extractFutureWatcher.setFuture(m_extractFuture);
}
void FtbPackInstallTask::onUnzipFinished()
void PackInstallTask::onUnzipFinished()
{
install();
}
void FtbPackInstallTask::onUnzipCanceled()
void PackInstallTask::onUnzipCanceled()
{
emitAborted();
}
void FtbPackInstallTask::install()
void PackInstallTask::install()
{
progress(3, 4);
setStatus(tr("Installing modpack"));
@@ -197,7 +201,7 @@ void FtbPackInstallTask::install()
emitSucceeded();
}
bool FtbPackInstallTask::abort()
bool PackInstallTask::abort()
{
if(abortable)
{
@@ -205,3 +209,5 @@ bool FtbPackInstallTask::abort()
}
return false;
}
}

View File

@@ -6,15 +6,17 @@
#include "meta/Index.h"
#include "meta/Version.h"
#include "meta/VersionList.h"
#include "modplatform/ftb/PackHelpers.h"
#include "PackHelpers.h"
class MULTIMC_LOGIC_EXPORT FtbPackInstallTask : public InstanceTask
namespace LegacyFTB {
class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask
{
Q_OBJECT
public:
explicit FtbPackInstallTask(FtbModpack pack, QString version);
virtual ~FtbPackInstallTask(){}
explicit PackInstallTask(Modpack pack, QString version);
virtual ~PackInstallTask(){}
bool abort() override;
@@ -43,6 +45,8 @@ private: /* data */
NetJobPtr netJobContainer;
QString archivePath;
FtbModpack m_pack;
Modpack m_pack;
QString m_version;
};
}

View File

@@ -1,10 +1,12 @@
#include "FtbPrivatePackManager.h"
#include "PrivatePackManager.h"
#include <QDebug>
#include "FileSystem.h"
void FtbPrivatePackManager::load()
namespace LegacyFTB {
void PrivatePackManager::load()
{
try
{
@@ -18,7 +20,7 @@ void FtbPrivatePackManager::load()
}
}
void FtbPrivatePackManager::save() const
void PrivatePackManager::save() const
{
if(!dirty)
{
@@ -35,3 +37,5 @@ void FtbPrivatePackManager::save() const
qWarning() << "Failed to write third party FTB pack codes to" << m_filename;
}
}
}

View File

@@ -5,10 +5,12 @@
#include <QFile>
#include "multimc_logic_export.h"
class MULTIMC_LOGIC_EXPORT FtbPrivatePackManager
namespace LegacyFTB {
class MULTIMC_LOGIC_EXPORT PrivatePackManager
{
public:
~FtbPrivatePackManager()
~PrivatePackManager()
{
save();
}
@@ -38,3 +40,5 @@ private:
QString m_filename = "private_packs.txt";
mutable bool dirty = false;
};
}

View File

@@ -29,7 +29,8 @@ const QString IMGUR_BASE_URL("https://api.imgur.com/3/");
const QString FMLLIBS_OUR_BASE_URL("https://files.multimc.org/fmllibs/");
const QString FMLLIBS_FORGE_BASE_URL("https://files.minecraftforge.net/fmllibs/");
const QString TRANSLATIONS_BASE_URL("https://files.multimc.org/translations/");
const QString FTB_CDN_BASE_URL("https://ftb.forgecdn.net/FTB2/");
const QString LEGACY_FTB_CDN_BASE_URL("https://dist.creeper.host/FTB2/");
QString getJarPath(QString version);
QString getLegacyJarUrl(QString version);

View File

@@ -33,7 +33,12 @@ struct Language
Language(const QString & _key)
{
key = _key;
locale = QLocale(key);
if(key == "pt") {
locale = QLocale("pt_PT");
}
else {
locale = QLocale(key);
}
updated = (key == defaultLangCode);
}

View File

@@ -185,25 +185,6 @@ slots:
qDebug() << expectedOperations;
QCOMPARE(operations, expectedOperations);
}
void test_OSXPathFixup()
{
QString path, pathOrig;
bool result;
// Proper OSX path
pathOrig = path = "MultiMC.app/Foo/Bar/Baz";
qDebug() << "Proper OSX path: " << path;
result = fixPathForOSX(path);
QCOMPARE(path, QString("Foo/Bar/Baz"));
QCOMPARE(result, true);
// Bad OSX path
pathOrig = path = "translations/klingon.lol";
qDebug() << "Bad OSX path: " << path;
result = fixPathForOSX(path);
QCOMPARE(path, pathOrig);
QCOMPARE(result, false);
}
};
extern "C"

View File

@@ -33,13 +33,7 @@ bool parseVersionInfo(const QByteArray &data, VersionFileList &list, QString &er
QJsonObject fileObj = fileValue.toObject();
QString file_path = fileObj.value("Path").toString();
#ifdef Q_OS_MAC
// On OSX, the paths for the updater need to be fixed.
// basically, anything that isn't in the .app folder is ignored.
// everything else is changed so the code that processes the files actually finds
// them and puts the replacements in the right spots.
fixPathForOSX(file_path);
#endif
VersionFileEntry file{file_path, fileObj.value("Perms").toVariant().toInt(),
FileSourceList(), fileObj.value("MD5").toString(), };
qDebug() << "File" << file.path << "with perms" << file.mode;
@@ -201,19 +195,4 @@ bool processFileLists
}
return true;
}
bool fixPathForOSX(QString &path)
{
if (path.startsWith("MultiMC.app/"))
{
// remove the prefix and add a new, more appropriate one.
path.remove(0, 12);
return true;
}
else
{
qCritical() << "Update path not within .app: " << path;
return false;
}
}
}

View File

@@ -123,14 +123,5 @@ bool MULTIMC_LOGIC_EXPORT processFileLists
OperationList &ops
);
/*!
* This fixes destination paths for OSX - removes 'MultiMC.app' prefix
* The updater runs in MultiMC.app/Contents/MacOs by default
* The destination paths are such as this: MultiMC.app/blah/blah
*
* @return false if the path couldn't be fixed (is invalid)
*/
bool MULTIMC_LOGIC_EXPORT fixPathForOSX(QString &path);
}
Q_DECLARE_METATYPE(GoUpdate::Status)

View File

@@ -125,20 +125,14 @@ SET(MULTIMC_SOURCES
pages/global/ProxyPage.h
pages/global/PasteEEPage.cpp
pages/global/PasteEEPage.h
pages/global/PackagesPage.cpp
pages/global/PackagesPage.h
# GUI - platform pages
pages/modplatform/VanillaPage.cpp
pages/modplatform/VanillaPage.h
pages/modplatform/FTBPage.cpp
pages/modplatform/FTBPage.h
pages/modplatform/FtbListModel.h
pages/modplatform/FtbListModel.cpp
pages/modplatform/TwitchPage.cpp
pages/modplatform/TwitchPage.h
pages/modplatform/TechnicPage.cpp
pages/modplatform/TechnicPage.h
pages/modplatform/legacy_ftb/Page.cpp
pages/modplatform/legacy_ftb/Page.h
pages/modplatform/legacy_ftb/ListModel.h
pages/modplatform/legacy_ftb/ListModel.cpp
pages/modplatform/ImportPage.cpp
pages/modplatform/ImportPage.h
@@ -159,8 +153,6 @@ SET(MULTIMC_SOURCES
dialogs/IconPickerDialog.h
dialogs/LoginDialog.cpp
dialogs/LoginDialog.h
dialogs/ModEditDialogCommon.cpp
dialogs/ModEditDialogCommon.h
dialogs/NewComponentDialog.cpp
dialogs/NewComponentDialog.h
dialogs/NewInstanceDialog.cpp
@@ -184,6 +176,8 @@ SET(MULTIMC_SOURCES
widgets/Common.h
widgets/CustomCommands.cpp
widgets/CustomCommands.h
widgets/DropLabel.cpp
widgets/DropLabel.h
widgets/FocusLineEdit.cpp
widgets/FocusLineEdit.h
widgets/IconLabel.cpp
@@ -213,10 +207,15 @@ SET(MULTIMC_SOURCES
widgets/VersionSelectWidget.h
widgets/ProgressWidget.h
widgets/ProgressWidget.cpp
widgets/WideBar.h
widgets/WideBar.cpp
# GUI - instance group view
groupview/GroupedProxyModel.cpp
groupview/GroupedProxyModel.h
groupview/AccessibleGroupView.cpp
groupview/AccessibleGroupView.h
groupview/AccessibleGroupView_p.h
groupview/GroupView.cpp
groupview/GroupView.h
groupview/InstanceDelegate.cpp
@@ -248,13 +247,10 @@ SET(MULTIMC_UIS
pages/global/MultiMCPage.ui
pages/global/ProxyPage.ui
pages/global/PasteEEPage.ui
pages/global/PackagesPage.ui
# Platform pages
pages/modplatform/VanillaPage.ui
pages/modplatform/FTBPage.ui
pages/modplatform/TwitchPage.ui
pages/modplatform/TechnicPage.ui
pages/modplatform/legacy_ftb/Page.ui
pages/modplatform/ImportPage.ui
# Dialogs
@@ -278,7 +274,6 @@ SET(MULTIMC_UIS
)
set(MULTIMC_QRCS
resources/assets/assets.qrc
resources/backgrounds/backgrounds.qrc
resources/multimc/multimc.qrc
resources/pe_dark/pe_dark.qrc

View File

@@ -45,7 +45,7 @@ public:
values.append(new ResourcePackPage(onesix.get()));
values.append(new TexturePackPage(onesix.get()));
values.append(new NotesPage(onesix.get()));
values.append(new WorldListPage(onesix.get(), onesix->worldList(), "worlds", "worlds", tr("Worlds"), "Worlds"));
values.append(new WorldListPage(onesix.get(), onesix->worldList()));
values.append(new ServersPage(onesix.get()));
// values.append(new GameOptionsPage(onesix.get()));
values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots")));
@@ -56,7 +56,7 @@ public:
{
values.append(new LegacyUpgradePage(legacy));
values.append(new NotesPage(legacy.get()));
values.append(new WorldListPage(legacy.get(), legacy->worldList(), "worlds", "worlds", tr("Worlds"), "Worlds"));
values.append(new WorldListPage(legacy.get(), legacy->worldList()));
values.append(new ScreenshotsPage(FS::PathCombine(legacy->gameRoot(), "screenshots")));
}
auto logMatcher = inst->getLogFileMatcher();

View File

@@ -50,6 +50,7 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget *parent)
m_container = new PageContainer(provider.get(), "console", this);
m_container->setParentContainer(this);
setCentralWidget(m_container);
setContentsMargins(0, 0, 0, 0);
}
// Add custom buttons to the page container layout.
@@ -149,10 +150,13 @@ void InstanceWindow::on_InstanceLaunchTask_changed(shared_qobject_ptr<LaunchTask
m_proc = proc;
}
void InstanceWindow::on_RunningState_changed(bool)
void InstanceWindow::on_RunningState_changed(bool running)
{
updateLaunchButtons();
m_container->refreshContainer();
if(running) {
selectPage("log");
}
}
void InstanceWindow::on_closeButton_clicked()

View File

@@ -24,7 +24,7 @@ void LaunchController::executeTask()
{
if (!m_instance)
{
emitFailed(tr("No instance specified"));
emitFailed(tr("No instance specified!"));
return;
}
@@ -74,7 +74,7 @@ void LaunchController::login()
// if no account is selected, we bail
if (!account.get())
{
emitFailed(tr("No account selected for launch"));
emitFailed(tr("No account selected for launch."));
return;
}
@@ -83,8 +83,7 @@ void LaunchController::login()
// we loop until the user succeeds in logging in or gives up
bool tryagain = true;
// the failure. the default failure.
const QString needLoginAgain = tr("Your account is currently not logged in. Please enter "
"your password to log in again.");
const QString needLoginAgain = tr("Your account is currently not logged in. Please enter your password to log in again. <br /> <br /> This could be caused by a password change.");
QString failReason = needLoginAgain;
while (tryagain)
@@ -193,7 +192,7 @@ void LaunchController::launchInstance()
if(!m_instance->reloadSettings())
{
QMessageBox::critical(m_parentWidget, tr("Error"), tr("Couldn't load the instance profile."));
QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile."));
emitFailed(tr("Couldn't load the instance profile."));
return;
}
@@ -233,8 +232,8 @@ void LaunchController::readyForLaunch()
if (!m_profiler->check(&error))
{
m_launcher->abort();
QMessageBox::critical(m_parentWidget, tr("Error"), tr("Couldn't start profiler: %1").arg(error));
emitFailed("Profiler startup failed");
QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't start profiler: %1").arg(error));
emitFailed("Profiler startup failed!");
return;
}
BaseProfiler *profilerInstance = m_profiler->createProfiler(m_launcher->instance(), this);
@@ -245,7 +244,7 @@ void LaunchController::readyForLaunch()
msg.setText(tr("The game launch is delayed until you press the "
"button. This is the right time to setup the profiler, as the "
"profiler server is running now.\n\n%1").arg(message));
msg.setWindowTitle(tr("Waiting"));
msg.setWindowTitle(tr("Waiting."));
msg.setIcon(QMessageBox::Information);
msg.addButton(tr("Launch"), QMessageBox::AcceptRole);
msg.setModal(true);
@@ -262,7 +261,7 @@ void LaunchController::readyForLaunch()
msg.setModal(true);
msg.exec();
m_launcher->abort();
emitFailed("Profiler startup failed");
emitFailed("Profiler startup failed!");
});
profilerInstance->beginProfiling(m_launcher);
}

View File

@@ -288,6 +288,7 @@ public:
foldersMenuButton->setPopupMode(QToolButton::InstantPopup);
foldersMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
foldersMenuButton->setIcon(MMC->getThemedIcon("viewfolder"));
foldersMenuButton->setFocusPolicy(Qt::NoFocus);
all_toolbuttons.append(&foldersMenuButton);
QWidgetAction* foldersButtonAction = new QWidgetAction(MainWindow);
foldersButtonAction->setDefaultWidget(foldersMenuButton);
@@ -345,6 +346,7 @@ public:
helpMenuButton->setPopupMode(QToolButton::InstantPopup);
helpMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
helpMenuButton->setIcon(MMC->getThemedIcon("help"));
helpMenuButton->setFocusPolicy(Qt::NoFocus);
all_toolbuttons.append(&helpMenuButton);
QWidgetAction* helpButtonAction = new QWidgetAction(MainWindow);
helpButtonAction->setDefaultWidget(helpMenuButton);
@@ -573,9 +575,10 @@ public:
{
MainWindow->setObjectName(QStringLiteral("MainWindow"));
}
MainWindow->resize(694, 563);
MainWindow->resize(800, 600);
MainWindow->setWindowIcon(MMC->getThemedIcon("logo"));
MainWindow->setWindowTitle("MultiMC 5");
MainWindow->setAccessibleName("MultiMC");
createMainToolbar(MainWindow);
@@ -583,7 +586,6 @@ public:
centralWidget->setObjectName(QStringLiteral("centralWidget"));
horizontalLayout = new QHBoxLayout(centralWidget);
horizontalLayout->setSpacing(0);
horizontalLayout->setContentsMargins(11, 11, 11, 11);
horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
horizontalLayout->setSizeConstraint(QLayout::SetDefaultConstraint);
horizontalLayout->setContentsMargins(0, 0, 0, 0);
@@ -652,6 +654,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
newsLabel->setIcon(MMC->getThemedIcon("news"));
newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
newsLabel->setFocusPolicy(Qt::NoFocus);
ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel);
QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked);
QObject::connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel);
@@ -680,6 +683,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
connect(proxymodel, &InstanceProxyModel::dataChanged, this, &MainWindow::instanceDataChanged);
view->setModel(proxymodel);
view->setSourceOfGroupCollapseStatus([](const QString & groupName)->bool {
return MMC->instances()->isGroupCollapsed(groupName);
});
connect(view, &GroupView::groupStateChanged, MMC->instances().get(), &InstanceList::on_GroupStateChanged);
ui->horizontalLayout->addWidget(view);
}
// The cat background
@@ -828,6 +835,13 @@ MainWindow::~MainWindow()
{
}
QMenu * MainWindow::createPopupMenu()
{
QMenu* filteredMenu = QMainWindow::createPopupMenu();
filteredMenu->removeAction( ui->mainToolBar->toggleViewAction() );
return filteredMenu;
}
void MainWindow::konamiTriggered()
{
// ENV.enableFeature("NewModsPage");
@@ -1344,7 +1358,7 @@ void MainWindow::on_actionCopyInstance_triggered()
if (!copyInstDlg.exec())
return;
auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.shouldCopySaves());
auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.shouldCopySaves(), copyInstDlg.shouldKeepPlaytime());
copyTask->setName(copyInstDlg.instName());
copyTask->setGroup(copyInstDlg.instGroup());
copyTask->setIcon(copyInstDlg.iconKey());
@@ -1654,13 +1668,6 @@ void MainWindow::on_actionAbout_triggered()
dialog.exec();
}
void MainWindow::on_mainToolBar_visibilityChanged(bool)
{
// Don't allow hiding the main toolbar.
// This is the only way I could find to prevent it... :/
ui->mainToolBar->setVisible(true);
}
void MainWindow::on_actionDeleteInstance_triggered()
{
if (!m_selectedInstance)
@@ -1894,7 +1901,7 @@ void MainWindow::checkInstancePathForProblems()
warning.setDefaultButton(QMessageBox::Ok);
warning.exec();
}
else if (pathfoldername.contains(QDir::tempPath()))
else if (pathfoldername.startsWith(QDir::tempPath()) || pathfoldername.contains("/TempState/"))
{
QMessageBox warning(this);
warning.setText(tr("Your instance folder is in a temporary folder: \'%1\'!").arg(QDir::tempPath()));

View File

@@ -57,9 +57,14 @@ public:
void checkInstancePathForProblems();
void updatesAllowedChanged(bool allowed);
void droppedURLs(QList<QUrl> urls);
signals:
void isClosing();
protected:
QMenu * createPopupMenu() override;
private slots:
void onCatToggled(bool);
@@ -109,8 +114,6 @@ private slots:
void newsButtonClicked();
void on_mainToolBar_visibilityChanged(bool);
void on_actionLaunchInstance_triggered();
void on_actionLaunchInstanceOffline_triggered();
@@ -179,8 +182,6 @@ private slots:
*/
void downloadUpdates(GoUpdate::Status status);
void droppedURLs(QList<QUrl> urls);
void konamiTriggered();
void globalSettingsClosed();

View File

@@ -2,6 +2,10 @@
#include "BuildConfig.h"
#include "MainWindow.h"
#include "InstanceWindow.h"
#include "groupview/AccessibleGroupView.h"
#include <QAccessible>
#include "pages/BasePageProvider.h"
#include "pages/global/MultiMCPage.h"
#include "pages/global/MinecraftPage.h"
@@ -11,7 +15,6 @@
#include "pages/global/ExternalToolsPage.h"
#include "pages/global/AccountListPage.h"
#include "pages/global/PasteEEPage.h"
#include "pages/global/PackagesPage.h"
#include "pages/global/CustomCommandsPage.h"
#include "themes/ITheme.h"
@@ -31,6 +34,7 @@
#include <QNetworkAccessManager>
#include <QTranslator>
#include <QLibraryInfo>
#include <QList>
#include <QStringList>
#include <QDebug>
#include <QStyleFactory>
@@ -153,23 +157,27 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
// --help
parser.addSwitch("help");
parser.addShortOpt("help", 'h');
parser.addDocumentation("help", "display this help and exit.");
parser.addDocumentation("help", "Display this help and exit.");
// --version
parser.addSwitch("version");
parser.addShortOpt("version", 'V');
parser.addDocumentation("version", "display program version and exit.");
parser.addDocumentation("version", "Display program version and exit.");
// --dir
parser.addOption("dir");
parser.addShortOpt("dir", 'd');
parser.addDocumentation("dir", "use the supplied folder as MultiMC root instead of "
parser.addDocumentation("dir", "Use the supplied folder as MultiMC root instead of "
"the binary location (use '.' for current)");
// --launch
parser.addOption("launch");
parser.addShortOpt("launch", 'l');
parser.addDocumentation("launch", "launch the specified instance (by instance ID)");
parser.addDocumentation("launch", "Launch the specified instance (by instance ID)");
// --alive
parser.addSwitch("alive");
parser.addDocumentation("alive", "write a small '" + liveCheckFile + "' file after MultiMC starts");
parser.addDocumentation("alive", "Write a small '" + liveCheckFile + "' file after MultiMC starts");
// --import
parser.addOption("import");
parser.addShortOpt("import", 'I');
parser.addDocumentation("import", "Import instance from specified zip (local path or URL)");
// parse the arguments
try
@@ -204,6 +212,7 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
}
m_instanceIdToLaunch = args["launch"].toString();
m_liveCheck = args["alive"].toBool();
m_zipToImport = args["import"].toUrl();
QString origcwdPath = QDir::currentPath();
QString binPath = applicationDirPath();
@@ -275,13 +284,20 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
connect(m_peerInstance, &LocalPeer::messageReceived, this, &MultiMC::messageReceived);
if(m_peerInstance->isClient())
{
int timeout = 2000;
if(m_instanceIdToLaunch.isEmpty())
{
m_peerInstance->sendMessage("activate", 2000);
m_peerInstance->sendMessage("activate", timeout);
if(!m_zipToImport.isEmpty())
{
m_peerInstance->sendMessage("import " + m_zipToImport.toString(), timeout);
}
}
else
{
m_peerInstance->sendMessage(m_instanceIdToLaunch, 2000);
m_peerInstance->sendMessage("launch " + m_instanceIdToLaunch, timeout);
}
m_status = MultiMC::Succeeded;
return;
@@ -378,7 +394,7 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
auto payload = appID.toString().toUtf8();
if(check.write(payload) != payload.size())
{
qWarning() << "Could not write into" << liveCheckFile;
qWarning() << "Could not write into" << liveCheckFile << "!";
check.remove();
break;
}
@@ -488,10 +504,6 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("InstSortMode", "Name");
m_settings->registerSetting("SelectedInstance", QString());
// Offline mode stuff
m_settings->registerSetting("OfflineModeNameMode", "UseAccountName");
m_settings->registerSetting("OfflineModeName", "Player");
// Window state and geometry
m_settings->registerSetting("MainWindowState", "");
m_settings->registerSetting("MainWindowGeometry", "");
@@ -527,7 +539,6 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
m_globalSettingsProvider->addPage<LanguagePage>();
m_globalSettingsProvider->addPage<CustomCommandsPage>();
m_globalSettingsProvider->addPage<ProxyPage>();
// m_globalSettingsProvider->addPage<PackagesPage>();
m_globalSettingsProvider->addPage<ExternalToolsPage>();
m_globalSettingsProvider->addPage<AccountListPage>();
m_globalSettingsProvider->addPage<PasteEEPage>();
@@ -535,6 +546,10 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
qDebug() << "<> Settings loaded.";
}
#ifndef QT_NO_ACCESSIBILITY
QAccessible::installFactory(groupViewAccessibleFactory);
#endif /* !QT_NO_ACCESSIBILITY */
// load translations
{
m_translations.reset(new TranslationsModel("translations"));
@@ -598,12 +613,12 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
{
auto InstDirSetting = m_settings->getSetting("InstanceDir");
// instance path: check for problems with '!' in instance path and warn the user in the log
// and rememer that we have to show him a dialog when the gui starts (if it does so)
// and remember that we have to show him a dialog when the gui starts (if it does so)
QString instDir = InstDirSetting->get().toString();
qDebug() << "Instance path : " << instDir;
if (FS::checkProblemticPathJava(QDir(instDir)))
{
qWarning() << "Your instance path contains \'!\' and this is known to cause java problems";
qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!";
}
m_instances.reset(new InstanceList(m_settings, instDir, this));
connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged);
@@ -810,6 +825,11 @@ void MultiMC::performMainStartupAction()
showMainWindow(false);
qDebug() << "<> Main window shown.";
}
if(!m_zipToImport.isEmpty())
{
qDebug() << "<> Importing instance from zip:" << m_zipToImport;
m_mainWindow->droppedURLs({ m_zipToImport });
}
}
void MultiMC::showFatalErrorMessage(const QString& title, const QString& content)
@@ -846,18 +866,40 @@ void MultiMC::messageReceived(const QString& message)
qDebug() << "Received message" << message << "while still initializing. It will be ignored.";
return;
}
if(message == "activate")
QStringList args = message.split(' ');
QString command = args.takeFirst();
if(command == "activate")
{
showMainWindow();
}
else
else if(command == "import")
{
auto inst = instances()->getInstanceById(message);
if(args.isEmpty())
{
qWarning() << "Received" << command << "message without a zip path/URL.";
return;
}
m_mainWindow->droppedURLs({ QUrl(args.takeFirst()) });
}
else if(command == "launch")
{
if(args.isEmpty())
{
qWarning() << "Received" << command << "message without an instance ID.";
return;
}
auto inst = instances()->getInstanceById(args.takeFirst());
if(inst)
{
launch(inst, true, nullptr);
}
}
else
{
qWarning() << "Received invalid message" << message;
}
}
void MultiMC::analyticsSettingChanged(const Setting&, QVariant value)
@@ -945,7 +987,7 @@ bool MultiMC::launch(InstancePtr instance, bool online, BaseProfilerFactory *pro
{
if(m_updateRunning)
{
qDebug() << "Cannot launch instances while an update is running.";
qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed.";
}
else if(instance->canLaunch())
{
@@ -994,7 +1036,7 @@ bool MultiMC::kill(InstancePtr instance)
{
if (!instance->isRunning())
{
qWarning() << "Attempted to kill instance" << instance->id() << "which isn't running.";
qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running.";
return false;
}
auto & extras = m_instanceExtras[instance->id()];

View File

@@ -6,6 +6,7 @@
#include <QFlag>
#include <QIcon>
#include <QDateTime>
#include <QUrl>
#include <updater/GoUpdate.h>
#include <BaseInstance.h>
@@ -221,5 +222,6 @@ private:
public:
QString m_instanceIdToLaunch;
bool m_liveCheck = false;
QUrl m_zipToImport;
std::unique_ptr<QFile> logFile;
};

View File

@@ -23,56 +23,44 @@
#include "HoeDown.h"
namespace {
// Credits
// This is a hack, but I can't think of a better way to do this easily without screwing with QTextDocument...
static QString getCreditsHtml(QStringList patrons)
QString getCreditsHtml(QStringList patrons)
{
QString creditsHtml = QObject::tr(
"<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.0//EN' 'http://www.w3.org/TR/REC-html40/strict.dtd'>"
"<html>"
""
"<head>"
"<meta name='qrichtext' content='1' />"
"<style type='text/css'>"
"p { white-space: pre-wrap; margin-top:2px; margin-bottom:2px; }"
"</style>"
"</head>"
""
"<body style=' font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;'>"
""
"<h3>MultiMC Developers</h3>"
"<p>Andrew Okin &lt;<a href='mailto:forkk@forkk.net'>forkk@forkk.net</a>&gt;</p>"
"<p>Petr Mrázek &lt;<a href='mailto:peterix@gmail.com'>peterix@gmail.com</a>&gt;</p>"
"<p>Sky Welch &lt;<a href='mailto:multimc@bunnies.io'>multimc@bunnies.io</a>&gt;</p>"
"<p>Jan (02JanDal) &lt;<a href='mailto:02jandal@gmail.com'>02jandal@gmail.com</a>&gt;</p>"
"<p>RoboSky &lt;<a href='https://twitter.com/RoboSky_'>@RoboSky_</a>&gt;</p>"
""
"<h3>With thanks to</h3>"
"<p>Orochimarufan &lt;<a href='mailto:orochimarufan.x3@gmail.com'>orochimarufan.x3@gmail.com</a>&gt;</p>"
"<p>TakSuyu &lt;<a href='mailto:taksuyu@gmail.com'>taksuyu@gmail.com</a>&gt;</p>"
"<p>Kilobyte &lt;<a href='mailto:stiepen22@gmx.de'>stiepen22@gmx.de</a>&gt;</p>"
"<p>Rootbear75 &lt;<a href='https://twitter.com/rootbear75'>@rootbear75</a>&gt;</p>"
""
"<h3>Patrons</h3>"
"%1"
""
"</body>"
"</html>");
if (patrons.isEmpty())
return creditsHtml.arg(QObject::tr("<p>Loading...</p>"));
else
{
QString patronsStr;
QString patronsHeading = QObject::tr("Patrons", "About Credits");
QString output;
QTextStream stream(&output);
stream << "<center>\n";
// TODO: possibly retrieve from git history at build time?
stream << "<h3>" << QObject::tr("MultiMC Developers", "About Credits") << "</h3>\n";
stream << "<p>Andrew Okin &lt;<a href='mailto:forkk@forkk.net'>forkk@forkk.net</a>&gt;</p>\n";
stream << "<p>Petr Mrázek &lt;<a href='mailto:peterix@gmail.com'>peterix@gmail.com</a>&gt;</p>\n";
stream << "<p>Sky Welch &lt;<a href='mailto:multimc@bunnies.io'>multimc@bunnies.io</a>&gt;</p>\n";
stream << "<p>Jan (02JanDal) &lt;<a href='mailto:02jandal@gmail.com'>02jandal@gmail.com</a>&gt;</p>\n";
stream << "<p>RoboSky &lt;<a href='https://twitter.com/RoboSky_'>@RoboSky_</a>&gt;</p>\n";
stream << "<br />\n";
stream << "<h3>" << QObject::tr("With thanks to", "About Credits") << "</h3>\n";
stream << "<p>Orochimarufan &lt;<a href='mailto:orochimarufan.x3@gmail.com'>orochimarufan.x3@gmail.com</a>&gt;</p>\n";
stream << "<p>TakSuyu &lt;<a href='mailto:taksuyu@gmail.com'>taksuyu@gmail.com</a>&gt;</p>\n";
stream << "<p>Kilobyte &lt;<a href='mailto:stiepen22@gmx.de'>stiepen22@gmx.de</a>&gt;</p>\n";
stream << "<p>Rootbear75 &lt;<a href='https://twitter.com/rootbear75'>@rootbear75</a>&gt;</p>\n";
stream << "<p>Zeker Zhayard &lt;<a href='https://twitter.com/zeker_zhayard'>@Zeker_Zhayard</a>&gt;</p>\n";
stream << "<br />\n";
if(!patrons.isEmpty()) {
stream << "<h3>" << QObject::tr("Patrons", "About Credits") << "</h3>\n";
for (QString patron : patrons)
{
patronsStr.append(QString("<p>%1</p>").arg(patron));
stream << "<p>" << patron << "</p>\n";
}
return creditsHtml.arg(patronsStr);
}
stream << "</center>\n";
return output;
}
static QString getLicenseHtml()
QString getLicenseHtml()
{
HoeDown hoedown;
QFile dataFile(":/documents/COPYING.md");
@@ -81,6 +69,8 @@ static QString getLicenseHtml()
return output;
}
}
AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDialog)
{
ui->setupUi(this);
@@ -109,6 +99,15 @@ AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDia
else
ui->channelLabel->setVisible(false);
ui->redistributionText->setHtml(tr(
"<p>We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license.</p>\n"
"<p>Part of the reason for using the Apache license is we don't want people using the &quot;MultiMC&quot; name when redistributing the project. "
"This means people must take the time to go through the source code and remove all references to &quot;MultiMC&quot;, including but not limited to the project "
"icon and the title of windows, (no <b>MultiMC-fork</b> in the title).</p>\n"
"<p>The Apache license covers reasonable use for the name - a mention of the project's origins in the About dialog and the license is acceptable. "
"However, it should be abundantly clear that the project is a fork <b>without</b> implying that you have our blessing.</p>"
));
connect(ui->closeButton, SIGNAL(clicked()), SLOT(close()));
connect(ui->aboutQt, &QPushButton::clicked, &QApplication::aboutQt);

View File

@@ -212,28 +212,11 @@
<property name="readOnly">
<bool>true</bool>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:12pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="translationInfo">
<property name="text">
<string extracomment="Hey, Translator, feel free to put credit to you here">No Language file loaded.</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="licenseTab">
@@ -257,13 +240,6 @@ p, li { white-space: pre-wrap; }
<property name="readOnly">
<bool>true</bool>
</property>
<property name="html">
<string notr="true">&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'DejaVu Sans Mono'; font-size:12pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
@@ -277,18 +253,7 @@ p, li { white-space: pre-wrap; }
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QTextEdit" name="textEdit">
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:12pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Bitstream Vera Sans'; font-size:11pt;&quot;&gt;We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Bitstream Vera Sans'; font-size:11pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Bitstream Vera Sans'; font-size:11pt;&quot;&gt;Part of the reason for using the Apache license is we don't want people using the &amp;quot;MultiMC&amp;quot; name when redistributing the project. This means people must take the time to go through the source code and remove all references to &amp;quot;MultiMC&amp;quot;, including but not limited to the project icon and the title of windows, (no *MultiMC-fork* in the title).&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Bitstream Vera Sans'; font-size:11pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Bitstream Vera Sans'; font-size:11pt;&quot;&gt;The Apache license covers reasonable use for the name - a mention of the project's origins in the About dialog and the license is acceptable. However, it should be abundantly clear that the project is a fork &lt;/span&gt;&lt;span style=&quot; font-family:'Bitstream Vera Sans'; font-size:11pt; font-weight:600;&quot;&gt;without&lt;/span&gt;&lt;span style=&quot; font-family:'Bitstream Vera Sans'; font-size:11pt;&quot;&gt; implying that you have our blessing.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<widget class="QTextEdit" name="redistributionText">
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
@@ -337,14 +302,11 @@ p, li { white-space: pre-wrap; }
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>creditsText</tabstop>
<tabstop>translationInfo</tabstop>
<tabstop>licenseText</tabstop>
<tabstop>textEdit</tabstop>
<tabstop>redistributionText</tabstop>
<tabstop>aboutQt</tabstop>
<tabstop>closeButton</tabstop>
</tabstops>
<resources>
<include location="../../resources/multimc/multimc.qrc"/>
</resources>
<resources/>
<connections/>
</ui>

View File

@@ -53,6 +53,7 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent)
ui->groupBox->setCurrentIndex(index);
ui->groupBox->lineEdit()->setPlaceholderText(tr("No group"));
ui->copySavesCheckbox->setChecked(m_copySaves);
ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime);
}
CopyInstanceDialog::~CopyInstanceDialog()
@@ -123,3 +124,21 @@ void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state)
m_copySaves = true;
}
}
bool CopyInstanceDialog::shouldKeepPlaytime() const
{
return m_keepPlaytime;
}
void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state)
{
if(state == Qt::Unchecked)
{
m_keepPlaytime = false;
}
else if(state == Qt::Checked)
{
m_keepPlaytime = true;
}
}

View File

@@ -40,16 +40,19 @@ public:
QString instGroup() const;
QString iconKey() const;
bool shouldCopySaves() const;
bool shouldKeepPlaytime() const;
private
slots:
void on_iconButton_clicked();
void on_instNameTextBox_textChanged(const QString &arg1);
void on_copySavesCheckbox_stateChanged(int state);
void on_keepPlaytimeCheckbox_stateChanged(int state);
private:
Ui::CopyInstanceDialog *ui;
QString InstIconKey;
InstancePtr m_original;
bool m_copySaves = true;
bool m_keepPlaytime = true;
};

View File

@@ -10,7 +10,7 @@
<x>0</x>
<y>0</y>
<width>345</width>
<height>240</height>
<height>323</height>
</rect>
</property>
<property name="windowTitle">
@@ -116,6 +116,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="keepPlaytimeCheckbox">
<property name="text">
<string>Keep play time</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
@@ -128,6 +135,13 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>iconButton</tabstop>
<tabstop>instNameTextBox</tabstop>
<tabstop>groupBox</tabstop>
<tabstop>copySavesCheckbox</tabstop>
<tabstop>keepPlaytimeCheckbox</tabstop>
</tabstops>
<resources>
<include location="../../graphics.qrc"/>
</resources>

View File

@@ -30,7 +30,7 @@
<item>
<widget class="QLineEdit" name="userTextBox">
<property name="placeholderText">
<string>Email / Username</string>
<string>Email</string>
</property>
</widget>
</item>

View File

@@ -36,7 +36,7 @@
<item>
<widget class="QLineEdit" name="userTextBox">
<property name="placeholderText">
<string>Email / Username</string>
<string>Email</string>
</property>
</widget>
</item>

View File

@@ -1,40 +0,0 @@
#include "ModEditDialogCommon.h"
#include "CustomMessageBox.h"
#include <QUrl>
bool lastfirst(QModelIndexList &list, int &first, int &last)
{
if (list.isEmpty())
return false;
first = last = list[0].row();
for (auto item : list)
{
int row = item.row();
if (row < first)
first = row;
if (row > last)
last = row;
}
return true;
}
void showWebsiteForMod(QWidget *parentDlg, Mod &m)
{
QString url = m.homeurl();
if (url.size())
{
// catch the cases where the protocol is missing
if (!url.startsWith("http"))
{
url = "http://" + url;
}
DesktopServices::openUrl(url);
}
else
{
CustomMessageBox::selectable(
parentDlg, QObject::tr("How sad!"),
QObject::tr("The mod author didn't provide a website link for this mod."),
QMessageBox::Warning);
}
}

View File

@@ -1,9 +0,0 @@
#pragma once
#include <QModelIndex>
#include <DesktopServices.h>
#include <QWidget>
#include <minecraft/Mod.h>
bool lastfirst(QModelIndexList &list, int &first, int &last);
void showWebsiteForMod(QWidget *parentDlg, Mod &m);

View File

@@ -34,11 +34,8 @@
#include "widgets/PageContainer.h"
#include <pages/modplatform/VanillaPage.h>
#include <pages/modplatform/FTBPage.h>
#include <pages/modplatform/TwitchPage.h>
#include <pages/modplatform/legacy_ftb/Page.h>
#include <pages/modplatform/ImportPage.h>
#include <pages/modplatform/TechnicPage.h>
NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString & url, QWidget *parent)
@@ -97,14 +94,8 @@ NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString
if(!url.isEmpty())
{
QUrl actualUrl(url);
if(actualUrl.host() == "www.curseforge.com") {
m_container->selectPage("twitch");
twitchPage->setUrl(url);
}
else {
m_container->selectPage("import");
importPage->setUrl(url);
}
m_container->selectPage("import");
importPage->setUrl(url);
}
updateDialogState();
@@ -128,13 +119,11 @@ void NewInstanceDialog::accept()
QList<BasePage *> NewInstanceDialog::getPages()
{
importPage = new ImportPage(this);
twitchPage = new TwitchPage(this);
return
{
new VanillaPage(this),
importPage,
twitchPage,
new FTBPage(this)
new LegacyFTB::Page(this),
};
}

View File

@@ -29,7 +29,6 @@ class NewInstanceDialog;
class PageContainer;
class QDialogButtonBox;
class ImportPage;
class TwitchPage;
class NewInstanceDialog : public QDialog, public BasePageProvider
{
@@ -68,7 +67,6 @@ private:
QString InstIconKey;
ImportPage *importPage = nullptr;
TwitchPage *twitchPage = nullptr;
std::unique_ptr<InstanceTask> creationTask;
bool importIcon = false;

View File

@@ -22,6 +22,7 @@ UpdateDialog::UpdateDialog(bool hasUpdate, QWidget *parent) : QDialog(parent), u
ui->btnUpdateNow->setHidden(true);
ui->btnUpdateLater->setText(tr("Close"));
}
ui->changelogBrowser->setHtml(tr("<center><h1>Loading changelog...</h1></center>"));
loadChangelog();
restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("UpdateDialogGeometry").toByteArray()));
}

View File

@@ -42,13 +42,6 @@
</item>
<item>
<widget class="QTextBrowser" name="changelogBrowser">
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:12pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p align=&quot;center&quot; style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Bitstream Vera Sans'; font-size:22pt;&quot;&gt;Loading changelog...&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>

View File

@@ -0,0 +1,778 @@
#include "GroupView.h"
#include "AccessibleGroupView.h"
#include "AccessibleGroupView_p.h"
#include <qvariant.h>
#include <qaccessible.h>
#include <qheaderview.h>
#ifndef QT_NO_ACCESSIBILITY
QAccessibleInterface *groupViewAccessibleFactory(const QString &classname, QObject *object)
{
QAccessibleInterface *iface = 0;
if (!object || !object->isWidgetType())
return iface;
QWidget *widget = static_cast<QWidget*>(object);
if (classname == QLatin1String("GroupView")) {
iface = new AccessibleGroupView((GroupView *)widget);
}
return iface;
}
QAbstractItemView *AccessibleGroupView::view() const
{
return qobject_cast<QAbstractItemView*>(object());
}
int AccessibleGroupView::logicalIndex(const QModelIndex &index) const
{
if (!view()->model() || !index.isValid())
return -1;
return index.row() * (index.model()->columnCount()) + index.column();
}
AccessibleGroupView::AccessibleGroupView(QWidget *w)
: QAccessibleObject(w)
{
Q_ASSERT(view());
}
bool AccessibleGroupView::isValid() const
{
return view();
}
AccessibleGroupView::~AccessibleGroupView()
{
for (QAccessible::Id id : childToId) {
QAccessible::deleteAccessibleInterface(id);
}
}
QAccessibleInterface *AccessibleGroupView::cellAt(int row, int column) const
{
if (!view()->model()) {
return 0;
}
QModelIndex index = view()->model()->index(row, column, view()->rootIndex());
if (Q_UNLIKELY(!index.isValid())) {
qWarning() << "AccessibleGroupView::cellAt: invalid index: " << index << " for " << view();
return 0;
}
return child(logicalIndex(index));
}
QAccessibleInterface *AccessibleGroupView::caption() const
{
return 0;
}
QString AccessibleGroupView::columnDescription(int column) const
{
if (!view()->model())
return QString();
return view()->model()->headerData(column, Qt::Horizontal).toString();
}
int AccessibleGroupView::columnCount() const
{
if (!view()->model())
return 0;
return 1;
}
int AccessibleGroupView::rowCount() const
{
if (!view()->model())
return 0;
return view()->model()->rowCount();
}
int AccessibleGroupView::selectedCellCount() const
{
if (!view()->selectionModel())
return 0;
return view()->selectionModel()->selectedIndexes().count();
}
int AccessibleGroupView::selectedColumnCount() const
{
if (!view()->selectionModel())
return 0;
return view()->selectionModel()->selectedColumns().count();
}
int AccessibleGroupView::selectedRowCount() const
{
if (!view()->selectionModel())
return 0;
return view()->selectionModel()->selectedRows().count();
}
QString AccessibleGroupView::rowDescription(int row) const
{
if (!view()->model())
return QString();
return view()->model()->headerData(row, Qt::Vertical).toString();
}
QList<QAccessibleInterface *> AccessibleGroupView::selectedCells() const
{
QList<QAccessibleInterface*> cells;
if (!view()->selectionModel())
return cells;
const QModelIndexList selectedIndexes = view()->selectionModel()->selectedIndexes();
cells.reserve(selectedIndexes.size());
for (const QModelIndex &index : selectedIndexes)
cells.append(child(logicalIndex(index)));
return cells;
}
QList<int> AccessibleGroupView::selectedColumns() const
{
if (!view()->selectionModel()) {
return QList<int>();
}
const QModelIndexList selectedColumns = view()->selectionModel()->selectedColumns();
QList<int> columns;
columns.reserve(selectedColumns.size());
for (const QModelIndex &index : selectedColumns) {
columns.append(index.column());
}
return columns;
}
QList<int> AccessibleGroupView::selectedRows() const
{
if (!view()->selectionModel()) {
return QList<int>();
}
QList<int> rows;
const QModelIndexList selectedRows = view()->selectionModel()->selectedRows();
rows.reserve(selectedRows.size());
for (const QModelIndex &index : selectedRows) {
rows.append(index.row());
}
return rows;
}
QAccessibleInterface *AccessibleGroupView::summary() const
{
return 0;
}
bool AccessibleGroupView::isColumnSelected(int column) const
{
if (!view()->selectionModel()) {
return false;
}
return view()->selectionModel()->isColumnSelected(column, QModelIndex());
}
bool AccessibleGroupView::isRowSelected(int row) const
{
if (!view()->selectionModel()) {
return false;
}
return view()->selectionModel()->isRowSelected(row, QModelIndex());
}
bool AccessibleGroupView::selectRow(int row)
{
if (!view()->model() || !view()->selectionModel()) {
return false;
}
QModelIndex index = view()->model()->index(row, 0, view()->rootIndex());
if (!index.isValid() || view()->selectionBehavior() == QAbstractItemView::SelectColumns) {
return false;
}
switch (view()->selectionMode()) {
case QAbstractItemView::NoSelection: {
return false;
}
case QAbstractItemView::SingleSelection: {
if (view()->selectionBehavior() != QAbstractItemView::SelectRows && columnCount() > 1 )
return false;
view()->clearSelection();
break;
}
case QAbstractItemView::ContiguousSelection: {
if ((!row || !view()->selectionModel()->isRowSelected(row - 1, view()->rootIndex())) && !view()->selectionModel()->isRowSelected(row + 1, view()->rootIndex())) {
view()->clearSelection();
}
break;
}
default: {
break;
}
}
view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
return true;
}
bool AccessibleGroupView::selectColumn(int column)
{
if (!view()->model() || !view()->selectionModel()) {
return false;
}
QModelIndex index = view()->model()->index(0, column, view()->rootIndex());
if (!index.isValid() || view()->selectionBehavior() == QAbstractItemView::SelectRows) {
return false;
}
switch (view()->selectionMode()) {
case QAbstractItemView::NoSelection: {
return false;
}
case QAbstractItemView::SingleSelection: {
if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) {
return false;
}
// fallthrough intentional
}
case QAbstractItemView::ContiguousSelection: {
if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) {
view()->clearSelection();
}
break;
}
default: {
break;
}
}
view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Columns);
return true;
}
bool AccessibleGroupView::unselectRow(int row)
{
if (!view()->model() || !view()->selectionModel()) {
return false;
}
QModelIndex index = view()->model()->index(row, 0, view()->rootIndex());
if (!index.isValid()) {
return false;
}
QItemSelection selection(index, index);
auto selectionModel = view()->selectionModel();
switch (view()->selectionMode()) {
case QAbstractItemView::SingleSelection:
// no unselect
if (selectedRowCount() == 1) {
return false;
}
break;
case QAbstractItemView::ContiguousSelection: {
// no unselect
if (selectedRowCount() == 1) {
return false;
}
if ((!row || selectionModel->isRowSelected(row - 1, view()->rootIndex())) && selectionModel->isRowSelected(row + 1, view()->rootIndex())) {
//If there are rows selected both up the current row and down the current rown,
//the ones which are down the current row will be deselected
selection = QItemSelection(index, view()->model()->index(rowCount() - 1, 0, view()->rootIndex()));
}
}
default: {
break;
}
}
selectionModel->select(selection, QItemSelectionModel::Deselect | QItemSelectionModel::Rows);
return true;
}
bool AccessibleGroupView::unselectColumn(int column)
{
auto model = view()->model();
if (!model || !view()->selectionModel()) {
return false;
}
QModelIndex index = model->index(0, column, view()->rootIndex());
if (!index.isValid()) {
return false;
}
QItemSelection selection(index, index);
switch (view()->selectionMode()) {
case QAbstractItemView::SingleSelection: {
//In SingleSelection and ContiguousSelection once an item
//is selected, there's no way for the user to unselect all items
if (selectedColumnCount() == 1) {
return false;
}
break;
}
case QAbstractItemView::ContiguousSelection:
if (selectedColumnCount() == 1) {
return false;
}
if ((!column || view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex()))
&& view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) {
//If there are columns selected both at the left of the current row and at the right
//of the current row, the ones which are at the right will be deselected
selection = QItemSelection(index, model->index(0, columnCount() - 1, view()->rootIndex()));
}
default:
break;
}
view()->selectionModel()->select(selection, QItemSelectionModel::Deselect | QItemSelectionModel::Columns);
return true;
}
QAccessible::Role AccessibleGroupView::role() const
{
return QAccessible::List;
}
QAccessible::State AccessibleGroupView::state() const
{
return QAccessible::State();
}
QAccessibleInterface *AccessibleGroupView::childAt(int x, int y) const
{
QPoint viewportOffset = view()->viewport()->mapTo(view(), QPoint(0,0));
QPoint indexPosition = view()->mapFromGlobal(QPoint(x, y) - viewportOffset);
// FIXME: if indexPosition < 0 in one coordinate, return header
QModelIndex index = view()->indexAt(indexPosition);
if (index.isValid()) {
return child(logicalIndex(index));
}
return 0;
}
int AccessibleGroupView::childCount() const
{
if (!view()->model()) {
return 0;
}
return (view()->model()->rowCount()) * (view()->model()->columnCount());
}
int AccessibleGroupView::indexOfChild(const QAccessibleInterface *iface) const
{
if (!view()->model())
return -1;
QAccessibleInterface *parent = iface->parent();
if (parent->object() != view())
return -1;
Q_ASSERT(iface->role() != QAccessible::TreeItem); // should be handled by tree class
if (iface->role() == QAccessible::Cell || iface->role() == QAccessible::ListItem) {
const AccessibleGroupViewItem* cell = static_cast<const AccessibleGroupViewItem*>(iface);
return logicalIndex(cell->m_index);
} else if (iface->role() == QAccessible::Pane) {
return 0; // corner button
} else {
qWarning() << "AccessibleGroupView::indexOfChild has a child with unknown role..." << iface->role() << iface->text(QAccessible::Name);
}
// FIXME: we are in denial of our children. this should stop.
return -1;
}
QString AccessibleGroupView::text(QAccessible::Text t) const
{
if (t == QAccessible::Description)
return view()->accessibleDescription();
return view()->accessibleName();
}
QRect AccessibleGroupView::rect() const
{
if (!view()->isVisible())
return QRect();
QPoint pos = view()->mapToGlobal(QPoint(0, 0));
return QRect(pos.x(), pos.y(), view()->width(), view()->height());
}
QAccessibleInterface *AccessibleGroupView::parent() const
{
if (view() && view()->parent()) {
if (qstrcmp("QComboBoxPrivateContainer", view()->parent()->metaObject()->className()) == 0) {
return QAccessible::queryAccessibleInterface(view()->parent()->parent());
}
return QAccessible::queryAccessibleInterface(view()->parent());
}
return 0;
}
QAccessibleInterface *AccessibleGroupView::child(int logicalIndex) const
{
if (!view()->model())
return 0;
auto id = childToId.constFind(logicalIndex);
if (id != childToId.constEnd())
return QAccessible::accessibleInterface(id.value());
int columns = view()->model()->columnCount();
int row = logicalIndex / columns;
int column = logicalIndex % columns;
QAccessibleInterface *iface = 0;
QModelIndex index = view()->model()->index(row, column, view()->rootIndex());
if (Q_UNLIKELY(!index.isValid())) {
qWarning("AccessibleGroupView::child: Invalid index at: %d %d", row, column);
return 0;
}
iface = new AccessibleGroupViewItem(view(), index);
QAccessible::registerAccessibleInterface(iface);
childToId.insert(logicalIndex, QAccessible::uniqueId(iface));
return iface;
}
void *AccessibleGroupView::interface_cast(QAccessible::InterfaceType t)
{
if (t == QAccessible::TableInterface)
return static_cast<QAccessibleTableInterface*>(this);
return 0;
}
void AccessibleGroupView::modelChange(QAccessibleTableModelChangeEvent *event)
{
// if there is no cache yet, we don't update anything
if (childToId.isEmpty())
return;
switch (event->modelChangeType()) {
case QAccessibleTableModelChangeEvent::ModelReset:
for (QAccessible::Id id : childToId)
QAccessible::deleteAccessibleInterface(id);
childToId.clear();
break;
// rows are inserted: move every row after that
case QAccessibleTableModelChangeEvent::RowsInserted:
case QAccessibleTableModelChangeEvent::ColumnsInserted: {
ChildCache newCache;
ChildCache::ConstIterator iter = childToId.constBegin();
while (iter != childToId.constEnd()) {
QAccessible::Id id = iter.value();
QAccessibleInterface *iface = QAccessible::accessibleInterface(id);
Q_ASSERT(iface);
if (indexOfChild(iface) >= 0) {
newCache.insert(indexOfChild(iface), id);
} else {
// ### This should really not happen,
// but it might if the view has a root index set.
// This needs to be fixed.
QAccessible::deleteAccessibleInterface(id);
}
++iter;
}
childToId = newCache;
break;
}
case QAccessibleTableModelChangeEvent::ColumnsRemoved:
case QAccessibleTableModelChangeEvent::RowsRemoved: {
ChildCache newCache;
ChildCache::ConstIterator iter = childToId.constBegin();
while (iter != childToId.constEnd()) {
QAccessible::Id id = iter.value();
QAccessibleInterface *iface = QAccessible::accessibleInterface(id);
Q_ASSERT(iface);
if (iface->role() == QAccessible::Cell || iface->role() == QAccessible::ListItem) {
Q_ASSERT(iface->tableCellInterface());
AccessibleGroupViewItem *cell = static_cast<AccessibleGroupViewItem*>(iface->tableCellInterface());
// Since it is a QPersistentModelIndex, we only need to check if it is valid
if (cell->m_index.isValid())
newCache.insert(indexOfChild(cell), id);
else
QAccessible::deleteAccessibleInterface(id);
}
++iter;
}
childToId = newCache;
break;
}
case QAccessibleTableModelChangeEvent::DataChanged:
// nothing to do in this case
break;
}
}
// TABLE CELL
AccessibleGroupViewItem::AccessibleGroupViewItem(QAbstractItemView *view_, const QModelIndex &index_)
: view(view_), m_index(index_)
{
if (Q_UNLIKELY(!index_.isValid()))
qWarning() << "AccessibleGroupViewItem::AccessibleGroupViewItem with invalid index: " << index_;
}
void *AccessibleGroupViewItem::interface_cast(QAccessible::InterfaceType t)
{
if (t == QAccessible::TableCellInterface)
return static_cast<QAccessibleTableCellInterface*>(this);
if (t == QAccessible::ActionInterface)
return static_cast<QAccessibleActionInterface*>(this);
return 0;
}
int AccessibleGroupViewItem::columnExtent() const { return 1; }
int AccessibleGroupViewItem::rowExtent() const { return 1; }
QList<QAccessibleInterface*> AccessibleGroupViewItem::rowHeaderCells() const
{
return {};
}
QList<QAccessibleInterface*> AccessibleGroupViewItem::columnHeaderCells() const
{
return {};
}
int AccessibleGroupViewItem::columnIndex() const
{
if (!isValid()) {
return -1;
}
return m_index.column();
}
int AccessibleGroupViewItem::rowIndex() const
{
if (!isValid()) {
return -1;
}
return m_index.row();
}
bool AccessibleGroupViewItem::isSelected() const
{
if (!isValid()) {
return false;
}
return view->selectionModel()->isSelected(m_index);
}
QStringList AccessibleGroupViewItem::actionNames() const
{
QStringList names;
names << toggleAction();
return names;
}
void AccessibleGroupViewItem::doAction(const QString& actionName)
{
if (actionName == toggleAction()) {
if (isSelected()) {
unselectCell();
}
else {
selectCell();
}
}
}
QStringList AccessibleGroupViewItem::keyBindingsForAction(const QString &) const
{
return QStringList();
}
void AccessibleGroupViewItem::selectCell()
{
if (!isValid()) {
return;
}
QAbstractItemView::SelectionMode selectionMode = view->selectionMode();
if (selectionMode == QAbstractItemView::NoSelection) {
return;
}
Q_ASSERT(table());
QAccessibleTableInterface *cellTable = table()->tableInterface();
switch (view->selectionBehavior()) {
case QAbstractItemView::SelectItems:
break;
case QAbstractItemView::SelectColumns:
if (cellTable)
cellTable->selectColumn(m_index.column());
return;
case QAbstractItemView::SelectRows:
if (cellTable)
cellTable->selectRow(m_index.row());
return;
}
if (selectionMode == QAbstractItemView::SingleSelection) {
view->clearSelection();
}
view->selectionModel()->select(m_index, QItemSelectionModel::Select);
}
void AccessibleGroupViewItem::unselectCell()
{
if (!isValid())
return;
QAbstractItemView::SelectionMode selectionMode = view->selectionMode();
if (selectionMode == QAbstractItemView::NoSelection)
return;
QAccessibleTableInterface *cellTable = table()->tableInterface();
switch (view->selectionBehavior()) {
case QAbstractItemView::SelectItems:
break;
case QAbstractItemView::SelectColumns:
if (cellTable)
cellTable->unselectColumn(m_index.column());
return;
case QAbstractItemView::SelectRows:
if (cellTable)
cellTable->unselectRow(m_index.row());
return;
}
//If the mode is not MultiSelection or ExtendedSelection and only
//one cell is selected it cannot be unselected by the user
if ((selectionMode != QAbstractItemView::MultiSelection) && (selectionMode != QAbstractItemView::ExtendedSelection) && (view->selectionModel()->selectedIndexes().count() <= 1))
return;
view->selectionModel()->select(m_index, QItemSelectionModel::Deselect);
}
QAccessibleInterface *AccessibleGroupViewItem::table() const
{
return QAccessible::queryAccessibleInterface(view);
}
QAccessible::Role AccessibleGroupViewItem::role() const
{
return QAccessible::ListItem;
}
QAccessible::State AccessibleGroupViewItem::state() const
{
QAccessible::State st;
if (!isValid())
return st;
QRect globalRect = view->rect();
globalRect.translate(view->mapToGlobal(QPoint(0,0)));
if (!globalRect.intersects(rect()))
st.invisible = true;
if (view->selectionModel()->isSelected(m_index))
st.selected = true;
if (view->selectionModel()->currentIndex() == m_index)
st.focused = true;
if (m_index.model()->data(m_index, Qt::CheckStateRole).toInt() == Qt::Checked)
st.checked = true;
Qt::ItemFlags flags = m_index.flags();
if (flags & Qt::ItemIsSelectable) {
st.selectable = true;
st.focusable = true;
if (view->selectionMode() == QAbstractItemView::MultiSelection)
st.multiSelectable = true;
if (view->selectionMode() == QAbstractItemView::ExtendedSelection)
st.extSelectable = true;
}
return st;
}
QRect AccessibleGroupViewItem::rect() const
{
QRect r;
if (!isValid())
return r;
r = view->visualRect(m_index);
if (!r.isNull()) {
r.translate(view->viewport()->mapTo(view, QPoint(0,0)));
r.translate(view->mapToGlobal(QPoint(0, 0)));
}
return r;
}
QString AccessibleGroupViewItem::text(QAccessible::Text t) const
{
QString value;
if (!isValid())
return value;
QAbstractItemModel *model = view->model();
switch (t) {
case QAccessible::Name:
value = model->data(m_index, Qt::AccessibleTextRole).toString();
if (value.isEmpty())
value = model->data(m_index, Qt::DisplayRole).toString();
break;
case QAccessible::Description:
value = model->data(m_index, Qt::AccessibleDescriptionRole).toString();
break;
default:
break;
}
return value;
}
void AccessibleGroupViewItem::setText(QAccessible::Text /*t*/, const QString &text)
{
if (!isValid() || !(m_index.flags() & Qt::ItemIsEditable))
return;
view->model()->setData(m_index, text);
}
bool AccessibleGroupViewItem::isValid() const
{
return view && view->model() && m_index.isValid();
}
QAccessibleInterface *AccessibleGroupViewItem::parent() const
{
return QAccessible::queryAccessibleInterface(view);
}
QAccessibleInterface *AccessibleGroupViewItem::child(int) const
{
return 0;
}
#endif /* !QT_NO_ACCESSIBILITY */

View File

@@ -0,0 +1,6 @@
#pragma once
#include <QString>
class QAccessibleInterface;
QAccessibleInterface *groupViewAccessibleFactory(const QString &classname, QObject *object);

View File

@@ -0,0 +1,118 @@
#pragma once
#include "QtCore/qpointer.h"
#include <QtGui/qaccessible.h>
#include <QAccessibleWidget>
#include <QAbstractItemView>
#ifndef QT_NO_ACCESSIBILITY
#include "GroupView.h"
// #include <QHeaderView>
class QAccessibleTableCell;
class QAccessibleTableHeaderCell;
class AccessibleGroupView :public QAccessibleTableInterface, public QAccessibleObject
{
public:
explicit AccessibleGroupView(QWidget *w);
bool isValid() const override;
QAccessible::Role role() const override;
QAccessible::State state() const override;
QString text(QAccessible::Text t) const override;
QRect rect() const override;
QAccessibleInterface *childAt(int x, int y) const override;
int childCount() const override;
int indexOfChild(const QAccessibleInterface *) const override;
QAccessibleInterface *parent() const override;
QAccessibleInterface *child(int index) const override;
void *interface_cast(QAccessible::InterfaceType t) override;
// table interface
QAccessibleInterface *cellAt(int row, int column) const override;
QAccessibleInterface *caption() const override;
QAccessibleInterface *summary() const override;
QString columnDescription(int column) const override;
QString rowDescription(int row) const override;
int columnCount() const override;
int rowCount() const override;
// selection
int selectedCellCount() const override;
int selectedColumnCount() const override;
int selectedRowCount() const override;
QList<QAccessibleInterface*> selectedCells() const override;
QList<int> selectedColumns() const override;
QList<int> selectedRows() const override;
bool isColumnSelected(int column) const override;
bool isRowSelected(int row) const override;
bool selectRow(int row) override;
bool selectColumn(int column) override;
bool unselectRow(int row) override;
bool unselectColumn(int column) override;
QAbstractItemView *view() const;
void modelChange(QAccessibleTableModelChangeEvent *event) override;
protected:
// maybe vector
typedef QHash<int, QAccessible::Id> ChildCache;
mutable ChildCache childToId;
virtual ~AccessibleGroupView();
private:
inline int logicalIndex(const QModelIndex &index) const;
};
class AccessibleGroupViewItem: public QAccessibleInterface, public QAccessibleTableCellInterface, public QAccessibleActionInterface
{
public:
AccessibleGroupViewItem(QAbstractItemView *view, const QModelIndex &m_index);
void *interface_cast(QAccessible::InterfaceType t) override;
QObject *object() const override { return nullptr; }
QAccessible::Role role() const override;
QAccessible::State state() const override;
QRect rect() const override;
bool isValid() const override;
QAccessibleInterface *childAt(int, int) const override { return nullptr; }
int childCount() const override { return 0; }
int indexOfChild(const QAccessibleInterface *) const override { return -1; }
QString text(QAccessible::Text t) const override;
void setText(QAccessible::Text t, const QString &text) override;
QAccessibleInterface *parent() const override;
QAccessibleInterface *child(int) const override;
// cell interface
int columnExtent() const override;
QList<QAccessibleInterface*> columnHeaderCells() const override;
int columnIndex() const override;
int rowExtent() const override;
QList<QAccessibleInterface*> rowHeaderCells() const override;
int rowIndex() const override;
bool isSelected() const override;
QAccessibleInterface* table() const override;
//action interface
QStringList actionNames() const override;
void doAction(const QString &actionName) override;
QStringList keyBindingsForAction(const QString &actionName) const override;
private:
QPointer<QAbstractItemView > view;
QPersistentModelIndex m_index;
void selectCell();
void unselectCell();
friend class AccessibleGroupView;
};
#endif /* !QT_NO_ACCESSIBILITY */

View File

@@ -25,6 +25,7 @@
#include <QMimeData>
#include <QCache>
#include <QScrollBar>
#include <QAccessible>
#include "VisualGroup.h"
#include <QDebug>
@@ -88,6 +89,20 @@ void GroupView::rowsRemoved()
scheduleDelayedItemsLayout();
}
void GroupView::currentChanged(const QModelIndex& current, const QModelIndex& previous)
{
QAbstractItemView::currentChanged(current, previous);
// TODO: for accessibility support, implement+register a factory, steal QAccessibleTable from Qt and return an instance of it for GroupView.
#ifndef QT_NO_ACCESSIBILITY
if (QAccessible::isActive() && current.isValid()) {
QAccessibleEvent event(this, QAccessible::Focus);
event.setChild(current.row());
QAccessible::updateAccessibility(&event);
}
#endif /* !QT_NO_ACCESSIBILITY */
}
class LocaleString : public QString
{
public:
@@ -162,6 +177,9 @@ void GroupView::updateGeometries()
else
{
auto cat = new VisualGroup(groupName, this);
if(fVisibility) {
cat->collapsed = fVisibility(groupName);
}
cats.insert(groupName, cat);
cat->update();
}
@@ -371,17 +389,25 @@ void GroupView::mouseReleaseEvent(QMouseEvent *event)
if (state() == ExpandingState)
{
m_pressedCategory->collapsed = false;
emit groupStateChanged(m_pressedCategory->text, false);
updateGeometries();
viewport()->update();
event->accept();
m_pressedCategory = nullptr;
setState(NoState);
return;
}
else if (state() == CollapsingState)
{
m_pressedCategory->collapsed = true;
emit groupStateChanged(m_pressedCategory->text, true);
updateGeometries();
viewport()->update();
event->accept();
m_pressedCategory = nullptr;
setState(NoState);
return;
}
}
@@ -594,8 +620,7 @@ void GroupView::dropEvent(QDropEvent *event)
const QString categoryText = category->text;
if (model()->dropMimeData(event->mimeData(), Qt::MoveAction, row, 0, QModelIndex()))
{
model()->setData(model()->index(row, 0), categoryText,
GroupViewRoles::GroupRole);
model()->setData(model()->index(row, 0), categoryText, GroupViewRoles::GroupRole);
event->setDropAction(Qt::MoveAction);
event->accept();
}

View File

@@ -20,6 +20,7 @@
#include <QScrollBar>
#include <QCache>
#include "VisualGroup.h"
#include <functional>
struct GroupViewRoles
{
@@ -41,6 +42,11 @@ public:
void setModel(QAbstractItemModel *model) override;
using visibilityFunction = std::function<bool(const QString &)>;
void setSourceOfGroupCollapseStatus(visibilityFunction f) {
fVisibility = f;
}
/// return geometry rectangle occupied by the specified model item
QRect geometryRect(const QModelIndex &index) const;
/// return visual rectangle occupied by the specified model item
@@ -48,8 +54,7 @@ public:
/// get the model index at the specified visual point
virtual QModelIndex indexAt(const QPoint &point) const override;
QString groupNameAt(const QPoint &point);
void setSelection(const QRect &rect,
const QItemSelectionModel::SelectionFlags commands) override;
void setSelection(const QRect &rect, const QItemSelectionModel::SelectionFlags commands) override;
virtual int horizontalOffset() const override;
virtual int verticalOffset() const override;
@@ -76,9 +81,11 @@ protected slots:
virtual void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) override;
void modelReset();
void rowsRemoved();
void currentChanged(const QModelIndex &current, const QModelIndex &previous) override;
signals:
void droppedURLs(QList<QUrl> urls);
void groupStateChanged(QString group, bool collapsed);
protected:
virtual bool isIndexHidden(const QModelIndex &index) const override;
@@ -102,6 +109,8 @@ private:
friend struct VisualGroup;
QList<VisualGroup *> m_groups;
visibilityFunction fVisibility;
// geometry
int m_leftMargin = 5;
int m_rightMargin = 5;

View File

@@ -205,6 +205,8 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti
{
// FIXME: unused
// QSize textSize = viewItemTextSize ( &opt );
drawSelectionRect(painter, opt, textHighlightRect);
/*
QPalette::ColorGroup cg;
QStyleOptionViewItem opt2(opt);
@@ -219,10 +221,13 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti
{
cg = QPalette::Disabled;
}
*/
/*
opt2.palette.setCurrentColorGroup(cg);
// fill in background, if any
if (opt.backgroundBrush.style() != Qt::NoBrush)
{
QPointF oldBO = painter->brushOrigin();
@@ -232,6 +237,7 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti
}
drawSelectionRect(painter, opt2, textHighlightRect);
*/
/*
if (opt.showDecorationSelected)

View File

@@ -29,8 +29,8 @@ int main(int argc, char *argv[])
#endif
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif
// initialize Qt
@@ -43,7 +43,6 @@ int main(int argc, char *argv[])
{
Q_INIT_RESOURCE(multimc);
Q_INIT_RESOURCE(backgrounds);
Q_INIT_RESOURCE(assets);
Q_INIT_RESOURCE(pe_dark);
Q_INIT_RESOURCE(pe_light);

View File

@@ -1,7 +1,7 @@
[Desktop Entry]
Version=1.0
Name=MultiMC
GenericName=MultiMC
GenericName=Minecraft Launcher
Comment=Free, open source launcher and instance manager for Minecraft.
Type=Application
Terminal=false

View File

@@ -1,11 +1,11 @@
Package: multimc
Version: 1.3-1
Version: 1.4-1
Architecture: all
Maintainer: Petr Mrázek <peterix@gmail.com>
Section: games
Priority: optional
Installed-Size: 75
Depends: zenity, desktop-file-utils, qt5-default
Depends: zenity, desktop-file-utils, qt5-default, wget
Recommends: openjdk-8-jre
Homepage: http://multimc.org
Description: A local install wrapper for MultiMC

View File

@@ -39,6 +39,7 @@ PageDialog::PageDialog(BasePageProvider *pageProvider, QString defaultId, QWidge
QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Close);
buttons->button(QDialogButtonBox::Close)->setDefault(true);
buttons->setContentsMargins(6, 0, 6, 0);
m_container->addButtons(buttons);
connect(buttons->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SLOT(close()));

View File

@@ -17,6 +17,7 @@
#include "ui_AccountListPage.h"
#include <QItemSelectionModel>
#include <QMenu>
#include <QDebug>
@@ -33,45 +34,37 @@
#include "MultiMC.h"
enum class OfflineModeNameMode
{
UseAccountName = 1,
RememberPerAccount = 2,
RememberPerInstance = 3,
UseFixedName = 4
};
AccountListPage::AccountListPage(QWidget *parent)
: QWidget(parent), ui(new Ui::AccountListPage)
: QMainWindow(parent), ui(new Ui::AccountListPage)
{
ui->setupUi(this);
ui->listView->setEmptyString(tr(
"Welcome!\n"
"If you're new here, you can click the \"Add\" button to add your Mojang or Minecraft account."
));
ui->listView->setEmptyMode(VersionListView::String);
ui->listView->setContextMenuPolicy(Qt::CustomContextMenu);
m_accounts = MMC->accounts();
ui->listView->setModel(m_accounts.get());
ui->listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
ui->listView->setSelectionMode(QAbstractItemView::SingleSelection);
// Expand the account column
ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
QItemSelectionModel *selectionModel = ui->listView->selectionModel();
connect(selectionModel, &QItemSelectionModel::selectionChanged,
[this](const QItemSelection &sel, const QItemSelection &dsel)
{ updateButtonStates(); });
connect(selectionModel, &QItemSelectionModel::selectionChanged, [this](const QItemSelection &sel, const QItemSelection &dsel) {
updateButtonStates();
});
connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu);
connect(m_accounts.get(), SIGNAL(listChanged()), SLOT(listChanged()));
connect(m_accounts.get(), SIGNAL(activeAccountChanged()), SLOT(listChanged()));
ui->offlineButtonGroup->setId(ui->useSelectedNameBtn, int(OfflineModeNameMode::UseAccountName));
ui->offlineButtonGroup->setId(ui->rememberNamesForAccountsBtn, int(OfflineModeNameMode::RememberPerAccount));
ui->offlineButtonGroup->setId(ui->rememberNamesForInstancesBtn, int(OfflineModeNameMode::RememberPerInstance));
ui->offlineButtonGroup->setId(ui->useFixedNameBtn, int(OfflineModeNameMode::UseFixedName));
connect(ui->offlineButtonGroup, SIGNAL(buttonToggled(int,bool)), this, SLOT(groupSelectionChanged(int,bool)));
updateButtonStates();
loadSettings();
}
AccountListPage::~AccountListPage()
@@ -79,18 +72,41 @@ AccountListPage::~AccountListPage()
delete ui;
}
void AccountListPage::ShowContextMenu(const QPoint& pos)
{
auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
menu->exec(ui->listView->mapToGlobal(pos));
delete menu;
}
void AccountListPage::changeEvent(QEvent* event)
{
if (event->type() == QEvent::LanguageChange)
{
ui->retranslateUi(this);
}
QMainWindow::changeEvent(event);
}
QMenu * AccountListPage::createPopupMenu()
{
QMenu* filteredMenu = QMainWindow::createPopupMenu();
filteredMenu->removeAction(ui->toolBar->toggleViewAction() );
return filteredMenu;
}
void AccountListPage::listChanged()
{
updateButtonStates();
}
void AccountListPage::on_addAccountBtn_clicked()
void AccountListPage::on_actionAdd_triggered()
{
addAccount(tr("Please enter your Mojang or Minecraft account username and password to add "
"your account."));
addAccount(tr("Please enter your Minecraft account email and password to add your account."));
}
void AccountListPage::on_rmAccountBtn_clicked()
void AccountListPage::on_actionRemove_triggered()
{
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
if (selection.size() > 0)
@@ -100,7 +116,7 @@ void AccountListPage::on_rmAccountBtn_clicked()
}
}
void AccountListPage::on_setDefaultBtn_clicked()
void AccountListPage::on_actionSetDefault_triggered()
{
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
if (selection.size() > 0)
@@ -112,7 +128,7 @@ void AccountListPage::on_setDefaultBtn_clicked()
}
}
void AccountListPage::on_noDefaultBtn_clicked()
void AccountListPage::on_actionNoDefault_triggered()
{
m_accounts->setActiveAccount("");
}
@@ -122,11 +138,19 @@ void AccountListPage::updateButtonStates()
// If there is no selection, disable buttons that require something selected.
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
ui->rmAccountBtn->setEnabled(selection.size() > 0);
ui->setDefaultBtn->setEnabled(selection.size() > 0);
ui->uploadSkinBtn->setEnabled(selection.size() > 0);
ui->actionRemove->setEnabled(selection.size() > 0);
ui->actionSetDefault->setEnabled(selection.size() > 0);
ui->actionUploadSkin->setEnabled(selection.size() > 0);
if(m_accounts->activeAccount().get() == nullptr) {
ui->actionNoDefault->setEnabled(false);
ui->actionNoDefault->setChecked(true);
}
else {
ui->actionNoDefault->setEnabled(true);
ui->actionNoDefault->setChecked(false);
}
ui->noDefaultBtn->setDown(m_accounts->activeAccount().get() == nullptr);
}
void AccountListPage::addAccount(const QString &errMsg)
@@ -155,7 +179,7 @@ void AccountListPage::addAccount(const QString &errMsg)
}
}
void AccountListPage::on_uploadSkinBtn_clicked()
void AccountListPage::on_actionUploadSkin_triggered()
{
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
if (selection.size() > 0)
@@ -166,68 +190,3 @@ void AccountListPage::on_uploadSkinBtn_clicked()
dialog.exec();
}
}
bool AccountListPage::apply()
{
applySettings();
return true;
}
void AccountListPage::applySettings()
{
auto s = MMC->settings();
auto sortMode = (OfflineModeNameMode)ui->offlineButtonGroup->checkedId();
switch (sortMode)
{
default:
case OfflineModeNameMode::UseAccountName:
s->set("OfflineModeNameMode", "UseAccountName");
break;
case OfflineModeNameMode::RememberPerAccount:
s->set("OfflineModeNameMode", "RememberPerAccount");
break;
case OfflineModeNameMode::RememberPerInstance:
s->set("OfflineModeNameMode", "RememberPerInstance");
break;
case OfflineModeNameMode::UseFixedName:
s->set("OfflineModeNameMode", "UseFixedName");
break;
}
s->set("OfflineModeName", ui->mainOfflineNameEdit->text());
}
void AccountListPage::loadSettings()
{
auto s = MMC->settings();
auto value = s->get("OfflineModeNameMode").toString();
if(value == "UseAccountName")
{
ui->useSelectedNameBtn->setChecked(true);
}
else if(value == "RememberPerAccount")
{
ui->rememberNamesForAccountsBtn->setChecked(true);
}
else if(value == "RememberPerInstance")
{
ui->rememberNamesForInstancesBtn->setChecked(true);
}
else if(value == "UseFixedName")
{
ui->useFixedNameBtn->setChecked(true);
}
ui->mainOfflineNameEdit->setText(s->get("OfflineModeName").toString());
}
void AccountListPage::groupSelectionChanged(int, bool)
{
auto sortMode = (OfflineModeNameMode)ui->offlineButtonGroup->checkedId();
if(sortMode == OfflineModeNameMode::UseFixedName)
{
ui->mainOfflineNameEdit->setEnabled(true);
}
else
{
ui->mainOfflineNameEdit->setEnabled(false);
}
}

View File

@@ -15,7 +15,7 @@
#pragma once
#include <QDialog>
#include <QMainWindow>
#include <memory>
#include "pages/BasePage.h"
@@ -30,7 +30,7 @@ class AccountListPage;
class AuthenticateTask;
class AccountListPage : public QWidget, public BasePage
class AccountListPage : public QMainWindow, public BasePage
{
Q_OBJECT
public:
@@ -58,24 +58,25 @@ public:
{
return "Getting-Started#adding-an-account";
}
bool apply() override;
private:
void changeEvent(QEvent * event) override;
QMenu * createPopupMenu() override;
public
slots:
void on_addAccountBtn_clicked();
void on_actionAdd_triggered();
void on_rmAccountBtn_clicked();
void on_actionRemove_triggered();
void on_setDefaultBtn_clicked();
void on_actionSetDefault_triggered();
void on_noDefaultBtn_clicked();
void on_actionNoDefault_triggered();
void on_uploadSkinBtn_clicked();
void on_actionUploadSkin_triggered();
void listChanged();
void groupSelectionChanged(int, bool);
//! Updates the states of the dialog's buttons.
void updateButtonStates();
@@ -84,12 +85,9 @@ protected:
protected
slots:
void ShowContextMenu(const QPoint &pos);
void addAccount(const QString& errMsg="");
private:
void applySettings();
void loadSettings();
private:
Ui::AccountListPage *ui;
};

View File

@@ -1,201 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AccountListPage</class>
<widget class="QWidget" name="AccountListPage">
<widget class="QMainWindow" name="AccountListPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>333</width>
<height>302</height>
<width>800</width>
<height>600</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
<widget class="QWidget" name="centralwidget">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="VersionListView" name="listView"/>
</item>
</layout>
</widget>
<widget class="WideBar" name="toolBar">
<attribute name="toolBarArea">
<enum>RightToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionAdd"/>
<addaction name="actionRemove"/>
<addaction name="actionSetDefault"/>
<addaction name="actionNoDefault"/>
<addaction name="actionUploadSkin"/>
</widget>
<action name="actionAdd">
<property name="text">
<string>Add</string>
</property>
<property name="topMargin">
<number>0</number>
</action>
<action name="actionRemove">
<property name="text">
<string>Remove</string>
</property>
<property name="rightMargin">
<number>0</number>
</action>
<action name="actionSetDefault">
<property name="text">
<string>Set Default</string>
</property>
<property name="bottomMargin">
<number>0</number>
</action>
<action name="actionNoDefault">
<property name="checkable">
<bool>true</bool>
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="onlineTab">
<attribute name="title">
<string>Online</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="welcomeLabel">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Welcome! If you're new here, you can click the &amp;quot;Add&amp;quot; button to add your Mojang or Minecraft account.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTreeView" name="listView"/>
</item>
<item>
<layout class="QVBoxLayout" name="manageAcctsBtnBox">
<item>
<widget class="QPushButton" name="addAccountBtn">
<property name="text">
<string>&amp;Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="rmAccountBtn">
<property name="text">
<string>&amp;Remove</string>
</property>
</widget>
</item>
<item>
<spacer name="buttonSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="setDefaultBtn">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Set the currently selected account as the active account. The active account is the account that is used to log in (unless it is overridden in an instance-specific setting).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>&amp;Set Default</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="noDefaultBtn">
<property name="toolTip">
<string>Set no default account. This will cause MultiMC to prompt you to select an account every time you launch an instance that doesn't have its own default set.</string>
</property>
<property name="text">
<string>&amp;No Default</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uploadSkinBtn">
<property name="toolTip">
<string>Opens a dialog to select and upload a skin image to the selected account.</string>
</property>
<property name="text">
<string>&amp;Upload Skin</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="offlineTab">
<attribute name="title">
<string>Offline</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout22">
<item>
<widget class="QGroupBox" name="offlineBox">
<property name="title">
<string>Offline mode name</string>
</property>
<layout class="QGridLayout" name="foldersBoxLayout_2">
<item row="0" column="0">
<widget class="QRadioButton" name="useSelectedNameBtn">
<property name="text">
<string>&amp;Use selected account name</string>
</property>
<attribute name="buttonGroup">
<string notr="true">offlineButtonGroup</string>
</attribute>
</widget>
</item>
<item row="3" column="0">
<widget class="QRadioButton" name="useFixedNameBtn">
<property name="text">
<string>Always use &amp;this offline name:</string>
</property>
<attribute name="buttonGroup">
<string notr="true">offlineButtonGroup</string>
</attribute>
</widget>
</item>
<item row="4" column="0">
<widget class="QLineEdit" name="mainOfflineNameEdit">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="rememberNamesForAccountsBtn">
<property name="text">
<string>Remember offline &amp;names per account</string>
</property>
<attribute name="buttonGroup">
<string notr="true">offlineButtonGroup</string>
</attribute>
</widget>
</item>
<item row="2" column="0">
<widget class="QRadioButton" name="rememberNamesForInstancesBtn">
<property name="text">
<string>Remember offline &amp;names per instance</string>
</property>
<attribute name="buttonGroup">
<string notr="true">offlineButtonGroup</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
<property name="text">
<string>No Default</string>
</property>
</action>
<action name="actionUploadSkin">
<property name="text">
<string>Upload Skin</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>VersionListView</class>
<extends>QTreeView</extends>
<header>widgets/VersionListView.h</header>
</customwidget>
<customwidget>
<class>WideBar</class>
<extends>QToolBar</extends>
<header>widgets/WideBar.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
<buttongroups>
<buttongroup name="offlineButtonGroup"/>
</buttongroups>
</ui>

View File

@@ -13,6 +13,7 @@ CustomCommandsPage::CustomCommandsPage(QWidget* parent): QWidget(parent)
auto tabWidget = new QTabWidget(this);
tabWidget->setObjectName(QStringLiteral("tabWidget"));
commands = new CustomCommands(this);
commands->setContentsMargins(6, 6, 6, 6);
tabWidget->addTab(commands, "Foo");
tabWidget->tabBar()->hide();
verticalLayout->addWidget(tabWidget);

View File

@@ -78,12 +78,12 @@ MultiMCPage::MultiMCPage(QWidget *parent) : QWidget(parent), ui(new Ui::MultiMCP
}
connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview()));
connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview()));
connect(ui->languageBox, SIGNAL(currentIndexChanged(int)), SLOT(languageIndexChanged(int)));
}
MultiMCPage::~MultiMCPage()
{
delete ui;
delete defaultFormat;
}
bool MultiMCPage::apply()
@@ -147,19 +147,6 @@ void MultiMCPage::on_modsDirBrowseBtn_clicked()
}
}
void MultiMCPage::languageIndexChanged(int index)
{
auto languageCode = ui->languageBox->itemData(ui->languageBox->currentIndex()).toString();
if(languageCode.isEmpty())
{
qWarning() << "Unknown language at index" << index;
return;
}
auto translations = MMC->translations();
translations->selectLanguage(languageCode);
translations->updateLanguage(languageCode);
}
void MultiMCPage::refreshUpdateChannelList()
{
// Stop listening for selection changes. It's going to change a lot while we update it and
@@ -236,10 +223,6 @@ void MultiMCPage::applySettings()
{
auto s = MMC->settings();
// Language
auto langCode = ui->languageBox->itemData(ui->languageBox->currentIndex()).toString();
s->set("Language", langCode.isEmpty() ? "en" : langCode);
if (ui->resetNotificationsBtn->isChecked())
{
s->set("ShownNotifications", QString());
@@ -332,12 +315,6 @@ void MultiMCPage::applySettings()
void MultiMCPage::loadSettings()
{
auto s = MMC->settings();
// Language
{
ui->languageBox->setModel(m_languageModel.get());
ui->languageBox->setCurrentIndex(ui->languageBox->findData(s->get("Language").toString()));
}
// Updates
ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool());
m_currentUpdateChannel = s->get("UpdateChannel").toString();

View File

@@ -68,8 +68,6 @@ slots:
void on_modsDirBrowseBtn_clicked();
void on_iconsDirBrowseBtn_clicked();
void languageIndexChanged(int index);
/*!
* Updates the list of update channels in the combo box.
*/

View File

@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>467</width>
<width>514</width>
<height>629</height>
</rect>
</property>
@@ -228,18 +228,6 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Language:</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QComboBox" name="languageBox"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="themeBox">
<property name="title">
@@ -570,7 +558,6 @@
<tabstop>resetNotificationsBtn</tabstop>
<tabstop>sortLastLaunchedBtn</tabstop>
<tabstop>sortByNameBtn</tabstop>
<tabstop>languageBox</tabstop>
<tabstop>themeComboBox</tabstop>
<tabstop>themeComboBoxColors</tabstop>
<tabstop>showConsoleCheck</tabstop>

View File

@@ -1,224 +0,0 @@
/* Copyright 2015-2019 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "PackagesPage.h"
#include "ui_PackagesPage.h"
#include <QDateTime>
#include <QSortFilterProxyModel>
#include <QRegularExpression>
#include "dialogs/ProgressDialog.h"
#include "VersionProxyModel.h"
#include "meta/Index.h"
#include "meta/VersionList.h"
#include "meta/Version.h"
#include "Env.h"
#include "MultiMC.h"
using namespace Meta;
static QString formatRequires(const VersionPtr &version)
{
QStringList lines;
auto & reqs = version->requires();
auto iter = reqs.begin();
while (iter != reqs.end())
{
auto &uid = iter->uid;
auto &version = iter->equalsVersion;
const QString readable = ENV.metadataIndex()->hasUid(uid) ? ENV.metadataIndex()->get(uid)->humanReadable() : uid;
if(!version.isEmpty())
{
lines.append(QString("%1 (%2)").arg(readable, version));
}
else
{
lines.append(QString("%1").arg(readable));
}
iter++;
}
return lines.join('\n');
}
PackagesPage::PackagesPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::PackagesPage)
{
ui->setupUi(this);
ui->tabWidget->tabBar()->hide();
m_fileProxy = new QSortFilterProxyModel(this);
m_fileProxy->setSortRole(Qt::DisplayRole);
m_fileProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
m_fileProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_fileProxy->setFilterRole(Qt::DisplayRole);
m_fileProxy->setFilterKeyColumn(0);
m_fileProxy->sort(0);
m_fileProxy->setSourceModel(ENV.metadataIndex().get());
ui->indexView->setModel(m_fileProxy);
m_filterProxy = new QSortFilterProxyModel(this);
m_filterProxy->setSortRole(VersionList::SortRole);
m_filterProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_filterProxy->setFilterRole(Qt::DisplayRole);
m_filterProxy->setFilterKeyColumn(0);
m_filterProxy->sort(0, Qt::DescendingOrder);
ui->versionsView->setModel(m_filterProxy);
m_versionProxy = new VersionProxyModel(this);
m_filterProxy->setSourceModel(m_versionProxy);
connect(ui->indexView->selectionModel(), &QItemSelectionModel::currentChanged, this, &PackagesPage::updateCurrentVersionList);
connect(ui->versionsView->selectionModel(), &QItemSelectionModel::currentChanged, this, &PackagesPage::updateVersion);
connect(m_filterProxy, &QSortFilterProxyModel::dataChanged, this, &PackagesPage::versionListDataChanged);
updateCurrentVersionList(QModelIndex());
updateVersion();
}
PackagesPage::~PackagesPage()
{
delete ui;
}
QIcon PackagesPage::icon() const
{
return MMC->getThemedIcon("packages");
}
void PackagesPage::on_refreshIndexBtn_clicked()
{
ENV.metadataIndex()->load(Net::Mode::Online);
}
void PackagesPage::on_refreshFileBtn_clicked()
{
VersionListPtr list = ui->indexView->currentIndex().data(Index::ListPtrRole).value<VersionListPtr>();
if (!list)
{
return;
}
list->load(Net::Mode::Online);
}
void PackagesPage::on_refreshVersionBtn_clicked()
{
VersionPtr version = ui->versionsView->currentIndex().data(VersionList::VersionPtrRole).value<VersionPtr>();
if (!version)
{
return;
}
version->load(Net::Mode::Online);
}
void PackagesPage::on_fileSearchEdit_textChanged(const QString &search)
{
if (search.isEmpty())
{
m_fileProxy->setFilterFixedString(QString());
}
else
{
QStringList parts = search.split(' ');
std::transform(parts.begin(), parts.end(), parts.begin(), &QRegularExpression::escape);
m_fileProxy->setFilterRegExp(".*" + parts.join(".*") + ".*");
}
}
void PackagesPage::on_versionSearchEdit_textChanged(const QString &search)
{
if (search.isEmpty())
{
m_filterProxy->setFilterFixedString(QString());
}
else
{
QStringList parts = search.split(' ');
std::transform(parts.begin(), parts.end(), parts.begin(), &QRegularExpression::escape);
m_filterProxy->setFilterRegExp(".*" + parts.join(".*") + ".*");
}
}
void PackagesPage::updateCurrentVersionList(const QModelIndex &index)
{
if (index.isValid())
{
VersionListPtr list = index.data(Index::ListPtrRole).value<VersionListPtr>();
ui->versionsBox->setEnabled(true);
ui->refreshFileBtn->setEnabled(true);
ui->fileUidLabel->setEnabled(true);
ui->fileUid->setText(list->uid());
ui->fileNameLabel->setEnabled(true);
ui->fileName->setText(list->name());
m_versionProxy->setSourceModel(list.get());
ui->refreshFileBtn->setText(tr("Refresh %1").arg(list->humanReadable()));
list->load(Net::Mode::Offline);
}
else
{
ui->versionsBox->setEnabled(false);
ui->refreshFileBtn->setEnabled(false);
ui->fileUidLabel->setEnabled(false);
ui->fileUid->clear();
ui->fileNameLabel->setEnabled(false);
ui->fileName->clear();
m_versionProxy->setSourceModel(nullptr);
ui->refreshFileBtn->setText(tr("Refresh"));
}
}
void PackagesPage::versionListDataChanged(const QModelIndex &tl, const QModelIndex &br)
{
if (QItemSelection(tl, br).contains(ui->versionsView->currentIndex()))
{
updateVersion();
}
}
void PackagesPage::updateVersion()
{
VersionPtr version = std::dynamic_pointer_cast<Version>(
ui->versionsView->currentIndex().data(VersionList::VersionPointerRole).value<BaseVersionPtr>());
if (version)
{
ui->refreshVersionBtn->setEnabled(true);
ui->versionVersionLabel->setEnabled(true);
ui->versionVersion->setText(version->version());
ui->versionTimeLabel->setEnabled(true);
ui->versionTime->setText(version->time().toString("yyyy-MM-dd HH:mm"));
ui->versionTypeLabel->setEnabled(true);
ui->versionType->setText(version->type());
ui->versionRequiresLabel->setEnabled(true);
ui->versionRequires->setText(formatRequires(version));
ui->refreshVersionBtn->setText(tr("Refresh %1").arg(version->version()));
}
else
{
ui->refreshVersionBtn->setEnabled(false);
ui->versionVersionLabel->setEnabled(false);
ui->versionVersion->clear();
ui->versionTimeLabel->setEnabled(false);
ui->versionTime->clear();
ui->versionTypeLabel->setEnabled(false);
ui->versionType->clear();
ui->versionRequiresLabel->setEnabled(false);
ui->versionRequires->clear();
ui->refreshVersionBtn->setText(tr("Refresh"));
}
}
void PackagesPage::openedImpl()
{
ENV.metadataIndex()->load(Net::Mode::Offline);
}

Some files were not shown because too many files have changed in this diff Show More