Compare commits

..

122 Commits

Author SHA1 Message Date
Petr Mrázek
e2378c79ff NOISSUE meow meow prototype 2020-02-08 15:01:53 +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
Petr Mrázek
62e1bf327d Merge pull request #2740 from jturnism/patch-1
Update changelog.md
2019-07-12 08:19:20 +02:00
Joseph Turner
280e0e6e36 Update changelog.md
Just installing "qt5-qtbase" on Fedora 30 does not allow MultiMC to run. It still needs "libQt5Widgets.so.5" and by running "dnf whatprovides" it tells me "qt5-qtbase-gui" provides that file and also pulls in "qt5-qtbase" as a dependency if not already installed. I am assuming this is the same situation for CentOS/RHEL
2019-07-11 21:00:55 -05:00
Petr Mrázek
3a67990acd NOISSUE bump deb package version 2019-07-11 01:28:48 +02:00
Petr Mrázek
23eab74e6d NOISSUE make the deb package depend on Qt5 2019-07-11 01:26:33 +02:00
Petr Mrázek
b9d4293552 NOISSUE update component buttons some more when the versions change 2019-07-11 01:01:47 +02:00
Petr Mrázek
5110b58def NOISSUE update version and changelog 2019-07-11 00:35:44 +02:00
Petr Mrázek
791a8227b6 NOISSUE disable component install buttons in impossible cases 2019-07-10 22:30:42 +02:00
Petr Mrázek
725ec35635 NOISSUE recognize curseforge URLs dropped on top of MultiMC 2019-07-09 22:04:52 +02:00
Petr Mrázek
739a86f171 Revert "NOISSUE Import page is now a MultiMC pack page"
This reverts commit f74e3db804.
2019-07-09 21:51:19 +02:00
Petr Mrázek
48b2f95129 Revert "NOISSUE simple/stupid default game options, UI only"
This reverts commit 497d9bec02.
2019-07-09 21:43:12 +02:00
Petr Mrázek
497d9bec02 NOISSUE simple/stupid default game options, UI only 2019-07-09 02:37:04 +02:00
Petr Mrázek
c01d020afc GH-2723 disable deprecation warnings
We are targeting version 5.4 of the Qt ABI.
Deprecations from 2019 are irrelevant.
2019-07-03 01:11:18 +02:00
Petr Mrázek
ee83d432f6 GH-2724 update group view geometries in more cases
Fixes crashes when adding instances to groups that didn't exist before.
2019-07-02 02:09:41 +02:00
Petr Mrázek
8ee11b1a8e GH-2716 do not censor values shorter than 4 in logs 2019-07-01 00:00:34 +02:00
Petr Mrázek
0b86a7ebf3 Merge pull request #2718 from therealfarfetchd/hidpi-icon-fix
Enable HiDPI pixmaps to fix icon scaling for HiDPI displays
2019-06-30 15:39:29 +02:00
Petr Mrázek
63330bf111 NOISSUE connect twitch URL resolving to modpack resolving. works now. 2019-06-30 11:03:59 +02:00
Petr Mrázek
f74e3db804 NOISSUE Import page is now a MultiMC pack page 2019-06-29 01:13:39 +02:00
therealfarfetchd
a55fa04353 Enable HiDPI pixmaps to fix icon scaling for HiDPI displays 2019-06-27 23:04:53 +02:00
Petr Mrázek
fde43c993e NOISSUE add silly twitch URL and CCIP resolving page to 'add instance'
It needs a few more steps and it will handle all kinds of twitch packs.
2019-06-27 03:20:11 +02:00
Petr Mrázek
917f148fc4 NOISSUE add support for 'experiment' Minecraft versions 2019-06-26 20:51:04 +02:00
Petr Mrázek
34611c00e3 Merge branch 'feature/update_translation_fix' into develop 2019-06-25 23:41:16 +02:00
Petr Mrázek
44a7c5867b Merge pull request #2703 from Janrupf/feature/apply_proxy_settings
GH-2499 Apply proxy settings immediately
2019-06-23 21:38:30 +02:00
Petr Mrázek
75ddbc8851 Merge pull request #2705 from Janrupf/feature/fix_external_deletion_interaction
GH-2515 Save instance ID before display dialog
2019-06-23 21:31:56 +02:00
Petr Mrázek
2f1d31cf43 Merge pull request #2706 from Janrupf/feature/fix_hashtag_in_notes
Feature/fix hashtag in notes
2019-06-23 21:19:10 +02:00
Petr Mrázek
e7c5b266c8 Merge pull request #2708 from Janrupf/feature/single_imgur_uploads
GH-689 Don't create album for single screenshot
2019-06-23 21:15:06 +02:00
Petr Mrázek
384979bf94 Merge pull request #2704 from Janrupf/feature/autoselect_new_instances
GH-2592 Autoselect newly created instances
2019-06-23 19:58:21 +02:00
janrupf
b5a16935b7 NOISSUE Renaming for better understanding 2019-06-23 14:54:17 +02:00
janrupf
320637e8dc GH-1701 Check job size for translation 2019-06-22 01:54:08 +02:00
janrupf
77f3f028fa GH-2499 Apply proxy settings immediately 2019-06-22 01:48:37 +02:00
janrupf
2a96e16902 GH-689 Don't create album for single screenshot 2019-06-22 01:47:07 +02:00
janrupf
1ed84eddd5 GH-2515 Save instance ID before display dialog 2019-06-21 23:55:16 +02:00
janrupf
7b52b8689b NOISSUE Test comment escaping with unit tests 2019-06-21 23:46:54 +02:00
janrupf
d21700ee91 NOISSUE Revert INI parser back to single pass 2019-06-21 23:46:54 +02:00
janrupf
f87c890912 GH-1813 Escape # in INI (and better reader) 2019-06-21 23:46:54 +02:00
janrupf
306b98edac GH-2592 Autoselect newly created instances 2019-06-21 22:38:26 +02:00
156 changed files with 4787 additions and 3716 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

@@ -32,7 +32,7 @@ set(CMAKE_C_STANDARD_REQUIRED true)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_C_STANDARD 11)
include(GenerateExportHeader)
set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}")
set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}")
if(UNIX AND APPLE)
set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}")
endif()
@@ -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 5)
set(MultiMC_VERSION_HOTFIX 8)
# 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 ################################
@@ -87,6 +88,7 @@ find_package(Qt5Concurrent REQUIRED)
find_package(Qt5Network REQUIRED)
find_package(Qt5Test REQUIRED)
find_package(Qt5Xml REQUIRED)
find_package(Qt5Multimedia REQUIRED)
# The Qt5 cmake files don't provide its install paths, so ask qmake.
include(QMakeQuery)

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
@@ -477,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)
{
@@ -304,10 +309,6 @@ QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{
string[i] = replaceWith;
}
else if(string[i] > 127) // non ASCII
{
string[i] = replaceWith;
}
}
return string;
}

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,16 +233,21 @@ 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);
if(!inst)
{
qDebug() << "Cannot delete instance" << id << " No such instance is present.";
qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?).";
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,10 +833,11 @@ 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);
}
saveGroupList();
return true;

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);
@@ -129,10 +131,12 @@ public:
signals:
void dataIsInvalid();
void instancesChanged();
void instanceSelectRequest(QString instanceId);
void groupsChanged(QSet<QString> groups);
public slots:
void on_InstFolderChanged(const Setting &setting, QVariant value);
void on_GroupStateChanged(const QString &group, bool collapsed);
private slots:
void propertiesChanged(BaseInstance *inst);
@@ -153,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);
}
@@ -635,6 +637,9 @@ void ComponentList::componentDataChanged()
qWarning() << "ComponentList got dataChenged signal from a non-Component!";
return;
}
if(objPtr->getID() == "net.minecraft") {
emit minecraftChanged();
}
// figure out which one is it... in a seriously dumb way.
int index = 0;
for (auto component: d->components)
@@ -762,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();
}
@@ -773,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())
@@ -853,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;
}
@@ -1202,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

@@ -104,6 +104,9 @@ public:
/// if there is a save scheduled, do it now.
void saveNow();
signals:
void minecraftChanged();
public:
/// get the profile component by id
Component * getComponent(const QString &id);
@@ -131,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

@@ -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())
@@ -648,8 +650,7 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess
auto i = sessionRef.u.properties.begin();
while (i != sessionRef.u.properties.end())
{
if(i.key() == "preferredLanguage")
{
if(i.value().length() <= 3) {
++i;
continue;
}
@@ -828,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));
@@ -893,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;
@@ -113,9 +111,6 @@ public:
virtual JavaVersion getJavaVersion() const;
signals:
void versionReloaded();
protected:
QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session);
QStringList validLaunchMethods();
@@ -126,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

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

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

@@ -1,7 +1,9 @@
#include "FileResolvingTask.h"
#include "Json.h"
const char * metabase = "https://cursemeta.dries007.net";
namespace {
const char * metabase = "https://cursemeta.dries007.net";
}
Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess)
: m_toProcess(toProcess)
@@ -34,70 +36,14 @@ void Flame::FileResolvingTask::netJobFinished()
int index = 0;
for(auto & bytes: results)
{
auto & out = m_toProcess.files[index];
try
{
auto doc = Json::requireDocument(bytes);
auto obj = Json::requireObject(doc);
auto & out = m_toProcess.files[index];
// result code signifies true failure.
if(obj.contains("code"))
{
qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a negative result:";
qCritical() << bytes;
failed = true;
continue;
}
out.fileName = Json::requireString(obj, "FileNameOnDisk");
QString rawUrl = Json::requireString(obj, "DownloadURL");
out.url = QUrl(rawUrl, QUrl::TolerantMode);
if(!out.url.isValid())
{
throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
}
// This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
// It is also optional
QJsonObject projObj = Json::ensureObject(obj, "_Project", {});
if(!projObj.isEmpty())
{
QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower();
if(strType == "singlefile")
{
out.type = File::Type::SingleFile;
}
else if(strType == "ctoc")
{
out.type = File::Type::Ctoc;
}
else if(strType == "cmod2")
{
out.type = File::Type::Cmod2;
}
else if(strType == "mod")
{
out.type = File::Type::Mod;
}
else if(strType == "folder")
{
out.type = File::Type::Folder;
}
else if(strType == "modpack")
{
out.type = File::Type::Modpack;
}
else
{
qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of unknown file type:" << strType;
out.type = File::Type::Unknown;
failed = true;
continue;
}
out.targetFolder = Json::ensureString(projObj, "Path", "mods");
}
out.resolved = true;
failed &= (!out.parseFromBytes(bytes));
}
catch (const JSONValidationError &e)
{
auto & out = m_toProcess.files[index];
qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
qCritical() << e.cause();
qCritical() << "JSON:";

View File

@@ -64,3 +64,63 @@ void Flame::loadManifest(Flame::Manifest & m, const QString &filepath)
}
loadManifestV1(m, obj);
}
bool Flame::File::parseFromBytes(const QByteArray& bytes)
{
auto doc = Json::requireDocument(bytes);
auto obj = Json::requireObject(doc);
// result code signifies true failure.
if(obj.contains("code"))
{
qCritical() << "Resolving of" << projectId << fileId << "failed because of a negative result:";
qCritical() << bytes;
return false;
}
fileName = Json::requireString(obj, "FileNameOnDisk");
QString rawUrl = Json::requireString(obj, "DownloadURL");
url = QUrl(rawUrl, QUrl::TolerantMode);
if(!url.isValid())
{
throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
}
// This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
// It is also optional
QJsonObject projObj = Json::ensureObject(obj, "_Project", {});
if(!projObj.isEmpty())
{
QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower();
if(strType == "singlefile")
{
type = File::Type::SingleFile;
}
else if(strType == "ctoc")
{
type = File::Type::Ctoc;
}
else if(strType == "cmod2")
{
type = File::Type::Cmod2;
}
else if(strType == "mod")
{
type = File::Type::Mod;
}
else if(strType == "folder")
{
type = File::Type::Folder;
}
else if(strType == "modpack")
{
type = File::Type::Modpack;
}
else
{
qCritical() << "Resolving of" << projectId << fileId << "failed because of unknown file type:" << strType;
type = File::Type::Unknown;
return false;
}
targetFolder = Json::ensureString(projObj, "Path", "mods");
}
resolved = true;
return true;
}

View File

@@ -8,6 +8,9 @@ namespace Flame
{
struct File
{
// NOTE: throws JSONValidationError
bool parseFromBytes(const QByteArray &bytes);
int projectId = 0;
int fileId = 0;
// NOTE: the opposite to 'optional'. This is at the time of writing unused.

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

@@ -36,8 +36,10 @@ QString INIFile::unescape(QString orig)
{
if(c == 'n')
out += '\n';
else if (c == 't')
else if(c == 't')
out += '\t';
else if(c == '#')
out += '#';
else
out += c;
prev = 0;
@@ -67,6 +69,8 @@ QString INIFile::escape(QString orig)
out += "\\t";
else if(c == '\\')
out += "\\\\";
else if(c == '#')
out += "\\#";
else
out += c;
}
@@ -120,7 +124,15 @@ bool INIFile::loadFile(QByteArray file)
{
QString &lineRaw = lines[i];
// Ignore comments.
QString line = lineRaw.left(lineRaw.indexOf('#')).trimmed();
int commentIndex = 0;
QString line = lineRaw;
// Search for comments until no more escaped # are available
while((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) {
if(commentIndex > 0 && line.at(commentIndex - 1) == '\\') {
continue;
}
line = line.left(lineRaw.indexOf('#')).trimmed();
}
int eqPos = line.indexOf('=');
if (eqPos == -1)

View File

@@ -26,6 +26,7 @@ slots:
QTest::newRow("Plain text") << "Lorem ipsum dolor sit amet.";
QTest::newRow("Escape sequences") << "Lorem\n\t\n\\n\\tAAZ\nipsum dolor\n\nsit amet.";
QTest::newRow("Escape sequences 2") << "\"\n\n\"";
QTest::newRow("Hashtags") << "some data#something";
}
void test_Escape()
{
@@ -40,7 +41,7 @@ slots:
void test_SaveLoad()
{
QString a = "a";
QString b = "a\nb\t\n\\\\\\C:\\Program files\\terrible\\name\\of something\\";
QString b = "a\nb\t\n\\\\\\C:\\Program files\\terrible\\name\\of something\\#thisIsNotAComment";
QString filename = "test_SaveLoad.ini";
// save

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

@@ -131,7 +131,14 @@ void DownloadTask::processDownloadedVersionInfo()
QObject::connect(netJob.get(), &NetJob::progress, this, &DownloadTask::fileDownloadProgressChanged);
QObject::connect(netJob.get(), &NetJob::failed, this, &DownloadTask::fileDownloadFailed);
setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size())));
if(netJob->size() == 1) // Translation issues... see https://github.com/MultiMC/MultiMC5/issues/1701
{
setStatus(tr("Downloading one update file."));
}
else
{
setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size())));
}
qDebug() << "Begin downloading update files to" << m_updateFilesDir.path();
m_filesNetJob = netJob;
m_filesNetJob->start();

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
@@ -302,7 +297,7 @@ qt5_add_resources(MULTIMC_RESOURCES ${MULTIMC_QRCS})
# Add executable
add_executable(MultiMC MACOSX_BUNDLE WIN32 ${MULTIMC_SOURCES} ${MULTIMC_UI} ${MULTIMC_RESOURCES} ${MULTIMC_RCS})
target_link_libraries(MultiMC MultiMC_gui ${QUAZIP_LIBRARIES} hoedown MultiMC_rainbow LocalPeer ganalytics)
target_link_libraries(MultiMC MultiMC_gui ${QUAZIP_LIBRARIES} hoedown MultiMC_rainbow LocalPeer ganalytics Qt5::Multimedia)
if(DEFINED MultiMC_APP_BINARY_NAME)
set_target_properties(MultiMC PROPERTIES OUTPUT_NAME "${MultiMC_APP_BINARY_NAME}")
endif()

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
@@ -702,6 +709,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
// model reset -> selection is invalid. All the instance pointers are wrong.
connect(MMC->instances().get(), &InstanceList::dataIsInvalid, this, &MainWindow::selectionBad);
// handle newly added instances
connect(MMC->instances().get(), &InstanceList::instanceSelectRequest, this, &MainWindow::instanceSelectRequest);
// When the global settings page closes, we want to know about it and update our state
connect(MMC, &MultiMC::globalSettingsClosed, this, &MainWindow::globalSettingsClosed);
@@ -825,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");
@@ -1268,7 +1285,7 @@ void MainWindow::downloadUpdates(GoUpdate::Status status)
void MainWindow::onCatToggled(bool state)
{
setCatBackground(state);
setCatBackground(state, false);
MMC->settings()->set("TheCat", state);
}
@@ -1282,8 +1299,9 @@ T non_stupid_abs(T in)
}
}
void MainWindow::setCatBackground(bool enabled)
void MainWindow::setCatBackground(bool enabled, bool initial)
{
static int currentMeow = 0;
if (enabled)
{
QDateTime now = QDateTime::currentDateTime();
@@ -1300,9 +1318,28 @@ GroupView
background-repeat: none;
background-color:palette(base);
})").arg(cat));
if(!initial) {
const char * sounds[] = {
"/home/peterix/meows/Fiona52fixed.wav",
"/home/peterix/meows/Fiona72.wav",
"/home/peterix/meows/Fiona76.wav",
"/home/peterix/meows/Fiona81.wav",
"/home/peterix/meows/Fiona85.wav"
};
qDebug() << "PLAY MEOW" << currentMeow;
meow.setSource(QUrl::fromLocalFile(sounds[currentMeow]));
meow.setLoopCount(1);
meow.play();
int newMeow = qrand() % 4;
if(newMeow == currentMeow) {
newMeow++;
}
currentMeow = newMeow;
}
}
else
{
meow.stop();
view->setStyleSheet(QString());
}
}
@@ -1341,7 +1378,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());
@@ -1651,19 +1688,13 @@ 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)
{
return;
}
auto id = m_selectedInstance->id();
auto response = CustomMessageBox::selectable(
this,
tr("CAREFUL!"),
@@ -1674,7 +1705,7 @@ void MainWindow::on_actionDeleteInstance_triggered()
)->exec();
if (response == QMessageBox::Yes)
{
MMC->instances()->deleteInstance(m_selectedInstance->id());
MMC->instances()->deleteInstance(id);
}
}
@@ -1836,6 +1867,11 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
}
}
void MainWindow::instanceSelectRequest(QString id)
{
setSelectedInstanceById(id);
}
void MainWindow::instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
{
auto current = view->selectionModel()->currentIndex();
@@ -1885,7 +1921,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

@@ -20,6 +20,7 @@
#include <QMainWindow>
#include <QProcess>
#include <QTimer>
#include <QtMultimedia/QSoundEffect>
#include "BaseInstance.h"
#include "minecraft/auth/MojangAccount.h"
@@ -60,6 +61,9 @@ public:
signals:
void isClosing();
protected:
QMenu * createPopupMenu() override;
private slots:
void onCatToggled(bool);
@@ -109,8 +113,6 @@ private slots:
void newsButtonClicked();
void on_mainToolBar_visibilityChanged(bool);
void on_actionLaunchInstance_triggered();
void on_actionLaunchInstanceOffline_triggered();
@@ -152,6 +154,8 @@ private slots:
void instanceChanged(const QModelIndex &current, const QModelIndex &previous);
void instanceSelectRequest(QString id);
void instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
void selectionBad();
@@ -186,7 +190,7 @@ private slots:
private:
void addInstance(QString url = QString());
void activateInstance(InstancePtr instance);
void setCatBackground(bool enabled);
void setCatBackground(bool enabled, bool initial = true);
void updateInstanceToolIcon(QString new_icon);
void setSelectedInstanceById(const QString &id);
@@ -197,6 +201,8 @@ private:
private:
std::unique_ptr<Ui> ui;
QSoundEffect meow;
// these are managed by Qt's memory management model!
GroupView *view = nullptr;
InstanceProxyModel *proxymodel = nullptr;

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"
@@ -153,23 +156,23 @@ 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");
// parse the arguments
try
@@ -378,7 +381,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;
}
@@ -523,7 +526,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>();
@@ -531,6 +533,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"));
@@ -594,12 +600,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);
@@ -941,7 +947,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())
{
@@ -990,7 +996,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

@@ -23,56 +23,43 @@
#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 << "<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 +68,8 @@ static QString getLicenseHtml()
return output;
}
}
AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDialog)
{
ui->setupUi(this);
@@ -109,6 +98,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,10 +34,9 @@
#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)
: QDialog(parent), ui(new Ui::NewInstanceDialog)
@@ -94,6 +93,7 @@ NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString
if(!url.isEmpty())
{
QUrl actualUrl(url);
m_container->selectPage("import");
importPage->setUrl(url);
}
@@ -122,10 +122,8 @@ QList<BasePage *> NewInstanceDialog::getPages()
return
{
new VanillaPage(this),
new FTBPage(this),
importPage,
new TwitchPage(this),
new TechnicPage(this)
new LegacyFTB::Page(this),
};
}

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();
}
@@ -220,6 +238,8 @@ VisualGroup *GroupView::categoryAt(const QPoint &pos, VisualGroup::HitResults &
QString GroupView::groupNameAt(const QPoint &point)
{
executeDelayedItemsLayout();
VisualGroup::HitResults hitresult;
auto group = categoryAt(point + offset(), hitresult);
if(group && (hitresult & (VisualGroup::HeaderHit | VisualGroup::BodyHit)))
@@ -246,7 +266,7 @@ int GroupView::itemWidth() const
void GroupView::mousePressEvent(QMouseEvent *event)
{
// endCategoryEditor();
executeDelayedItemsLayout();
QPoint visualPos = event->pos();
QPoint geometryPos = event->pos() + offset();
@@ -295,6 +315,8 @@ void GroupView::mousePressEvent(QMouseEvent *event)
void GroupView::mouseMoveEvent(QMouseEvent *event)
{
executeDelayedItemsLayout();
QPoint topLeft;
QPoint visualPos = event->pos();
QPoint geometryPos = event->pos() + offset();
@@ -351,6 +373,8 @@ void GroupView::mouseMoveEvent(QMouseEvent *event)
void GroupView::mouseReleaseEvent(QMouseEvent *event)
{
executeDelayedItemsLayout();
QPoint visualPos = event->pos();
QPoint geometryPos = event->pos() + offset();
QPersistentModelIndex index = indexAt(visualPos);
@@ -365,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;
}
}
@@ -405,6 +437,8 @@ void GroupView::mouseReleaseEvent(QMouseEvent *event)
void GroupView::mouseDoubleClickEvent(QMouseEvent *event)
{
executeDelayedItemsLayout();
QModelIndex index = indexAt(event->pos());
if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index))
{
@@ -528,6 +562,8 @@ void GroupView::resizeEvent(QResizeEvent *event)
void GroupView::dragEnterEvent(QDragEnterEvent *event)
{
executeDelayedItemsLayout();
if (!isDragEventAccepted(event))
{
return;
@@ -539,6 +575,8 @@ void GroupView::dragEnterEvent(QDragEnterEvent *event)
void GroupView::dragMoveEvent(QDragMoveEvent *event)
{
executeDelayedItemsLayout();
if (!isDragEventAccepted(event))
{
return;
@@ -550,12 +588,16 @@ void GroupView::dragMoveEvent(QDragMoveEvent *event)
void GroupView::dragLeaveEvent(QDragLeaveEvent *event)
{
executeDelayedItemsLayout();
m_lastDragPosition = QPoint();
viewport()->update();
}
void GroupView::dropEvent(QDropEvent *event)
{
executeDelayedItemsLayout();
m_lastDragPosition = QPoint();
stopAutoScroll();
@@ -578,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();
}
@@ -606,6 +647,8 @@ void GroupView::dropEvent(QDropEvent *event)
void GroupView::startDrag(Qt::DropActions supportedActions)
{
executeDelayedItemsLayout();
QModelIndexList indexes = selectionModel()->selectedIndexes();
if(indexes.count() == 0)
return;
@@ -651,11 +694,15 @@ void GroupView::startDrag(Qt::DropActions supportedActions)
QRect GroupView::visualRect(const QModelIndex &index) const
{
const_cast<GroupView*>(this)->executeDelayedItemsLayout();
return geometryRect(index).translated(-offset());
}
QRect GroupView::geometryRect(const QModelIndex &index) const
{
const_cast<GroupView*>(this)->executeDelayedItemsLayout();
if (!index.isValid() || isIndexHidden(index) || index.column() > 0)
{
return QRect();
@@ -695,9 +742,10 @@ QModelIndex GroupView::indexAt(const QPoint &point) const
return QModelIndex();
}
void GroupView::setSelection(const QRect &rect,
const QItemSelectionModel::SelectionFlags commands)
void GroupView::setSelection(const QRect &rect, const QItemSelectionModel::SelectionFlags commands)
{
executeDelayedItemsLayout();
for (int i = 0; i < model()->rowCount(); ++i)
{
QModelIndex index = model()->index(i, 0);
@@ -732,8 +780,7 @@ QPixmap GroupView::renderToPixmap(const QModelIndexList &indices, QRect *r) cons
return pixmap;
}
QList<QPair<QRect, QModelIndex>> GroupView::draggablePaintPairs(const QModelIndexList &indices,
QRect *r) const
QList<QPair<QRect, QModelIndex>> GroupView::draggablePaintPairs(const QModelIndexList &indices, QRect *r) const
{
Q_ASSERT(r);
QRect &rect = *r;

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,7 +29,8 @@ int main(int argc, char *argv[])
#endif
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif
// initialize Qt
@@ -42,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,11 +1,11 @@
Package: multimc
Version: 1.2-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
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

@@ -4,9 +4,9 @@ A simple ubuntu package for MultiMC that wraps the contains a script that downlo
It contains a `.desktop` file, an icon, and a simple script that does the heavy lifting.
# How to build this?
You need dpkg utils. Rename the `multimc` folder to `multimc_1.2-1` and then run:
You need dpkg utils. Rename the `multimc` folder to `multimc_1.3-1` and then run:
```
fakeroot dpkg-deb --build multimc_1.2-1
fakeroot dpkg-deb --build multimc_1.3-1
```
Replace the version with whatever is appropriate.

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>
@@ -34,24 +35,31 @@
#include "MultiMC.h"
AccountListPage::AccountListPage(QWidget *parent)
: QWidget(parent), ui(new Ui::AccountListPage)
: QMainWindow(parent), ui(new Ui::AccountListPage)
{
ui->setupUi(this);
ui->tabWidget->tabBar()->hide();
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()));
@@ -64,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)
@@ -85,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)
@@ -97,7 +128,7 @@ void AccountListPage::on_setDefaultBtn_clicked()
}
}
void AccountListPage::on_noDefaultBtn_clicked()
void AccountListPage::on_actionNoDefault_triggered()
{
m_accounts->setActiveAccount("");
}
@@ -107,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)
@@ -140,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)

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:
@@ -59,17 +59,21 @@ public:
return "Getting-Started#adding-an-account";
}
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();
@@ -81,6 +85,7 @@ protected:
protected
slots:
void ShowContextMenu(const QPoint &pos);
void addAccount(const QString& errMsg="");
private:

View File

@@ -1,122 +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>694</width>
<height>609</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="tab">
<attribute name="title">
<string notr="true">Tab 1</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>
</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/>
</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);
}

View File

@@ -1,57 +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.
*/
#pragma once
#include <QWidget>
#include "pages/BasePage.h"
namespace Ui {
class PackagesPage;
}
class QSortFilterProxyModel;
class VersionProxyModel;
class PackagesPage : public QWidget, public BasePage
{
Q_OBJECT
public:
explicit PackagesPage(QWidget *parent = 0);
~PackagesPage();
QString id() const override { return "packages-global"; }
QString displayName() const override { return tr("Packages"); }
QIcon icon() const override;
void openedImpl() override;
private slots:
void on_refreshIndexBtn_clicked();
void on_refreshFileBtn_clicked();
void on_refreshVersionBtn_clicked();
void on_fileSearchEdit_textChanged(const QString &search);
void on_versionSearchEdit_textChanged(const QString &search);
void updateCurrentVersionList(const QModelIndex &index);
void versionListDataChanged(const QModelIndex &tl, const QModelIndex &br);
private:
Ui::PackagesPage *ui;
QSortFilterProxyModel *m_fileProxy;
QSortFilterProxyModel *m_filterProxy;
VersionProxyModel *m_versionProxy;
void updateVersion();
};

View File

@@ -1,252 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PackagesPage</class>
<widget class="QWidget" name="PackagesPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>636</width>
<height>621</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<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="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Tab 1</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="2">
<widget class="QGroupBox" name="versionsBox">
<property name="title">
<string>Versions</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLineEdit" name="versionSearchEdit">
<property name="placeholderText">
<string>Search...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTreeView" name="versionsView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="refreshVersionBtn">
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="versionVersionLabel">
<property name="text">
<string>Version:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="versionVersion">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="versionTimeLabel">
<property name="text">
<string>Time:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="versionTime">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="versionTypeLabel">
<property name="text">
<string>Type:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="versionType">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="versionRequiresLabel">
<property name="text">
<string>Dependencies:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="versionRequires">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<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>
</item>
<item row="1" column="1">
<widget class="QGroupBox" name="versionListsBox">
<property name="title">
<string>Resources</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLineEdit" name="fileSearchEdit">
<property name="placeholderText">
<string>Search...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTreeView" name="indexView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="refreshFileBtn">
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="fileUidLabel">
<property name="text">
<string>UID:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="fileUid">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="fileNameLabel">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="fileName">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</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>
</item>
<item row="0" column="1" colspan="2">
<widget class="QPushButton" name="refreshIndexBtn">
<property name="text">
<string>Refresh Index</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -20,6 +20,7 @@
#include "settings/SettingsObject.h"
#include "MultiMC.h"
#include "Env.h"
ProxyPage::ProxyPage(QWidget *parent) : QWidget(parent), ui(new Ui::ProxyPage)
{
@@ -75,6 +76,9 @@ void ProxyPage::applySettings()
s->set("ProxyPort", ui->proxyPortEdit->value());
s->set("ProxyUser", ui->proxyUserEdit->text());
s->set("ProxyPass", ui->proxyPassEdit->text());
ENV.updateProxySettings(proxyType, ui->proxyAddrEdit->text(), ui->proxyPortEdit->value(),
ui->proxyUserEdit->text(), ui->proxyPassEdit->text());
}
void ProxyPage::loadSettings()
{

View File

@@ -11,6 +11,18 @@
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<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="QCommandLinkButton" name="openGlobalJavaSettingsButton">
<property name="text">

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