From d8f20d2e667ee4104fb6c4b9e5e23fce73f44b5a Mon Sep 17 00:00:00 2001 From: pvdk Date: Sat, 7 Dec 2013 16:17:07 +0100 Subject: [PATCH 01/69] Added the infrastructure to start working on the installation wizard --- CMakeLists.txt | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f6014dff6..f5e2d0a81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ option(BUILD_ESMTOOL "build ESM inspector" ON) option(BUILD_LAUNCHER "build Launcher" ON) option(BUILD_MWINIIMPORTER "build MWiniImporter" ON) option(BUILD_OPENCS "build OpenMW Construction Set" ON) +option(BUILD_WIZARD "build Installation Wizard" ON) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest ang GMock frameworks" OFF) @@ -384,6 +385,10 @@ IF(NOT WIN32 AND NOT APPLE) IF(BUILD_OPENCS) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/opencs" DESTINATION "${BINDIR}" ) ENDIF(BUILD_OPENCS) + IF(BUILD_WIZARD) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/wizard" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_WIZARD) + # Install licenses INSTALL(FILES "DejaVu Font License.txt" DESTINATION "${LICDIR}" ) @@ -434,7 +439,7 @@ IF(NOT WIN32 AND NOT APPLE) Data files from the original game is required to run it.") SET(CPACK_DEBIAN_PACKAGE_NAME "openmw") SET(CPACK_DEBIAN_PACKAGE_VERSION "${VERSION_STRING}") - SET(CPACK_PACKAGE_EXECUTABLES "openmw;OpenMW opencs;OpenCS bsatool;Bsatool esmtool;Esmtool omwlauncher;OMWLauncher mwiniimporter;MWiniImporter") + SET(CPACK_PACKAGE_EXECUTABLES "openmw;OpenMW opencs;OpenCS bsatool;Bsatool esmtool;Esmtool omwlauncher;OMWLauncher mwiniimporter;MWiniImporter openmw-wizard;OpenMW Wizard") SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.11.2), libfreetype6 (>= 2.2.1), libgcc1 (>= 1:4.1.1), libmpg123-0 (>= 1.12.1), libopenal1 (>= 1:1.12.854), libsndfile1 (>= 1.0.23), libstdc++6 (>= 4.4.5), libuuid1 (>= 2.17.2), libqtgui4 (>= 4.7.0)") SET(CPACK_DEBIAN_PACKAGE_SECTION "Games") @@ -476,6 +481,10 @@ if(WIN32) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/opencs.exe" DESTINATION ".") INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.cfg" DESTINATION ".") ENDIF(BUILD_OPENCS) + IF(BUILD_WIZARD) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/openmw-wizard.exe" DESTINATION ".") + ENDIF(BUILD_WIZARD) + INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION ".") @@ -493,6 +502,9 @@ if(WIN32) IF(BUILD_OPENCS) SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};opencs;OpenMW Construction Set") ENDIF(BUILD_OPENCS) + IF(BUILD_WIZARD) + SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};openmw-wizard;OpenMW Wizard") + ENDIF(BUILD_WIZARD) SET(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\Readme.lnk' '\$INSTDIR\\\\readme.txt'") SET(CPACK_NSIS_DELETE_ICONS_EXTRA " !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP @@ -573,6 +585,10 @@ if (BUILD_OPENCS) add_subdirectory (apps/opencs) endif() +if (BUILD_WIZARD) + add_subdirectory(apps/wizard) +endif() + # UnitTests if (BUILD_UNITTESTS) add_subdirectory( apps/openmw_test_suite ) @@ -649,6 +665,9 @@ if (WIN32) if (BUILD_ESMTOOL) set_target_properties(esmtool PROPERTIES COMPILE_FLAGS ${WARNINGS}) endif (BUILD_ESMTOOL) + if (BUILD_WIZARD) + set_target_properties(openmw-wizard PROPERTIES COMPILE_FLAGS ${WARNINGS}) + endif (BUILD_WIZARD) endif(MSVC) # Same for MinGW From f458f0c6c16d13bac0d45ccd7ed972d92cfed870 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sat, 7 Dec 2013 23:17:50 +0100 Subject: [PATCH 02/69] Added the Wizard .ui/resource files and some basic source to get started --- apps/wizard/CMakeLists.txt | 96 +++++++++++ apps/wizard/main.cpp | 51 ++++++ apps/wizard/wizard.cpp | 1 + apps/wizard/wizard.hpp | 1 + files/ui/wizard/componentselectionpage.ui | 60 +++++++ files/ui/wizard/conclusionpage.ui | 36 ++++ files/ui/wizard/configurationpage.ui | 58 +++++++ files/ui/wizard/existinginstallationpage.ui | 85 ++++++++++ files/ui/wizard/importpage.ui | 67 ++++++++ files/ui/wizard/installationpage.ui | 51 ++++++ files/ui/wizard/installationtargetpage.ui | 99 +++++++++++ files/ui/wizard/intropage.ui | 34 ++++ files/ui/wizard/methodselectionpage.ui | 156 ++++++++++++++++++ files/wizard/icons/tango/48x48/folder.png | Bin 0 -> 1610 bytes .../icons/tango/48x48/system-installer.png | Bin 0 -> 2663 bytes files/wizard/icons/tango/index.theme | 8 + files/wizard/wizard.qrc | 7 + 17 files changed, 810 insertions(+) create mode 100644 apps/wizard/CMakeLists.txt create mode 100644 apps/wizard/main.cpp create mode 100644 apps/wizard/wizard.cpp create mode 100644 apps/wizard/wizard.hpp create mode 100644 files/ui/wizard/componentselectionpage.ui create mode 100644 files/ui/wizard/conclusionpage.ui create mode 100644 files/ui/wizard/configurationpage.ui create mode 100644 files/ui/wizard/existinginstallationpage.ui create mode 100644 files/ui/wizard/importpage.ui create mode 100644 files/ui/wizard/installationpage.ui create mode 100644 files/ui/wizard/installationtargetpage.ui create mode 100644 files/ui/wizard/intropage.ui create mode 100644 files/ui/wizard/methodselectionpage.ui create mode 100644 files/wizard/icons/tango/48x48/folder.png create mode 100644 files/wizard/icons/tango/48x48/system-installer.png create mode 100644 files/wizard/icons/tango/index.theme create mode 100644 files/wizard/wizard.qrc diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt new file mode 100644 index 000000000..d6423daad --- /dev/null +++ b/apps/wizard/CMakeLists.txt @@ -0,0 +1,96 @@ +set(WIZARD + main.cpp + wizard.cpp + +) +# if(NOT WIN32) +# LIST(APPEND WIZARD unshieldthread.cpp) +# endif(NOT WIN32) + +set(WIZARD_HEADER + wizard.hpp +) +# if(NOT WIN32) +# LIST(APPEND WIZARD_HEADER unshieldthread.hpp) +# endif(NOT WIN32) + + +# Headers that must be pre-processed +set(WIZARD_HEADER_MOC + wizard.hpp +) + +# if(NOT WIN32) +# LIST(APPEND WIZARD_HEADER_MOC unshieldthread.hpp) +# endif(NOT WIN32) + + +set(WIZARD_UI + ${CMAKE_SOURCE_DIR}/files/ui/wizard/componentselectionpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/configurationpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/existinginstallationpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/importpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationtargetpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/intropage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/methodselectionpage.ui +) + +source_group(wizard FILES ${WIZARD} ${WIZARD_HEADER}) + +find_package(Qt4 REQUIRED) +set(QT_USE_QTGUI 1) + +# Set some platform specific settings +if(WIN32) + set(GUI_TYPE WIN32) + set(QT_USE_QTMAIN TRUE) +endif(WIN32) + +QT4_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/wizard/wizard.qrc) +QT4_WRAP_CPP(MOC_SRCS ${WIZARD_HEADER_MOC}) +QT4_WRAP_UI(UI_HDRS ${WIZARD_UI}) + + +include(${QT_USE_FILE}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +if(NOT WIN32) + include_directories(${LIBUNSHIELD_INCLUDE}) +endif(NOT WIN32) + +add_executable(openmw-wizard + ${GUI_TYPE} + ${WIZARD} + ${WIZARD_HEADER} + ${RCC_SRCS} + ${MOC_SRCS} + ${UI_HDRS} +) + +target_link_libraries(openmw-wizard +# ${Boost_LIBRARIES} + ${QT_LIBRARIES} +) +if(NOT WIN32) + target_link_libraries(openmw-wizard + ${LIBUNSHIELD_LIBRARY} + ) +endif(NOT WIN32) + + + +if(DPKG_PROGRAM) + INSTALL(TARGETS openmw-wizard RUNTIME DESTINATION games COMPONENT openmw-wizard) +endif() + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions (--coverage) + target_link_libraries(openmw-wizard gcov) +endif() + +# Workaround for binutil => 2.23 problem when linking, should be fixed eventually upstream +if (UNIX AND NOT APPLE) +target_link_libraries(openmw-wizard dl Xt) +endif() + diff --git a/apps/wizard/main.cpp b/apps/wizard/main.cpp new file mode 100644 index 000000000..ed3f3a6ea --- /dev/null +++ b/apps/wizard/main.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +#include + +#ifdef MAC_OS_X_VERSION_MIN_REQUIRED +#undef MAC_OS_X_VERSION_MIN_REQUIRED +// We need to do this because of Qt: https://bugreports.qt-project.org/browse/QTBUG-22154 +#define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ +#endif // MAC_OS_X_VERSION_MIN_REQUIRED + +int main(int argc, char *argv[]) +{ + + QApplication app(argc, argv); + + // Now we make sure the current dir is set to application path + QDir dir(QCoreApplication::applicationDirPath()); + + #ifdef Q_OS_MAC + if (dir.dirName() == "MacOS") { + dir.cdUp(); + dir.cdUp(); + dir.cdUp(); + } + + // force Qt to load only LOCAL plugins, don't touch system Qt installation + QDir pluginsPath(QCoreApplication::applicationDirPath()); + pluginsPath.cdUp(); + pluginsPath.cd("Plugins"); + + QStringList libraryPaths; + libraryPaths << pluginsPath.path() << QCoreApplication::applicationDirPath(); + app.setLibraryPaths(libraryPaths); + #endif + + QDir::setCurrent(dir.absolutePath()); + + // Support non-latin characters + QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); + +// Launcher::MainDialog mainWin; + +// if (mainWin.setup()) { +// mainWin.show(); +// } else { +// return 0; +// } + + return app.exec(); +} diff --git a/apps/wizard/wizard.cpp b/apps/wizard/wizard.cpp new file mode 100644 index 000000000..8d1c8b69c --- /dev/null +++ b/apps/wizard/wizard.cpp @@ -0,0 +1 @@ + diff --git a/apps/wizard/wizard.hpp b/apps/wizard/wizard.hpp new file mode 100644 index 000000000..8d1c8b69c --- /dev/null +++ b/apps/wizard/wizard.hpp @@ -0,0 +1 @@ + diff --git a/files/ui/wizard/componentselectionpage.ui b/files/ui/wizard/componentselectionpage.ui new file mode 100644 index 000000000..c3333752a --- /dev/null +++ b/files/ui/wizard/componentselectionpage.ui @@ -0,0 +1,60 @@ + + + ComponentSelectionPage + + + + 0 + 0 + 454 + 393 + + + + WizardPage + + + Select Components + + + Which components should be installed? + + + + + + <html><head/><body><p>Select which official Morrowind expansions should be installed. For best results, it is recommended to have both expansions installed.</p><p><span style=" font-weight:bold;">Note:</span> It is possible to install expansions later by re-running this Wizard.<br/></p></body></html> + + + true + + + + + + + Selected components: + + + + + + + + + + Qt::Vertical + + + + 20 + 81 + + + + + + + + + diff --git a/files/ui/wizard/conclusionpage.ui b/files/ui/wizard/conclusionpage.ui new file mode 100644 index 000000000..bedad3e2a --- /dev/null +++ b/files/ui/wizard/conclusionpage.ui @@ -0,0 +1,36 @@ + + + ConclusionPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Completing the OpenMW Wizard + + + + + + The OpenMW Wizard successfully installed Morrowind on your computer. + +Click Finish to close the Wizard. + + + true + + + + + + + + diff --git a/files/ui/wizard/configurationpage.ui b/files/ui/wizard/configurationpage.ui new file mode 100644 index 000000000..8340b2ebf --- /dev/null +++ b/files/ui/wizard/configurationpage.ui @@ -0,0 +1,58 @@ + + + WizardPage + + + + 0 + 0 + 440 + 300 + + + + WizardPage + + + + + + <html><head/><body><p>OpenMW needs to import settings from the Morrowind configuration file in order to function properly. Select the steps you want to be taken and click Next when you are ready to continue.</p><p><span style=" font-weight:600;">Note:</span> it is possible to import settings later, by re-running this Wizard.</p><p/></body></html> + + + true + + + + + + + Import settings from Morrowind.ini + + + + + + + Import selected add-ons (creates a new Cotent List in the launcher) + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/files/ui/wizard/existinginstallationpage.ui b/files/ui/wizard/existinginstallationpage.ui new file mode 100644 index 000000000..579ddbe67 --- /dev/null +++ b/files/ui/wizard/existinginstallationpage.ui @@ -0,0 +1,85 @@ + + + ExistingInstallationPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Select Existing Installation + + + Select an existing installation for OpenMW to use or modify. + + + + + + Detected Installations: + + + + + + + + No existing installations detected + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Browse... + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/files/ui/wizard/importpage.ui b/files/ui/wizard/importpage.ui new file mode 100644 index 000000000..21037f3e8 --- /dev/null +++ b/files/ui/wizard/importpage.ui @@ -0,0 +1,67 @@ + + + ImportPage + + + + 0 + 0 + 513 + 328 + + + + WizardPage + + + Import Settings + + + Import settings from the Morrowind installation. + + + + + + <html><head/><body><p>OpenMW needs to import settings from the Morrowind configuration file in order to function properly.</p><p><span style=" font-weight:bold;">Note:</span> It is possible to import settings later by re-running this Wizard.</p><p/></body></html> + + + true + + + + + + + Import settings from Morrowind.ini + + + true + + + + + + + Import previously selected add-ons (creates a new Content List in the launcher) + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/files/ui/wizard/installationpage.ui b/files/ui/wizard/installationpage.ui new file mode 100644 index 000000000..3e94c1e30 --- /dev/null +++ b/files/ui/wizard/installationpage.ui @@ -0,0 +1,51 @@ + + + InstallationPage + + + + 0 + 0 + 518 + 423 + + + + WizardPage + + + Installing + + + Please wait while Morrowind is installed on your computer. + + + + + + Extracting: %1 + + + + + + + 24 + + + + + + + false + + + true + + + + + + + + diff --git a/files/ui/wizard/installationtargetpage.ui b/files/ui/wizard/installationtargetpage.ui new file mode 100644 index 000000000..6bdac5592 --- /dev/null +++ b/files/ui/wizard/installationtargetpage.ui @@ -0,0 +1,99 @@ + + + InstallationTargetPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Select Installation Destination + + + Where should Morrowind be installed? + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Morrowind will be installed to the following location. + + + true + + + + + + + + + Click Next to continue. If you would like to select a different location, click Browse. + + + true + + + + + + + + + + + + Browse... + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/files/ui/wizard/intropage.ui b/files/ui/wizard/intropage.ui new file mode 100644 index 000000000..bb597e3a6 --- /dev/null +++ b/files/ui/wizard/intropage.ui @@ -0,0 +1,34 @@ + + + IntroPage + + + + 0 + 0 + 474 + 370 + + + + WizardPage + + + Welcome to the OpenMW Wizard + + + + + + This Wizard will help you install Morrowind and its add-ons for OpenMW to use. + + + true + + + + + + + + diff --git a/files/ui/wizard/methodselectionpage.ui b/files/ui/wizard/methodselectionpage.ui new file mode 100644 index 000000000..e77349f5c --- /dev/null +++ b/files/ui/wizard/methodselectionpage.ui @@ -0,0 +1,156 @@ + + + MethodSelectionPage + + + + 0 + 0 + 398 + 298 + + + + WizardPage + + + Select Installation Method + + + Select how OpenMW should get the required Morrowind installation files. + + + + + + font-weight:bold; + + + Install to a new location + + + true + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><img src=":/icons/tango/48x48/system-installer.png"/></p></body></html> + + + Qt::RichText + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + newLocationRadioButton + + + + + + + Install Morrowind to a new location for OpenMW to use. + + + true + + + + + + + + + font-weight:bold + + + Select an existing installation + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Select an existing Morrowind installation for OpenMW to use. + + + true + + + + + + + + + + + + diff --git a/files/wizard/icons/tango/48x48/folder.png b/files/wizard/icons/tango/48x48/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..e93d7cf8f8460c1576f40b6981d42181a1e1b38f GIT binary patch literal 1610 zcmV-Q2DSN#P)$Qp1=mSL zK~!ko?U~DqWJwjqe-ZbQkIIKN(@NvQh5aXpOL5^W8pPGW;L?rD?gl|-(F>P0xaw}) z%tG)F(4~zi!fZqvM22Bt#Dy>vgWXnERc7A2_r~+M=zDXrvZ^byYUoOoIuPW^xWwiA z;yWkeL|nMB8@sXpX;Z6z9zJ~d6Q$JqB9a0qr99voKPaVa<42{GSs7XZgb z=+U39i$J>iM?`*h_wL%jxO;fG4 ze)sy z2wSPJ9#Xe&-yZdPJ@)ta>G%6+tx-y$)Tu>}hSc&|2!UtMo_+7$y?eiY`t<4S(gZ>X z)>_N)@i9k7N5mK}rNU|`0UR70EJ3#*+orW%5^McD&jCVF6k}j^6$NVN9HY@_Is2;w zHmR^qpatIkt}4;`Ns=rHXsr>En5Jp(sv%T+@0rbJ93CDn0l!X#^(0;gze=JdvOepp zewJkb(HPUcDuK#5$8b2rT1yCl-~H;Go+S#O ze{<;b?2q4l@B_R49_PcSfBxkU-?=CUDr+r8Q81ZI2q7@7Z}HO~eTQK$yROYu17BV5 zENjotfA!(pTPZLY45+FK<07Ew zKBeE)2(mNq08xa>c(i76o?NR6l&u1z(TKD=V$|>8{qDf~ASk7%YKId2;z9zg0!2}9 ze0n3j%yzk|Ex5Ng54D9!S!DJ$zlc)2NwYin(66;NTo zpx432cRPCIjEK81NHQh{MLuh97RlNEjXI61woCSctsXCWKs? zrrH?8yw;>|=Tt79jd|G?sBG?)&E<4iJG?h9HxcL_92|7-+_{rvS%&KLQAvuife_@& z&FCdM*B_e@S(Mhfay|z}Kmo*c0y(fZola8`NsKX6Zop6!#%)esx%7kWLrk2oFvh!j zRsn-%==?kao%wvOVvI?aWq933rwL(qhq2{x($prH*^3F#0Su6}-!1f74v11J{JTgf zLr$;X-7R{7>B(G7ZNuk(|LsT3{fk=tFw-jFn`VtMAN=*jTR#q(?30iG$<8hMmoGp0 z!~50bVAfm~y9VnQ*Gi2IfIXlO^cuO6MkTe2^L%^K1!|ydTG0?WLja(=0zPdBBs(Gy zn$^ZPfK5XnoVnOLuZC(8MX&D_*BLww*wCmT+}MrX*#DpX7iXTUrIOxbGynhq07*qo IM6N<$f?B=}0ssI2 literal 0 HcmV?d00001 diff --git a/files/wizard/icons/tango/48x48/system-installer.png b/files/wizard/icons/tango/48x48/system-installer.png new file mode 100644 index 0000000000000000000000000000000000000000..dbd08f7e2f5d40f313206326b379ccc467c139c1 GIT binary patch literal 2663 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 + + icons/tango/index.theme + icons/tango/48x48/folder.png + icons/tango/48x48/system-installer.png + + From 8162f8370b21d4b666693a3260fdd5cbc1bb6b03 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 8 Dec 2013 21:35:57 +0100 Subject: [PATCH 03/69] Added all the pages to the wizard --- apps/wizard/CMakeLists.txt | 32 +++++++++++-- apps/wizard/componentselectionpage.cpp | 7 +++ apps/wizard/componentselectionpage.hpp | 21 ++++++++ apps/wizard/conclusionpage.cpp | 7 +++ apps/wizard/conclusionpage.hpp | 21 ++++++++ apps/wizard/existinginstallationpage.cpp | 7 +++ apps/wizard/existinginstallationpage.hpp | 21 ++++++++ apps/wizard/importpage.cpp | 7 +++ apps/wizard/importpage.hpp | 21 ++++++++ apps/wizard/installationpage.cpp | 7 +++ apps/wizard/installationpage.hpp | 21 ++++++++ apps/wizard/installationtargetpage.cpp | 7 +++ apps/wizard/installationtargetpage.hpp | 21 ++++++++ apps/wizard/intropage.cpp | 7 +++ apps/wizard/intropage.hpp | 21 ++++++++ apps/wizard/main.cpp | 11 ++--- apps/wizard/mainwizard.cpp | 36 ++++++++++++++ apps/wizard/mainwizard.hpp | 33 +++++++++++++ apps/wizard/methodselectionpage.cpp | 7 +++ apps/wizard/methodselectionpage.hpp | 21 ++++++++ apps/wizard/wizard.cpp | 1 - apps/wizard/wizard.hpp | 1 - files/ui/wizard/configurationpage.ui | 58 ----------------------- files/ui/wizard/installationtargetpage.ui | 10 ---- 24 files changed, 324 insertions(+), 82 deletions(-) create mode 100644 apps/wizard/componentselectionpage.cpp create mode 100644 apps/wizard/componentselectionpage.hpp create mode 100644 apps/wizard/conclusionpage.cpp create mode 100644 apps/wizard/conclusionpage.hpp create mode 100644 apps/wizard/existinginstallationpage.cpp create mode 100644 apps/wizard/existinginstallationpage.hpp create mode 100644 apps/wizard/importpage.cpp create mode 100644 apps/wizard/importpage.hpp create mode 100644 apps/wizard/installationpage.cpp create mode 100644 apps/wizard/installationpage.hpp create mode 100644 apps/wizard/installationtargetpage.cpp create mode 100644 apps/wizard/installationtargetpage.hpp create mode 100644 apps/wizard/intropage.cpp create mode 100644 apps/wizard/intropage.hpp create mode 100644 apps/wizard/mainwizard.cpp create mode 100644 apps/wizard/mainwizard.hpp create mode 100644 apps/wizard/methodselectionpage.cpp create mode 100644 apps/wizard/methodselectionpage.hpp delete mode 100644 apps/wizard/wizard.cpp delete mode 100644 apps/wizard/wizard.hpp delete mode 100644 files/ui/wizard/configurationpage.ui diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index d6423daad..3ff41c206 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -1,14 +1,29 @@ set(WIZARD + componentselectionpage.cpp + conclusionpage.cpp + existinginstallationpage.cpp + importpage.cpp + installationpage.cpp + installationtargetpage.cpp + intropage.cpp main.cpp - wizard.cpp - + mainwizard.cpp + methodselectionpage.cpp ) # if(NOT WIN32) # LIST(APPEND WIZARD unshieldthread.cpp) # endif(NOT WIN32) set(WIZARD_HEADER - wizard.hpp + componentselectionpage.hpp + conclusionpage.hpp + existinginstallationpage.hpp + importpage.hpp + installationpage.hpp + installationtargetpage.hpp + intropage.hpp + mainwizard.hpp + methodselectionpage.hpp ) # if(NOT WIN32) # LIST(APPEND WIZARD_HEADER unshieldthread.hpp) @@ -17,7 +32,15 @@ set(WIZARD_HEADER # Headers that must be pre-processed set(WIZARD_HEADER_MOC - wizard.hpp + componentselectionpage.hpp + conclusionpage.hpp + existinginstallationpage.hpp + importpage.hpp + installationpage.hpp + installationtargetpage.hpp + intropage.hpp + mainwizard.hpp + methodselectionpage.hpp ) # if(NOT WIN32) @@ -28,7 +51,6 @@ set(WIZARD_HEADER_MOC set(WIZARD_UI ${CMAKE_SOURCE_DIR}/files/ui/wizard/componentselectionpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/wizard/configurationpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/existinginstallationpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/importpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp new file mode 100644 index 000000000..dbfa9412e --- /dev/null +++ b/apps/wizard/componentselectionpage.cpp @@ -0,0 +1,7 @@ +#include "componentselectionpage.hpp" + +Wizard::ComponentSelectionPage::ComponentSelectionPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp new file mode 100644 index 000000000..cc36ee485 --- /dev/null +++ b/apps/wizard/componentselectionpage.hpp @@ -0,0 +1,21 @@ +#ifndef COMPONENTSELECTIONPAGE_HPP +#define COMPONENTSELECTIONPAGE_HPP + +#include + +#include "ui_componentselectionpage.h" + +namespace Wizard +{ + + class ComponentSelectionPage : public QWizardPage, private Ui::ComponentSelectionPage + { + Q_OBJECT + public: + ComponentSelectionPage(QWidget *parent = 0); + + }; + +} + +#endif // COMPONENTSELECTIONPAGE_HPP diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp new file mode 100644 index 000000000..2913e431d --- /dev/null +++ b/apps/wizard/conclusionpage.cpp @@ -0,0 +1,7 @@ +#include "conclusionpage.hpp" + +Wizard::ConclusionPage::ConclusionPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/conclusionpage.hpp b/apps/wizard/conclusionpage.hpp new file mode 100644 index 000000000..37c477b44 --- /dev/null +++ b/apps/wizard/conclusionpage.hpp @@ -0,0 +1,21 @@ +#ifndef CONCLUSIONPAGE_HPP +#define CONCLUSIONPAGE_HPP + +#include + +#include "ui_conclusionpage.h" + +namespace Wizard +{ + + class ConclusionPage : public QWizardPage, private Ui::ConclusionPage + { + Q_OBJECT + public: + ConclusionPage(QWidget *parent = 0); + + }; + +} + +#endif // CONCLUSIONPAGE_HPP diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp new file mode 100644 index 000000000..ad540de73 --- /dev/null +++ b/apps/wizard/existinginstallationpage.cpp @@ -0,0 +1,7 @@ +#include "existinginstallationpage.hpp" + +Wizard::ExistingInstallationPage::ExistingInstallationPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp new file mode 100644 index 000000000..5048a14d4 --- /dev/null +++ b/apps/wizard/existinginstallationpage.hpp @@ -0,0 +1,21 @@ +#ifndef EXISTINGINSTALLATIONPAGE_HPP +#define EXISTINGINSTALLATIONPAGE_HPP + +#include + +#include "ui_existinginstallationpage.h" + +namespace Wizard +{ + + class ExistingInstallationPage : public QWizardPage, private Ui::ExistingInstallationPage + { + Q_OBJECT + public: + ExistingInstallationPage(QWidget *parent = 0); + + }; + +} + +#endif // EXISTINGINSTALLATIONPAGE_HPP diff --git a/apps/wizard/importpage.cpp b/apps/wizard/importpage.cpp new file mode 100644 index 000000000..2ac92efd0 --- /dev/null +++ b/apps/wizard/importpage.cpp @@ -0,0 +1,7 @@ +#include "importpage.hpp" + +Wizard::ImportPage::ImportPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/importpage.hpp b/apps/wizard/importpage.hpp new file mode 100644 index 000000000..e3917c837 --- /dev/null +++ b/apps/wizard/importpage.hpp @@ -0,0 +1,21 @@ +#ifndef IMPORTPAGE_HPP +#define IMPORTPAGE_HPP + +#include + +#include "ui_importpage.h" + +namespace Wizard +{ + + class ImportPage : public QWizardPage, private Ui::ImportPage + { + Q_OBJECT + public: + ImportPage(QWidget *parent = 0); + + }; + +} + +#endif // IMPORTPAGE_HPP diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp new file mode 100644 index 000000000..20737ebfb --- /dev/null +++ b/apps/wizard/installationpage.cpp @@ -0,0 +1,7 @@ +#include "installationpage.hpp" + +Wizard::InstallationPage::InstallationPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp new file mode 100644 index 000000000..3c4470047 --- /dev/null +++ b/apps/wizard/installationpage.hpp @@ -0,0 +1,21 @@ +#ifndef INSTALLATIONPAGE_HPP +#define INSTALLATIONPAGE_HPP + +#include + +#include "ui_installationpage.h" + +namespace Wizard +{ + + class InstallationPage : public QWizardPage, private Ui::InstallationPage + { + Q_OBJECT + public: + InstallationPage(QWidget *parent = 0); + + }; + +} + +#endif // INSTALLATIONPAGE_HPP diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp new file mode 100644 index 000000000..7632b2331 --- /dev/null +++ b/apps/wizard/installationtargetpage.cpp @@ -0,0 +1,7 @@ +#include "installationtargetpage.hpp" + +Wizard::InstallationTargetPage::InstallationTargetPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp new file mode 100644 index 000000000..266d1adc7 --- /dev/null +++ b/apps/wizard/installationtargetpage.hpp @@ -0,0 +1,21 @@ +#ifndef INSTALLATIONTARGETPAGE_HPP +#define INSTALLATIONTARGETPAGE_HPP + +#include + +#include "ui_installationtargetpage.h" + +namespace Wizard +{ + + class InstallationTargetPage : public QWizardPage, private Ui::InstallationTargetPage + { + Q_OBJECT + public: + InstallationTargetPage(QWidget *parent = 0); + + }; + +} + +#endif // INSTALLATIONTARGETPAGE_HPP diff --git a/apps/wizard/intropage.cpp b/apps/wizard/intropage.cpp new file mode 100644 index 000000000..7b390f059 --- /dev/null +++ b/apps/wizard/intropage.cpp @@ -0,0 +1,7 @@ +#include "intropage.hpp" + +Wizard::IntroPage::IntroPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/intropage.hpp b/apps/wizard/intropage.hpp new file mode 100644 index 000000000..b247efd89 --- /dev/null +++ b/apps/wizard/intropage.hpp @@ -0,0 +1,21 @@ +#ifndef INTROPAGE_HPP +#define INTROPAGE_HPP + +#include + +#include "ui_intropage.h" + +namespace Wizard +{ + + class IntroPage : public QWizardPage, private Ui::IntroPage + { + Q_OBJECT + public: + IntroPage(QWidget *parent = 0); + + }; + +} + +#endif // INTROPAGE_HPP diff --git a/apps/wizard/main.cpp b/apps/wizard/main.cpp index ed3f3a6ea..e6a94118a 100644 --- a/apps/wizard/main.cpp +++ b/apps/wizard/main.cpp @@ -3,6 +3,8 @@ #include #include +#include "mainwizard.hpp" + #ifdef MAC_OS_X_VERSION_MIN_REQUIRED #undef MAC_OS_X_VERSION_MIN_REQUIRED // We need to do this because of Qt: https://bugreports.qt-project.org/browse/QTBUG-22154 @@ -39,13 +41,8 @@ int main(int argc, char *argv[]) // Support non-latin characters QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); -// Launcher::MainDialog mainWin; - -// if (mainWin.setup()) { -// mainWin.show(); -// } else { -// return 0; -// } + Wizard::MainWizard wizard; + wizard.show(); return app.exec(); } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp new file mode 100644 index 000000000..438d9e576 --- /dev/null +++ b/apps/wizard/mainwizard.cpp @@ -0,0 +1,36 @@ +#include "mainwizard.hpp" +#include "intropage.hpp" +#include "methodselectionpage.hpp" +#include "existinginstallationpage.hpp" +#include "installationtargetpage.hpp" +#include "componentselectionpage.hpp" +#include "installationpage.hpp" +#include "importpage.hpp" +#include "conclusionpage.hpp" + +Wizard::MainWizard::MainWizard(QWidget *parent) : + QWizard(parent) +{ + +#ifndef Q_OS_MAC + setWizardStyle(QWizard::ModernStyle); +#else + setWizardStyle(QWizard::ClassicStyle); +#endif + + setWindowTitle(tr("OpenMW Wizard")); + setupPages(); +} + +void Wizard::MainWizard::setupPages() +{ + setPage(Page_Intro, new IntroPage); + setPage(Page_MethodSelection, new MethodSelectionPage); + setPage(Page_ExistingInstallation, new ExistingInstallationPage); + setPage(Page_InstallationTarget, new InstallationTargetPage); + setPage(Page_ComponentSelection, new ComponentSelectionPage); + setPage(Page_Installation, new InstallationPage); + setPage(Page_Import, new ImportPage); + setPage(Page_Conclusion, new ConclusionPage); + setStartId(Page_Intro); +} diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp new file mode 100644 index 000000000..e5cdc866b --- /dev/null +++ b/apps/wizard/mainwizard.hpp @@ -0,0 +1,33 @@ +#ifndef MAINWIZARD_HPP +#define MAINWIZARD_HPP + +#include + +namespace Wizard +{ + + class MainWizard : public QWizard + { + Q_OBJECT + + public: + enum { + Page_Intro, + Page_MethodSelection, + Page_ExistingInstallation, + Page_InstallationTarget, + Page_ComponentSelection, + Page_Installation, + Page_Import, + Page_Conclusion + }; + + MainWizard(QWidget *parent = 0); + + private: + void setupPages(); + }; + +} + +#endif // MAINWIZARD_HPP diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp new file mode 100644 index 000000000..bd9997367 --- /dev/null +++ b/apps/wizard/methodselectionpage.cpp @@ -0,0 +1,7 @@ +#include "methodselectionpage.hpp" + +Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/methodselectionpage.hpp b/apps/wizard/methodselectionpage.hpp new file mode 100644 index 000000000..cc32a825e --- /dev/null +++ b/apps/wizard/methodselectionpage.hpp @@ -0,0 +1,21 @@ +#ifndef METHODSELECTIONPAGE_HPP +#define METHODSELECTIONPAGE_HPP + +#include + +#include "ui_methodselectionpage.h" + +namespace Wizard +{ + + class MethodSelectionPage : public QWizardPage, private Ui::MethodSelectionPage + { + Q_OBJECT + public: + MethodSelectionPage(QWidget *parent = 0); + + }; + +} + +#endif // METHODSELECTIONPAGE_HPP diff --git a/apps/wizard/wizard.cpp b/apps/wizard/wizard.cpp deleted file mode 100644 index 8d1c8b69c..000000000 --- a/apps/wizard/wizard.cpp +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/wizard/wizard.hpp b/apps/wizard/wizard.hpp deleted file mode 100644 index 8d1c8b69c..000000000 --- a/apps/wizard/wizard.hpp +++ /dev/null @@ -1 +0,0 @@ - diff --git a/files/ui/wizard/configurationpage.ui b/files/ui/wizard/configurationpage.ui deleted file mode 100644 index 8340b2ebf..000000000 --- a/files/ui/wizard/configurationpage.ui +++ /dev/null @@ -1,58 +0,0 @@ - - - WizardPage - - - - 0 - 0 - 440 - 300 - - - - WizardPage - - - - - - <html><head/><body><p>OpenMW needs to import settings from the Morrowind configuration file in order to function properly. Select the steps you want to be taken and click Next when you are ready to continue.</p><p><span style=" font-weight:600;">Note:</span> it is possible to import settings later, by re-running this Wizard.</p><p/></body></html> - - - true - - - - - - - Import settings from Morrowind.ini - - - - - - - Import selected add-ons (creates a new Cotent List in the launcher) - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/files/ui/wizard/installationtargetpage.ui b/files/ui/wizard/installationtargetpage.ui index 6bdac5592..c87214d8d 100644 --- a/files/ui/wizard/installationtargetpage.ui +++ b/files/ui/wizard/installationtargetpage.ui @@ -53,16 +53,6 @@ - - - - Click Next to continue. If you would like to select a different location, click Browse. - - - true - - - From 0e1d3237fe280a07a228eda14008bc116e36463a Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 8 Dec 2013 22:58:29 +0100 Subject: [PATCH 04/69] Added some logic to the page switching --- apps/wizard/componentselectionpage.cpp | 7 +++++++ apps/wizard/componentselectionpage.hpp | 2 ++ apps/wizard/conclusionpage.cpp | 5 +++++ apps/wizard/conclusionpage.hpp | 2 ++ apps/wizard/existinginstallationpage.cpp | 7 +++++++ apps/wizard/existinginstallationpage.hpp | 2 ++ apps/wizard/importpage.cpp | 7 +++++++ apps/wizard/importpage.hpp | 2 ++ apps/wizard/installationpage.cpp | 7 +++++++ apps/wizard/installationpage.hpp | 2 ++ apps/wizard/installationtargetpage.cpp | 7 +++++++ apps/wizard/installationtargetpage.hpp | 2 ++ apps/wizard/intropage.cpp | 7 +++++++ apps/wizard/intropage.hpp | 2 ++ apps/wizard/methodselectionpage.cpp | 11 +++++++++++ apps/wizard/methodselectionpage.hpp | 2 ++ 16 files changed, 74 insertions(+) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index dbfa9412e..0293bdc05 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -1,7 +1,14 @@ #include "componentselectionpage.hpp" +#include "mainwizard.hpp" + Wizard::ComponentSelectionPage::ComponentSelectionPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::ComponentSelectionPage::nextId() const +{ + return MainWizard::Page_Installation; +} diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index cc36ee485..277845774 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: ComponentSelectionPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 2913e431d..567d325b3 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -5,3 +5,8 @@ Wizard::ConclusionPage::ConclusionPage(QWidget *parent) : { setupUi(this); } + +int Wizard::ConclusionPage::nextId() const +{ + return -1; +} diff --git a/apps/wizard/conclusionpage.hpp b/apps/wizard/conclusionpage.hpp index 37c477b44..e00fc0fe3 100644 --- a/apps/wizard/conclusionpage.hpp +++ b/apps/wizard/conclusionpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: ConclusionPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index ad540de73..d6dea0af9 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -1,7 +1,14 @@ #include "existinginstallationpage.hpp" +#include "mainwizard.hpp" + Wizard::ExistingInstallationPage::ExistingInstallationPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::ExistingInstallationPage::nextId() const +{ + return MainWizard::Page_ComponentSelection; +} diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index 5048a14d4..823bc2140 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: ExistingInstallationPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/importpage.cpp b/apps/wizard/importpage.cpp index 2ac92efd0..29db6919d 100644 --- a/apps/wizard/importpage.cpp +++ b/apps/wizard/importpage.cpp @@ -1,7 +1,14 @@ #include "importpage.hpp" +#include "mainwizard.hpp" + Wizard::ImportPage::ImportPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::ImportPage::nextId() const +{ + return MainWizard::Page_Conclusion; +} diff --git a/apps/wizard/importpage.hpp b/apps/wizard/importpage.hpp index e3917c837..f1cc11e03 100644 --- a/apps/wizard/importpage.hpp +++ b/apps/wizard/importpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: ImportPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 20737ebfb..092cf62e0 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,7 +1,14 @@ #include "installationpage.hpp" +#include "mainwizard.hpp" + Wizard::InstallationPage::InstallationPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::InstallationPage::nextId() const +{ + return MainWizard::Page_Import; +} diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 3c4470047..125fcc2e1 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: InstallationPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 7632b2331..fa178f913 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -1,7 +1,14 @@ #include "installationtargetpage.hpp" +#include "mainwizard.hpp" + Wizard::InstallationTargetPage::InstallationTargetPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::InstallationTargetPage::nextId() const +{ + return MainWizard::Page_ComponentSelection; +} diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp index 266d1adc7..4e399b707 100644 --- a/apps/wizard/installationtargetpage.hpp +++ b/apps/wizard/installationtargetpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: InstallationTargetPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/intropage.cpp b/apps/wizard/intropage.cpp index 7b390f059..72c769ae6 100644 --- a/apps/wizard/intropage.cpp +++ b/apps/wizard/intropage.cpp @@ -1,7 +1,14 @@ #include "intropage.hpp" +#include "mainwizard.hpp" + Wizard::IntroPage::IntroPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::IntroPage::nextId() const +{ + return MainWizard::Page_MethodSelection; +} diff --git a/apps/wizard/intropage.hpp b/apps/wizard/intropage.hpp index b247efd89..603d283fa 100644 --- a/apps/wizard/intropage.hpp +++ b/apps/wizard/intropage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: IntroPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index bd9997367..59125f251 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -1,7 +1,18 @@ #include "methodselectionpage.hpp" +#include "mainwizard.hpp" + Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::MethodSelectionPage::nextId() const +{ + if (newLocationRadioButton->isChecked()) { + return MainWizard::Page_InstallationTarget; + } else { + return MainWizard::Page_ExistingInstallation; + } +} diff --git a/apps/wizard/methodselectionpage.hpp b/apps/wizard/methodselectionpage.hpp index cc32a825e..1555ea157 100644 --- a/apps/wizard/methodselectionpage.hpp +++ b/apps/wizard/methodselectionpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: MethodSelectionPage(QWidget *parent = 0); + int nextId() const; + }; } From d7f96041400c8398da4fd061b3218671e598d81a Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 13 Dec 2013 13:38:49 +0100 Subject: [PATCH 05/69] WIP: Working on the installation selection and added the ui for the language page --- apps/wizard/componentselectionpage.cpp | 107 +++++++++++++++++++- apps/wizard/componentselectionpage.hpp | 12 ++- apps/wizard/conclusionpage.cpp | 21 +++- apps/wizard/conclusionpage.hpp | 9 +- apps/wizard/existinginstallationpage.cpp | 97 +++++++++++++++++- apps/wizard/existinginstallationpage.hpp | 15 ++- apps/wizard/importpage.cpp | 5 +- apps/wizard/importpage.hpp | 6 +- apps/wizard/installationpage.cpp | 13 ++- apps/wizard/installationpage.hpp | 9 +- apps/wizard/installationtargetpage.cpp | 33 +++++- apps/wizard/installationtargetpage.hpp | 12 ++- apps/wizard/intropage.cpp | 6 +- apps/wizard/intropage.hpp | 5 +- apps/wizard/mainwizard.cpp | 69 +++++++++++-- apps/wizard/mainwizard.hpp | 13 +++ apps/wizard/methodselectionpage.cpp | 11 +- apps/wizard/methodselectionpage.hpp | 6 +- files/ui/wizard/conclusionpage.ui | 8 +- files/ui/wizard/existinginstallationpage.ui | 9 +- files/ui/wizard/languageselectionpage.ui | 72 +++++++++++++ files/wizard/wizard.qrc | 7 +- 22 files changed, 499 insertions(+), 46 deletions(-) create mode 100644 files/ui/wizard/languageselectionpage.ui diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 0293bdc05..1ec1380b1 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -1,14 +1,115 @@ #include "componentselectionpage.hpp" +#include +#include + #include "mainwizard.hpp" -Wizard::ComponentSelectionPage::ComponentSelectionPage(QWidget *parent) : - QWizardPage(parent) +Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); + + setCommitPage(true); + setButtonText(QWizard::CommitButton, tr("&Install")); + + connect(componentsList, SIGNAL(itemChanged(QListWidgetItem*)), + this, SLOT(updateButton(QListWidgetItem*))); +} + +void Wizard::ComponentSelectionPage::updateButton(QListWidgetItem *item) +{ + if (field("installation.new").toBool() == true) + return; // Morrowind is always checked here + + bool unchecked = true; + + for (int i =0; i < componentsList->count(); ++i) { + QListWidgetItem *item = componentsList->item(i); + + qDebug() << "item is " << item->text(); + + if (!item) + continue; + + if (item->checkState() == Qt::Checked) { + unchecked = false; + } + } + + if (unchecked) { + setCommitPage(false); + setButtonText(QWizard::NextButton, tr("&Skip")); + } else { + setCommitPage(true); + } +} + +void Wizard::ComponentSelectionPage::initializePage() +{ + componentsList->clear(); + + QString path = field("installation.path").toString(); + + QListWidgetItem *morrowindItem = new QListWidgetItem(QString("Morrowind")); + QListWidgetItem *tribunalItem = new QListWidgetItem(QString("Tribunal")); + QListWidgetItem *bloodmoonItem = new QListWidgetItem(QString("Bloodmoon")); + + if (field("installation.new").toBool() == true) + { + morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + morrowindItem->setData(Qt::CheckStateRole, Qt::Checked); + componentsList->addItem(morrowindItem); + + tribunalItem->setFlags(tribunalItem->flags() | Qt::ItemIsUserCheckable); + tribunalItem->setData(Qt::CheckStateRole, Qt::Checked); + componentsList->addItem(tribunalItem); + + bloodmoonItem->setFlags(bloodmoonItem->flags() | Qt::ItemIsUserCheckable); + bloodmoonItem->setData(Qt::CheckStateRole, Qt::Checked); + componentsList->addItem(bloodmoonItem); + } else { + + if (mWizard->mInstallations[path]->hasMorrowind == true) { + morrowindItem->setText(tr("Morrowind\t\t(installed)")); + morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + morrowindItem->setData(Qt::CheckStateRole, Qt::Unchecked); + } else { + morrowindItem->setText(tr("Morrowind")); + morrowindItem->setData(Qt::CheckStateRole, Qt::Checked); + } + + componentsList->addItem(morrowindItem); + + if (mWizard->mInstallations[path]->hasTribunal == true) { + tribunalItem->setText(tr("Tribunal\t\t(installed)")); + tribunalItem->setFlags(tribunalItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + tribunalItem->setData(Qt::CheckStateRole, Qt::Unchecked); + } else { + tribunalItem->setText(tr("Tribunal")); + tribunalItem->setData(Qt::CheckStateRole, Qt::Checked); + } + + componentsList->addItem(tribunalItem); + + if (mWizard->mInstallations[path]->hasBloodmoon == true) { + bloodmoonItem->setText(tr("Bloodmoon\t\t(installed)")); + bloodmoonItem->setFlags(bloodmoonItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + bloodmoonItem->setData(Qt::CheckStateRole, Qt::Unchecked); + } else { + bloodmoonItem->setText(tr("Bloodmoon")); + bloodmoonItem->setData(Qt::CheckStateRole, Qt::Checked); + } + + componentsList->addItem(bloodmoonItem); + } } int Wizard::ComponentSelectionPage::nextId() const { - return MainWizard::Page_Installation; + if (isCommitPage()) + return MainWizard::Page_Installation; + + return MainWizard::Page_Import; } diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index 277845774..8b4c186d0 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -7,15 +7,25 @@ namespace Wizard { + class MainWizard; class ComponentSelectionPage : public QWizardPage, private Ui::ComponentSelectionPage { Q_OBJECT public: - ComponentSelectionPage(QWidget *parent = 0); + ComponentSelectionPage(MainWizard *wizard); int nextId() const; + private slots: + void updateButton(QListWidgetItem *item); + + private: + MainWizard *mWizard; + + protected: + void initializePage(); + }; } diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 567d325b3..55b03b912 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -1,11 +1,28 @@ #include "conclusionpage.hpp" -Wizard::ConclusionPage::ConclusionPage(QWidget *parent) : - QWizardPage(parent) +#include + +#include "mainwizard.hpp" + +Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); } +void Wizard::ConclusionPage::initializePage() +{ + if (field("installation.new").toBool() == true) + { + textLabel->setText(tr("The OpenMW Wizard successfully installed Morrowind on your computer.\n\n") + + tr("Click Finish to close the Wizard.")); + } else { + textLabel->setText(tr("The OpenMW Wizard successfully modified your existing Morrowind installation.\n\n") + + tr("Click Finish to close the Wizard.")); + } +} + int Wizard::ConclusionPage::nextId() const { return -1; diff --git a/apps/wizard/conclusionpage.hpp b/apps/wizard/conclusionpage.hpp index e00fc0fe3..e050684e1 100644 --- a/apps/wizard/conclusionpage.hpp +++ b/apps/wizard/conclusionpage.hpp @@ -7,15 +7,22 @@ namespace Wizard { + class MainWizard; class ConclusionPage : public QWizardPage, private Ui::ConclusionPage { Q_OBJECT public: - ConclusionPage(QWidget *parent = 0); + ConclusionPage(MainWizard *wizard); int nextId() const; + private: + MainWizard *mWizard; + + protected: + void initializePage(); + }; } diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index d6dea0af9..f9eb7c283 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -1,14 +1,105 @@ #include "existinginstallationpage.hpp" +#include +#include + #include "mainwizard.hpp" -Wizard::ExistingInstallationPage::ExistingInstallationPage(QWidget *parent) : - QWizardPage(parent) +Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); + + connect(detectedList, SIGNAL(currentTextChanged(QString)), + this, SLOT(textChanged(QString))); + + connect(detectedList,SIGNAL(itemSelectionChanged()), + this, SIGNAL(completeChanged())); +} + +void Wizard::ExistingInstallationPage::on_browseButton_clicked() +{ + QString selectedFile = QFileDialog::getOpenFileName( + this, + tr("Select master file"), + QDir::currentPath(), + QString(tr("Morrowind master file (*.esm)")), + NULL, + QFileDialog::DontResolveSymlinks); + + QFileInfo info(selectedFile); + if (!info.exists()) + return; + + QDir dir(info.absolutePath()); + if (!dir.cdUp()) + return; // Cannot move out of the Data Files directory + + QString path = QDir::toNativeSeparators(dir.absolutePath()); + QList items = detectedList->findItems(path, Qt::MatchExactly); + + if (items.isEmpty()) + { + // Path is not yet in the list, add it + QListWidgetItem *item = new QListWidgetItem(path); + detectedList->addItem(item); + detectedList->setCurrentItem(item); // Select it too + } else { + detectedList->setCurrentItem(items.first()); + } + +} + +void Wizard::ExistingInstallationPage::textChanged(const QString &text) +{ + // Set the installation path manually, as registerField doesn't work + if (!text.isEmpty()) + wizard()->setField("installation.path", text); +} + +void Wizard::ExistingInstallationPage::initializePage() +{ + + QStringList paths = mWizard->mInstallations.keys(); + + if (paths.isEmpty()) + return; + + detectedList->clear(); + + foreach (const QString &path, paths) { + QListWidgetItem *item = new QListWidgetItem(path); + detectedList->addItem(item); + } + +} + +bool Wizard::ExistingInstallationPage::isComplete() const +{ + if (detectedList->selectionModel()->hasSelection()) { + return true; + } else { + return false; + } } int Wizard::ExistingInstallationPage::nextId() const { - return MainWizard::Page_ComponentSelection; + QString path = field("installation.path").toString(); + + if (path.isEmpty()) + return MainWizard::Page_ComponentSelection; + + if (!mWizard->mInstallations.contains(path)) + return MainWizard::Page_ComponentSelection; + + if (mWizard->mInstallations[path]->hasMorrowind == true && + mWizard->mInstallations[path]->hasTribunal == true && + mWizard->mInstallations[path]->hasBloodmoon == true) + { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_ComponentSelection; + } } diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index 823bc2140..88d6913bc 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -7,14 +7,27 @@ namespace Wizard { + class MainWizard; class ExistingInstallationPage : public QWizardPage, private Ui::ExistingInstallationPage { Q_OBJECT public: - ExistingInstallationPage(QWidget *parent = 0); + ExistingInstallationPage(MainWizard *wizard); int nextId() const; + virtual bool isComplete() const; + + private slots: + void on_browseButton_clicked(); + void textChanged(const QString &text); + + + private: + MainWizard *mWizard; + + protected: + void initializePage(); }; diff --git a/apps/wizard/importpage.cpp b/apps/wizard/importpage.cpp index 29db6919d..059683cf0 100644 --- a/apps/wizard/importpage.cpp +++ b/apps/wizard/importpage.cpp @@ -2,8 +2,9 @@ #include "mainwizard.hpp" -Wizard::ImportPage::ImportPage(QWidget *parent) : - QWizardPage(parent) +Wizard::ImportPage::ImportPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); } diff --git a/apps/wizard/importpage.hpp b/apps/wizard/importpage.hpp index f1cc11e03..2eae08531 100644 --- a/apps/wizard/importpage.hpp +++ b/apps/wizard/importpage.hpp @@ -7,15 +7,19 @@ namespace Wizard { + class MainWizard; class ImportPage : public QWizardPage, private Ui::ImportPage { Q_OBJECT public: - ImportPage(QWidget *parent = 0); + ImportPage(MainWizard *wizard); int nextId() const; + private: + MainWizard *mWizard; + }; } diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 092cf62e0..de0720ecb 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,13 +1,22 @@ #include "installationpage.hpp" +#include + #include "mainwizard.hpp" -Wizard::InstallationPage::InstallationPage(QWidget *parent) : - QWizardPage(parent) +Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); } +void Wizard::InstallationPage::initializePage() +{ + qDebug() << "installing to: " << field("installation.path").toString(); + logTextEdit->setText(QString("Installing to %1").arg(field("installation.path").toString())); +} + int Wizard::InstallationPage::nextId() const { return MainWizard::Page_Import; diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 125fcc2e1..cb206e225 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -7,15 +7,22 @@ namespace Wizard { + class MainWizard; class InstallationPage : public QWizardPage, private Ui::InstallationPage { Q_OBJECT public: - InstallationPage(QWidget *parent = 0); + InstallationPage(MainWizard *wizard); int nextId() const; + private: + MainWizard *mWizard; + + protected: + void initializePage(); + }; } diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index fa178f913..43941f0dc 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -1,11 +1,40 @@ #include "installationtargetpage.hpp" +#include +#include + #include "mainwizard.hpp" -Wizard::InstallationTargetPage::InstallationTargetPage(QWidget *parent) : - QWizardPage(parent) +Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); + + registerField("installation.path*", targetLineEdit); +} + +void Wizard::InstallationTargetPage::initializePage() +{ + +} + +void Wizard::InstallationTargetPage::on_browseButton_clicked() +{ + QString selectedPath = QFileDialog::getExistingDirectory( + this, + tr("Select where to install Morrowind"), + QDir::currentPath(), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + qDebug() << selectedPath; + QFileInfo info(selectedPath); + if (!info.exists()) + return; + + if (info.isWritable()) + targetLineEdit->setText(info.absoluteFilePath()); + } int Wizard::InstallationTargetPage::nextId() const diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp index 4e399b707..ccaac5969 100644 --- a/apps/wizard/installationtargetpage.hpp +++ b/apps/wizard/installationtargetpage.hpp @@ -7,15 +7,25 @@ namespace Wizard { + class MainWizard; class InstallationTargetPage : public QWizardPage, private Ui::InstallationTargetPage { Q_OBJECT public: - InstallationTargetPage(QWidget *parent = 0); + InstallationTargetPage(MainWizard *wizard); int nextId() const; + private slots: + void on_browseButton_clicked(); + + private: + MainWizard *mWizard; + + protected: + void initializePage(); + }; } diff --git a/apps/wizard/intropage.cpp b/apps/wizard/intropage.cpp index 72c769ae6..93510cd21 100644 --- a/apps/wizard/intropage.cpp +++ b/apps/wizard/intropage.cpp @@ -2,8 +2,10 @@ #include "mainwizard.hpp" -Wizard::IntroPage::IntroPage(QWidget *parent) : - QWizardPage(parent) +Wizard::IntroPage::IntroPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) + { setupUi(this); } diff --git a/apps/wizard/intropage.hpp b/apps/wizard/intropage.hpp index 603d283fa..78bac3155 100644 --- a/apps/wizard/intropage.hpp +++ b/apps/wizard/intropage.hpp @@ -7,15 +7,18 @@ namespace Wizard { + class MainWizard; class IntroPage : public QWizardPage, private Ui::IntroPage { Q_OBJECT public: - IntroPage(QWidget *parent = 0); + IntroPage(MainWizard *wizard); int nextId() const; + private: + MainWizard *mWizard; }; } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 438d9e576..a741546fd 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -1,4 +1,8 @@ #include "mainwizard.hpp" + +#include +#include + #include "intropage.hpp" #include "methodselectionpage.hpp" #include "existinginstallationpage.hpp" @@ -19,18 +23,67 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : #endif setWindowTitle(tr("OpenMW Wizard")); + setupInstallations(); setupPages(); + + QDir dir("/home/pvdk/data"); + QFileInfo info(dir.absoluteFilePath("../Morrowind.ini")); + + qDebug() << "exists? " << info.exists(); +} + +void Wizard::MainWizard::setupInstallations() +{ + // TODO: detect existing installations + QStringList paths; + paths << QString("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind"); + paths << QString("/home/pvdk/openmw/Data Files"); + paths << QString("/usr/games/morrowind"); + + foreach (const QString &path, paths) + { + Installation* install = new Installation(); + + install->hasMorrowind = (findFiles(QString("Morrowind"), path)); + install->hasTribunal = true; + install->hasBloodmoon = false; + + mInstallations.insert(QDir::toNativeSeparators(path), install); + } + } void Wizard::MainWizard::setupPages() { - setPage(Page_Intro, new IntroPage); - setPage(Page_MethodSelection, new MethodSelectionPage); - setPage(Page_ExistingInstallation, new ExistingInstallationPage); - setPage(Page_InstallationTarget, new InstallationTargetPage); - setPage(Page_ComponentSelection, new ComponentSelectionPage); - setPage(Page_Installation, new InstallationPage); - setPage(Page_Import, new ImportPage); - setPage(Page_Conclusion, new ConclusionPage); + setPage(Page_Intro, new IntroPage(this)); + setPage(Page_MethodSelection, new MethodSelectionPage(this)); + setPage(Page_ExistingInstallation, new ExistingInstallationPage(this)); + setPage(Page_InstallationTarget, new InstallationTargetPage(this)); + setPage(Page_ComponentSelection, new ComponentSelectionPage(this)); + setPage(Page_Installation, new InstallationPage(this)); + setPage(Page_Import, new ImportPage(this)); + setPage(Page_Conclusion, new ConclusionPage(this)); setStartId(Page_Intro); } + +bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) +{ + QDir dir(path); + + if (!dir.exists()) + return false; + + if (!dir.cd(QString("Data Files"))) + return false; + + qDebug() << "name: " << name + QString(".esm") << dir.absolutePath(); + + // TODO: add MIME handling to make sure the files are real + if (dir.exists(name + QString(".esm")) && dir.exists(name + QString(".bsa"))) + { + return true; + } else { + return false; + } + +} diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index e5cdc866b..15706e8e3 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -2,6 +2,7 @@ #define MAINWIZARD_HPP #include +#include namespace Wizard { @@ -11,6 +12,12 @@ namespace Wizard Q_OBJECT public: + struct Installation { + bool hasMorrowind; + bool hasTribunal; + bool hasBloodmoon; + }; + enum { Page_Intro, Page_MethodSelection, @@ -24,8 +31,14 @@ namespace Wizard MainWizard(QWidget *parent = 0); + static bool findFiles(const QString &name, const QString &path); + QMap mInstallations; + private: + void setupInstallations(); void setupPages(); + + }; } diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index 59125f251..1efffc13d 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -1,18 +1,23 @@ #include "methodselectionpage.hpp" - +#include #include "mainwizard.hpp" -Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : - QWizardPage(parent) +Wizard::MethodSelectionPage::MethodSelectionPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); + + registerField("installation.new", newLocationRadioButton); } int Wizard::MethodSelectionPage::nextId() const { if (newLocationRadioButton->isChecked()) { + //wizard()->setField("installation.new", true); return MainWizard::Page_InstallationTarget; } else { + //wizard()->setField("installation.new", false); return MainWizard::Page_ExistingInstallation; } } diff --git a/apps/wizard/methodselectionpage.hpp b/apps/wizard/methodselectionpage.hpp index 1555ea157..de34ff555 100644 --- a/apps/wizard/methodselectionpage.hpp +++ b/apps/wizard/methodselectionpage.hpp @@ -7,15 +7,19 @@ namespace Wizard { + class MainWizard; class MethodSelectionPage : public QWizardPage, private Ui::MethodSelectionPage { Q_OBJECT public: - MethodSelectionPage(QWidget *parent = 0); + MethodSelectionPage(MainWizard *wizard); int nextId() const; + private: + MainWizard *mWizard; + }; } diff --git a/files/ui/wizard/conclusionpage.ui b/files/ui/wizard/conclusionpage.ui index bedad3e2a..a27f66789 100644 --- a/files/ui/wizard/conclusionpage.ui +++ b/files/ui/wizard/conclusionpage.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 398 + 298 @@ -20,9 +20,7 @@ - The OpenMW Wizard successfully installed Morrowind on your computer. - -Click Finish to close the Wizard. + Placeholder true diff --git a/files/ui/wizard/existinginstallationpage.ui b/files/ui/wizard/existinginstallationpage.ui index 579ddbe67..3ca831636 100644 --- a/files/ui/wizard/existinginstallationpage.ui +++ b/files/ui/wizard/existinginstallationpage.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 396 + 296 @@ -23,7 +23,7 @@ - Detected Installations: + Detected installations: @@ -33,6 +33,9 @@ No existing installations detected + + NoItemFlags + diff --git a/files/ui/wizard/languageselectionpage.ui b/files/ui/wizard/languageselectionpage.ui new file mode 100644 index 000000000..134a9de86 --- /dev/null +++ b/files/ui/wizard/languageselectionpage.ui @@ -0,0 +1,72 @@ + + + WizardPage + + + + 0 + 0 + 398 + 298 + + + + WizardPage + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><img src=":/icons/tango/48x48/preferences-desktop-locale.png"/></p></body></html> + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + What is the language of the Morrowind installation? + + + true + + + + + + + + + + + + Qt::Vertical + + + + 20 + 230 + + + + + + + + + + + diff --git a/files/wizard/wizard.qrc b/files/wizard/wizard.qrc index 86b3795c4..b3bb722c9 100644 --- a/files/wizard/wizard.qrc +++ b/files/wizard/wizard.qrc @@ -1,7 +1,8 @@ - - + + + icons/tango/48x48/preferences-desktop-locale.png icons/tango/index.theme icons/tango/48x48/folder.png icons/tango/48x48/system-installer.png - + From c82f0ec35fb6464fd1485e61f0633cb51c8a8660 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 13 Dec 2013 15:30:49 +0100 Subject: [PATCH 06/69] Implemented the language selection page --- apps/wizard/CMakeLists.txt | 4 ++ apps/wizard/languageselectionpage.cpp | 18 +++++ apps/wizard/languageselectionpage.hpp | 25 +++++++ apps/wizard/mainwizard.cpp | 2 + apps/wizard/mainwizard.hpp | 1 + apps/wizard/methodselectionpage.cpp | 8 +-- files/ui/wizard/languageselectionpage.ui | 88 ++++++++++++++++++++++-- 7 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 apps/wizard/languageselectionpage.cpp create mode 100644 apps/wizard/languageselectionpage.hpp diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 3ff41c206..a62327d85 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -6,6 +6,7 @@ set(WIZARD installationpage.cpp installationtargetpage.cpp intropage.cpp + languageselectionpage.cpp main.cpp mainwizard.cpp methodselectionpage.cpp @@ -22,6 +23,7 @@ set(WIZARD_HEADER installationpage.hpp installationtargetpage.hpp intropage.hpp + languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp ) @@ -39,6 +41,7 @@ set(WIZARD_HEADER_MOC installationpage.hpp installationtargetpage.hpp intropage.hpp + languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp ) @@ -56,6 +59,7 @@ set(WIZARD_UI ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationtargetpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/intropage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/languageselectionpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/methodselectionpage.ui ) diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp new file mode 100644 index 000000000..dc64f1ae1 --- /dev/null +++ b/apps/wizard/languageselectionpage.cpp @@ -0,0 +1,18 @@ +#include "languageselectionpage.hpp" + +#include "mainwizard.hpp" + +Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) +{ + setupUi(this); +} + +int Wizard::LanguageSelectionPage::nextId() const +{ + if (field("installation.new").toBool() == true) + return MainWizard::Page_InstallationTarget; + + return MainWizard::Page_ExistingInstallation; +} diff --git a/apps/wizard/languageselectionpage.hpp b/apps/wizard/languageselectionpage.hpp new file mode 100644 index 000000000..aa7a81ff1 --- /dev/null +++ b/apps/wizard/languageselectionpage.hpp @@ -0,0 +1,25 @@ +#ifndef LANGUAGESELECTIONPAGE_HPP +#define LANGUAGESELECTIONPAGE_HPP + +#include + +#include "ui_languageselectionpage.h" + +namespace Wizard +{ + class MainWizard; + + class LanguageSelectionPage : public QWizardPage, private Ui::LanguageSelectionPage + { + Q_OBJECT + public: + LanguageSelectionPage(MainWizard *wizard); + + int nextId() const; + + private: + MainWizard *mWizard; + }; +} + +#endif // LANGUAGESELECTIONPAGE_HPP diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index a741546fd..77f17499e 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -5,6 +5,7 @@ #include "intropage.hpp" #include "methodselectionpage.hpp" +#include "languageselectionpage.hpp" #include "existinginstallationpage.hpp" #include "installationtargetpage.hpp" #include "componentselectionpage.hpp" @@ -57,6 +58,7 @@ void Wizard::MainWizard::setupPages() { setPage(Page_Intro, new IntroPage(this)); setPage(Page_MethodSelection, new MethodSelectionPage(this)); + setPage(Page_LanguageSelection, new LanguageSelectionPage(this)); setPage(Page_ExistingInstallation, new ExistingInstallationPage(this)); setPage(Page_InstallationTarget, new InstallationTargetPage(this)); setPage(Page_ComponentSelection, new ComponentSelectionPage(this)); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 15706e8e3..c6b541d35 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -21,6 +21,7 @@ namespace Wizard enum { Page_Intro, Page_MethodSelection, + Page_LanguageSelection, Page_ExistingInstallation, Page_InstallationTarget, Page_ComponentSelection, diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index 1efffc13d..4185fa2bc 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -13,11 +13,5 @@ Wizard::MethodSelectionPage::MethodSelectionPage(MainWizard *wizard) : int Wizard::MethodSelectionPage::nextId() const { - if (newLocationRadioButton->isChecked()) { - //wizard()->setField("installation.new", true); - return MainWizard::Page_InstallationTarget; - } else { - //wizard()->setField("installation.new", false); - return MainWizard::Page_ExistingInstallation; - } + return MainWizard::Page_LanguageSelection; } diff --git a/files/ui/wizard/languageselectionpage.ui b/files/ui/wizard/languageselectionpage.ui index 134a9de86..a76e3004b 100644 --- a/files/ui/wizard/languageselectionpage.ui +++ b/files/ui/wizard/languageselectionpage.ui @@ -1,7 +1,7 @@ - WizardPage - + LanguageSelectionPage + 0 @@ -13,11 +13,17 @@ WizardPage + + Select Morrowind Language + + + What is the language of the Morrowind installation? + - + 0 @@ -38,7 +44,7 @@ - What is the language of the Morrowind installation? + Select the language of the Morrowind installation. true @@ -48,7 +54,79 @@ - + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 250 + 0 + + + + + English + + + + + French + + + + + Russian + + + + + Polish + + + + + Italian + + + + + Spanish + + + + + German + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + From 7132b9b0c590574e4d603c605a7307ba23c1151c Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 13 Dec 2013 23:48:55 +0100 Subject: [PATCH 07/69] Implement default installation path support for all platforms --- apps/wizard/componentselectionpage.cpp | 6 +++--- apps/wizard/installationtargetpage.cpp | 28 +++++++++++++++++++++++++- apps/wizard/languageselectionpage.cpp | 2 ++ apps/wizard/mainwizard.cpp | 5 ----- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 1ec1380b1..046f34076 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -52,9 +52,9 @@ void Wizard::ComponentSelectionPage::initializePage() QString path = field("installation.path").toString(); - QListWidgetItem *morrowindItem = new QListWidgetItem(QString("Morrowind")); - QListWidgetItem *tribunalItem = new QListWidgetItem(QString("Tribunal")); - QListWidgetItem *bloodmoonItem = new QListWidgetItem(QString("Bloodmoon")); + QListWidgetItem *morrowindItem = new QListWidgetItem(QLatin1String("Morrowind")); + QListWidgetItem *tribunalItem = new QListWidgetItem(QLatin1String("Tribunal")); + QListWidgetItem *bloodmoonItem = new QListWidgetItem(QLatin1String("Bloodmoon")); if (field("installation.new").toBool() == true) { diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 43941f0dc..0952abdcb 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "mainwizard.hpp" @@ -11,12 +12,37 @@ Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard) : { setupUi(this); - registerField("installation.path*", targetLineEdit); + registerField(QLatin1String("installation.path*"), targetLineEdit); } void Wizard::InstallationTargetPage::initializePage() { +#ifdef Q_OS_WIN + QString path = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation); +#endif +#ifdef Q_OS_MAC + QString path = QDesktopServices::storageLocation(QDesktopServices::DataLocation); +#endif + +#if defined(Q_OS_LINUX) || defined(Q_OS_UNIX) + QString path = QFile::decodeName(qgetenv("XDG_DATA_HOME")); + + if (path.isEmpty()) + path = QDir::homePath() + QLatin1String("/.local/share"); +#endif + + path.append(QLatin1String("/openmw/data")); + + if (!QFile::exists(path)) { + QDir dir; + dir.mkpath(path); + } + + QDir dir(path); + targetLineEdit->setText(QDir::toNativeSeparators(dir.absolutePath())); + + qDebug() << path; } void Wizard::InstallationTargetPage::on_browseButton_clicked() diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index dc64f1ae1..903ab175f 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -7,6 +7,8 @@ Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : mWizard(wizard) { setupUi(this); + + registerField(QLatin1String("installation.language"), languagesComboBox); } int Wizard::LanguageSelectionPage::nextId() const diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 77f17499e..e6cb140d0 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -26,11 +26,6 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setWindowTitle(tr("OpenMW Wizard")); setupInstallations(); setupPages(); - - QDir dir("/home/pvdk/data"); - QFileInfo info(dir.absoluteFilePath("../Morrowind.ini")); - - qDebug() << "exists? " << info.exists(); } void Wizard::MainWizard::setupInstallations() From 77fe73799af9e3638528d79e2c72443c884fcde0 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 15 Dec 2013 13:12:48 +0100 Subject: [PATCH 08/69] Enabled language setting to be accessable from different pages --- apps/wizard/componentselectionpage.cpp | 2 -- apps/wizard/installationpage.cpp | 2 ++ apps/wizard/installationtargetpage.cpp | 2 ++ apps/wizard/mainwizard.cpp | 4 ++++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 046f34076..b5618c677 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -28,8 +28,6 @@ void Wizard::ComponentSelectionPage::updateButton(QListWidgetItem *item) for (int i =0; i < componentsList->count(); ++i) { QListWidgetItem *item = componentsList->item(i); - qDebug() << "item is " << item->text(); - if (!item) continue; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index de0720ecb..1053c3240 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -13,6 +13,8 @@ Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : void Wizard::InstallationPage::initializePage() { + QString path = field("installation.path").toString(); + qDebug() << "installing to: " << field("installation.path").toString(); logTextEdit->setText(QString("Installing to %1").arg(field("installation.path").toString())); } diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 0952abdcb..60acfd5f7 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -17,6 +17,8 @@ Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard) : void Wizard::InstallationTargetPage::initializePage() { + qDebug() << mWizard->field("installation.language"); + #ifdef Q_OS_WIN QString path = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation); #endif diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index e6cb140d0..58ef2e061 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -24,6 +24,10 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : #endif setWindowTitle(tr("OpenMW Wizard")); + + // Set the property for comboboxes to the text instead of index + setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); + setupInstallations(); setupPages(); } From 30710bad704cc9fa2ad7c4efd534c3cd7d3b5cd7 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 15 Dec 2013 15:13:49 +0100 Subject: [PATCH 09/69] Modified components widget so the checked items can be stored for all pages to access --- apps/wizard/CMakeLists.txt | 6 +++ apps/wizard/componentselectionpage.cpp | 12 +++++- apps/wizard/componentselectionpage.hpp | 1 + apps/wizard/installationpage.cpp | 6 ++- apps/wizard/mainwizard.cpp | 2 + apps/wizard/utils/componentlistwidget.cpp | 46 +++++++++++++++++++++++ apps/wizard/utils/componentlistwidget.hpp | 29 ++++++++++++++ files/ui/wizard/componentselectionpage.ui | 13 +++++-- 8 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 apps/wizard/utils/componentlistwidget.cpp create mode 100644 apps/wizard/utils/componentlistwidget.hpp diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index a62327d85..741f790b5 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -10,6 +10,8 @@ set(WIZARD main.cpp mainwizard.cpp methodselectionpage.cpp + + utils/componentlistwidget.cpp ) # if(NOT WIN32) # LIST(APPEND WIZARD unshieldthread.cpp) @@ -26,6 +28,8 @@ set(WIZARD_HEADER languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp + + utils/componentlistwidget.hpp ) # if(NOT WIN32) # LIST(APPEND WIZARD_HEADER unshieldthread.hpp) @@ -44,6 +48,8 @@ set(WIZARD_HEADER_MOC languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp + + utils/componentlistwidget.hpp ) # if(NOT WIN32) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index b5618c677..50e6e8ef4 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -14,8 +14,16 @@ Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : setCommitPage(true); setButtonText(QWizard::CommitButton, tr("&Install")); - connect(componentsList, SIGNAL(itemChanged(QListWidgetItem*)), - this, SLOT(updateButton(QListWidgetItem*))); + registerField("installation.components", componentsList); + + connect(componentsList, SIGNAL(itemChanged(QListWidgetItem *)), + this, SLOT(updateButton(QListWidgetItem *))); + +} + +void Wizard::ComponentSelectionPage::debugMe(QString &text) +{ + qDebug() << "Debug Me" << text; } void Wizard::ComponentSelectionPage::updateButton(QListWidgetItem *item) diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index 8b4c186d0..f5e955782 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -19,6 +19,7 @@ namespace Wizard private slots: void updateButton(QListWidgetItem *item); + void debugMe(QString &text); private: MainWizard *mWizard; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 1053c3240..632b9c6a6 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -14,9 +14,11 @@ Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : void Wizard::InstallationPage::initializePage() { QString path = field("installation.path").toString(); + QStringList components = field("installation.components").toStringList(); + + logTextEdit->append(QString("Installing to %1").arg(path)); + logTextEdit->append(QString("Installing %1.").arg(components.join(", "))); - qDebug() << "installing to: " << field("installation.path").toString(); - logTextEdit->setText(QString("Installing to %1").arg(field("installation.path").toString())); } int Wizard::InstallationPage::nextId() const diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 58ef2e061..e752d0455 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -28,6 +28,8 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : // Set the property for comboboxes to the text instead of index setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); + setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); + setupInstallations(); setupPages(); } diff --git a/apps/wizard/utils/componentlistwidget.cpp b/apps/wizard/utils/componentlistwidget.cpp new file mode 100644 index 000000000..c9a1c1940 --- /dev/null +++ b/apps/wizard/utils/componentlistwidget.cpp @@ -0,0 +1,46 @@ +#include "componentlistwidget.hpp" + +#include +#include + +ComponentListWidget::ComponentListWidget(QWidget *parent) : + QListWidget(parent) +{ + mCheckedItems = QStringList(); + + connect(this, SIGNAL(itemChanged(QListWidgetItem *)), + this, SLOT(updateCheckedItems(QListWidgetItem *))); +} + +void ComponentListWidget::addItem(QListWidgetItem *item) +{ + // The model does not emit a dataChanged signal when items are added + // So we need to update manually + QListWidget::insertItem(count(), item); + updateCheckedItems(item); + +} + +QStringList ComponentListWidget::checkedItems() +{ + mCheckedItems.removeDuplicates(); + return mCheckedItems; +} + +void ComponentListWidget::updateCheckedItems(QListWidgetItem *item) +{ + QString text = item->text(); + + if (item->checkState() == Qt::Checked) { + if (!mCheckedItems.contains(text)) + mCheckedItems.append(text); + } else { + if (mCheckedItems.contains(text)) + mCheckedItems.removeAll(text); + } + + mCheckedItems.removeDuplicates(); + + emit checkedItemsChanged(mCheckedItems); + +} diff --git a/apps/wizard/utils/componentlistwidget.hpp b/apps/wizard/utils/componentlistwidget.hpp new file mode 100644 index 000000000..c9c782d45 --- /dev/null +++ b/apps/wizard/utils/componentlistwidget.hpp @@ -0,0 +1,29 @@ +#ifndef COMPONENTLISTWIDGET_HPP +#define COMPONENTLISTWIDGET_HPP + +#include + +class ComponentListWidget : public QListWidget +{ + Q_OBJECT + + Q_PROPERTY(QStringList mCheckedItems READ checkedItems) + +public: + ComponentListWidget(QWidget *parent = 0); + + QStringList mCheckedItems; + QStringList checkedItems(); + + void addItem(QListWidgetItem *item); + +signals: + void checkedItemsChanged(const QStringList &items); + +private slots: + void updateCheckedItems(QListWidgetItem *item); +}; + + + +#endif // COMPONENTLISTWIDGET_HPP diff --git a/files/ui/wizard/componentselectionpage.ui b/files/ui/wizard/componentselectionpage.ui index c3333752a..026fcdff6 100644 --- a/files/ui/wizard/componentselectionpage.ui +++ b/files/ui/wizard/componentselectionpage.ui @@ -6,8 +6,8 @@ 0 0 - 454 - 393 + 448 + 387 @@ -38,7 +38,7 @@ - + @@ -55,6 +55,13 @@ + + + ComponentListWidget + QListWidget +
apps/wizard/utils/componentlistwidget.hpp
+
+
From 8ea31e5050f5ed83009b68a41effb57903218ee3 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 24 Dec 2013 19:38:21 +0100 Subject: [PATCH 10/69] Working on a Morrowind.ini reader --- apps/wizard/CMakeLists.txt | 32 ++++----------- apps/wizard/inisettings.cpp | 68 ++++++++++++++++++++++++++++++++ apps/wizard/inisettings.hpp | 45 +++++++++++++++++++++ apps/wizard/installationpage.cpp | 64 ++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 24 deletions(-) create mode 100644 apps/wizard/inisettings.cpp create mode 100644 apps/wizard/inisettings.hpp diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 741f790b5..7639cc8e1 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -3,6 +3,7 @@ set(WIZARD conclusionpage.cpp existinginstallationpage.cpp importpage.cpp + inisettings.cpp installationpage.cpp installationtargetpage.cpp intropage.cpp @@ -10,31 +11,27 @@ set(WIZARD main.cpp mainwizard.cpp methodselectionpage.cpp + unshieldthread.cpp utils/componentlistwidget.cpp ) -# if(NOT WIN32) -# LIST(APPEND WIZARD unshieldthread.cpp) -# endif(NOT WIN32) set(WIZARD_HEADER componentselectionpage.hpp conclusionpage.hpp existinginstallationpage.hpp importpage.hpp + inisettings.hpp installationpage.hpp installationtargetpage.hpp intropage.hpp languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp + unshieldthread.hpp utils/componentlistwidget.hpp ) -# if(NOT WIN32) -# LIST(APPEND WIZARD_HEADER unshieldthread.hpp) -# endif(NOT WIN32) - # Headers that must be pre-processed set(WIZARD_HEADER_MOC @@ -48,15 +45,11 @@ set(WIZARD_HEADER_MOC languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp + unshieldthread.hpp utils/componentlistwidget.hpp ) -# if(NOT WIN32) -# LIST(APPEND WIZARD_HEADER_MOC unshieldthread.hpp) -# endif(NOT WIN32) - - set(WIZARD_UI ${CMAKE_SOURCE_DIR}/files/ui/wizard/componentselectionpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui @@ -86,10 +79,7 @@ QT4_WRAP_UI(UI_HDRS ${WIZARD_UI}) include(${QT_USE_FILE}) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) -if(NOT WIN32) - include_directories(${LIBUNSHIELD_INCLUDE}) -endif(NOT WIN32) +include_directories(${CMAKE_CURRENT_BINARY_DIR} ${LIBUNSHIELD_INCLUDE}) add_executable(openmw-wizard ${GUI_TYPE} @@ -101,16 +91,10 @@ add_executable(openmw-wizard ) target_link_libraries(openmw-wizard -# ${Boost_LIBRARIES} + ${Boost_LIBRARIES} ${QT_LIBRARIES} + ${LIBUNSHIELD_LIBRARY} ) -if(NOT WIN32) - target_link_libraries(openmw-wizard - ${LIBUNSHIELD_LIBRARY} - ) -endif(NOT WIN32) - - if(DPKG_PROGRAM) INSTALL(TARGETS openmw-wizard RUNTIME DESTINATION games COMPONENT openmw-wizard) diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp new file mode 100644 index 000000000..8bdac5ad0 --- /dev/null +++ b/apps/wizard/inisettings.cpp @@ -0,0 +1,68 @@ +#include "inisettings.hpp" + + +#include +#include +#include +#include +#include + +Wizard::IniSettings::IniSettings() +{ +} + +Wizard::IniSettings::~IniSettings() +{ +} + +bool Wizard::IniSettings::readFile(QTextStream &stream) +{ + // Look for a square bracket, "'\\[" + // that has one or more "not nothing" in it, "([^]]+)" + // and is closed with a square bracket, "\\]" + QRegExp sectionRe("^\\[([^]]+)\\]"); + + // Find any character(s) that is/are not equal sign(s), "[^=]+" + // followed by an optional whitespace, an equal sign, and another optional whitespace, "\\s*=\\s*" + // and one or more periods, "(.+)" + QRegExp keyRe("^([^=]+)\\s*=\\s*(.+)$"); + + QString currentSection; + + while (!stream.atEnd()) + { + const QString &line = stream.readLine(); + + if (line.isEmpty() || line.startsWith(";")) + continue; + + if (sectionRe.exactMatch(line)) + { + currentSection = sectionRe.cap(1); + } + else if (keyRe.indexIn(line) != -1) + { + QString key = keyRe.cap(1).trimmed(); + QString value = keyRe.cap(2).trimmed(); + + // Append the section, but only if there is one + if (!currentSection.isEmpty()) + key = currentSection + QLatin1Char('/') + key; + + mSettings[key] = QVariant(value); + } + } + + return true; +} + +bool Wizard::IniSettings::writeFile(QTextStream &stream) +{ + qDebug() << "test! " << stream.readAll(); + + while (!stream.atEnd()) { + qDebug() << "test! " << stream.readLine(); + } + + return true; +} diff --git a/apps/wizard/inisettings.hpp b/apps/wizard/inisettings.hpp new file mode 100644 index 000000000..66fe96ec1 --- /dev/null +++ b/apps/wizard/inisettings.hpp @@ -0,0 +1,45 @@ +#ifndef INISETTINGS_HPP +#define INISETTINGS_HPP + +#include +#include + +class QTextStream; + +namespace Wizard +{ + + typedef QHash SettingsMap; + + class IniSettings + { + public: + explicit IniSettings(); + ~IniSettings(); + + inline QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const + { + return mSettings.value(key, defaultValue); + } + + inline void setValue(const QString &key, const QVariant &value) + { + mSettings.insert(key, value); + } + + inline void remove(const QString &key) + { + mSettings.remove(key); + } + + bool readFile(QTextStream &stream); + bool writeFile(QTextStream &stream); + + private: + + SettingsMap mSettings; + }; + +} + +#endif // INISETTINGS_HPP diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 632b9c6a6..5d943dbfa 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,8 +1,12 @@ #include "installationpage.hpp" #include +#include +#include +#include #include "mainwizard.hpp" +#include "inisettings.hpp" Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : QWizardPage(wizard), @@ -19,6 +23,66 @@ void Wizard::InstallationPage::initializePage() logTextEdit->append(QString("Installing to %1").arg(path)); logTextEdit->append(QString("Installing %1.").arg(components.join(", "))); + installProgressBar->setMinimum(0); + + // Set the progressbar maximum to a multiple of 100 + // That way installing all three components would yield 300% + // When one component is done the bar will be filled by 33% + + if (field("installation.new").toBool() == true) + { + installProgressBar->setMaximum((components.count() * 100)); + } + else + { + if (components.contains("Tribunal") && mWizard->mInstallations[path]->hasTribunal == false) + installProgressBar->setMaximum(100); + + if (components.contains("Bloodmoon") && mWizard->mInstallations[path]->hasBloodmoon == false) + installProgressBar->setMaximum(installProgressBar->maximum() + 100); + } + + installProgressBar->setValue(100); + + // Test settings + IniSettings iniSettings; + + QFile file("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind/Morrowind.ini"); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + Please make sure you have the right permissions \ + and try again.
").arg(file.fileName())); + msgBox.exec(); + } + + QTextStream stream(&file); + + QString language = field("installation.language").toString(); + + if (language == QLatin1String("English") || + language == QLatin1String("German") || + language == QLatin1String("French")) + { + stream.setCodec(QTextCodec::codecForName("windows-1252")); + } + else if (language == QLatin1String("Russian")) + { + stream.setCodec(QTextCodec::codecForName("windows-1251")); + } + else if (language == QLatin1String("Polish")) + { + stream.setCodec(QTextCodec::codecForName("windows-1250")); + } + + iniSettings.readFile(stream); + + qDebug() << iniSettings.value("Game Files/GameFile0"); + } int Wizard::InstallationPage::nextId() const From 445f96434e537613234f7641ebaf28e9e0708a6f Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 24 Dec 2013 23:09:31 +0100 Subject: [PATCH 11/69] Added Morrowind.ini detection logic --- apps/wizard/componentselectionpage.cpp | 5 -- apps/wizard/componentselectionpage.hpp | 1 - apps/wizard/existinginstallationpage.cpp | 64 ++++++++++++++++++++---- apps/wizard/existinginstallationpage.hpp | 2 +- apps/wizard/inisettings.cpp | 2 +- apps/wizard/installationpage.cpp | 37 ++++++++------ apps/wizard/installationpage.hpp | 3 ++ apps/wizard/languageselectionpage.cpp | 16 +++++- apps/wizard/languageselectionpage.hpp | 3 ++ apps/wizard/mainwizard.cpp | 41 ++++++++++----- apps/wizard/mainwizard.hpp | 6 ++- files/ui/wizard/languageselectionpage.ui | 45 ++--------------- 12 files changed, 135 insertions(+), 90 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 50e6e8ef4..7d934fe84 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -21,11 +21,6 @@ Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : } -void Wizard::ComponentSelectionPage::debugMe(QString &text) -{ - qDebug() << "Debug Me" << text; -} - void Wizard::ComponentSelectionPage::updateButton(QListWidgetItem *item) { if (field("installation.new").toBool() == true) diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index f5e955782..8b4c186d0 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -19,7 +19,6 @@ namespace Wizard private slots: void updateButton(QListWidgetItem *item); - void debugMe(QString &text); private: MainWizard *mWizard; diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index f9eb7c283..aeaa5a1f6 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -1,7 +1,10 @@ #include "existinginstallationpage.hpp" #include +#include #include +#include +#include #include "mainwizard.hpp" @@ -32,16 +35,14 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() if (!info.exists()) return; - QDir dir(info.absolutePath()); - if (!dir.cdUp()) - return; // Cannot move out of the Data Files directory - - QString path = QDir::toNativeSeparators(dir.absolutePath()); + QString path(QDir::toNativeSeparators(info.absolutePath())); QList items = detectedList->findItems(path, Qt::MatchExactly); if (items.isEmpty()) { // Path is not yet in the list, add it + mWizard->addInstallation(path); + QListWidgetItem *item = new QListWidgetItem(path); detectedList->addItem(item); detectedList->setCurrentItem(item); // Select it too @@ -55,13 +56,13 @@ void Wizard::ExistingInstallationPage::textChanged(const QString &text) { // Set the installation path manually, as registerField doesn't work if (!text.isEmpty()) - wizard()->setField("installation.path", text); + mWizard->setField("installation.path", text); } void Wizard::ExistingInstallationPage::initializePage() { - QStringList paths = mWizard->mInstallations.keys(); + QStringList paths(mWizard->mInstallations.keys()); if (paths.isEmpty()) return; @@ -75,6 +76,50 @@ void Wizard::ExistingInstallationPage::initializePage() } +bool Wizard::ExistingInstallationPage::validatePage() +{ + // See if Morrowind.ini is detected, if not, ask the user + // It can be missing entirely + // Or failed to be detected due to the target being a symlink + + QString path(field("installation.path").toString()); + QFile file(mWizard->mInstallations[path]->iniPath); + + if (!file.exists()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error detecting Morrowind configuration")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Cancel); + msgBox.setText(QObject::tr("
Could not find Morrowind.ini

\ + The Wizard needs to update settings in this file.

\ + Press \"Browse...\" to specify the location manually.
")); + + QAbstractButton *browseButton = + msgBox.addButton(QObject::tr("B&rowse..."), QMessageBox::ActionRole); + + msgBox.exec(); + + QString iniFile; + if (msgBox.clickedButton() == browseButton) { + iniFile = QFileDialog::getOpenFileName( + NULL, + QObject::tr("Select configuration file"), + QDir::currentPath(), + QString(tr("Morrowind configuration file (*.ini)"))); + } + + if (iniFile.isEmpty()) { + return false; // Cancel was clicked; + } + + // A proper Morrowind.ini was selected, set it + QFileInfo info(iniFile); + mWizard->mInstallations[path]->iniPath = info.absoluteFilePath(); + } + + return true; +} + bool Wizard::ExistingInstallationPage::isComplete() const { if (detectedList->selectionModel()->hasSelection()) { @@ -86,14 +131,11 @@ bool Wizard::ExistingInstallationPage::isComplete() const int Wizard::ExistingInstallationPage::nextId() const { - QString path = field("installation.path").toString(); + QString path(field("installation.path").toString()); if (path.isEmpty()) return MainWizard::Page_ComponentSelection; - if (!mWizard->mInstallations.contains(path)) - return MainWizard::Page_ComponentSelection; - if (mWizard->mInstallations[path]->hasMorrowind == true && mWizard->mInstallations[path]->hasTribunal == true && mWizard->mInstallations[path]->hasBloodmoon == true) diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index 88d6913bc..d243bb868 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -17,6 +17,7 @@ namespace Wizard int nextId() const; virtual bool isComplete() const; + virtual bool validatePage(); private slots: void on_browseButton_clicked(); @@ -28,7 +29,6 @@ namespace Wizard protected: void initializePage(); - }; } diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index 8bdac5ad0..2c099acec 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -31,7 +31,7 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) while (!stream.atEnd()) { - const QString &line = stream.readLine(); + QString line(stream.readLine()); if (line.isEmpty() || line.startsWith(";")) continue; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 5d943dbfa..e98d75ba4 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,7 +1,9 @@ #include "installationpage.hpp" #include +#include #include +#include #include #include @@ -17,8 +19,8 @@ Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : void Wizard::InstallationPage::initializePage() { - QString path = field("installation.path").toString(); - QStringList components = field("installation.components").toStringList(); + QString path(field("installation.path").toString()); + QStringList components(field("installation.components").toStringList()); logTextEdit->append(QString("Installing to %1").arg(path)); logTextEdit->append(QString("Installing %1.").arg(components.join(", "))); @@ -44,45 +46,48 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setValue(100); + if (field("installation.new").toBool() == false) + setupSettings(); +} + +void Wizard::InstallationPage::setupSettings() +{ // Test settings IniSettings iniSettings; - QFile file("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind/Morrowind.ini"); + QString path(field("installation.path").toString()); + QFile file(mWizard->mInstallations[path]->iniPath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setWindowTitle(tr("Error opening Morrowind configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(QObject::tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); msgBox.exec(); + mWizard->close(); + return; } QTextStream stream(&file); - QString language = field("installation.language").toString(); + QString language(field("installation.language").toString()); - if (language == QLatin1String("English") || - language == QLatin1String("German") || - language == QLatin1String("French")) - { - stream.setCodec(QTextCodec::codecForName("windows-1252")); + if (language == QLatin1String("Polish")) { + stream.setCodec(QTextCodec::codecForName("windows-1250")); } - else if (language == QLatin1String("Russian")) - { + else if (language == QLatin1String("Russian")) { stream.setCodec(QTextCodec::codecForName("windows-1251")); } - else if (language == QLatin1String("Polish")) - { - stream.setCodec(QTextCodec::codecForName("windows-1250")); + else { + stream.setCodec(QTextCodec::codecForName("windows-1252")); } iniSettings.readFile(stream); qDebug() << iniSettings.value("Game Files/GameFile0"); - } int Wizard::InstallationPage::nextId() const diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index cb206e225..b4fecc3ba 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -20,9 +20,12 @@ namespace Wizard private: MainWizard *mWizard; + void setupSettings(); + protected: void initializePage(); + }; } diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index 903ab175f..102a4c39a 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -8,7 +8,21 @@ Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : { setupUi(this); - registerField(QLatin1String("installation.language"), languagesComboBox); + registerField(QLatin1String("installation.language"), languageComboBox); +} + +void Wizard::LanguageSelectionPage::initializePage() +{ + QStringList languages; + languages << "English" + << "French" + << "German" + << "Italian" + << "Polish" + << "Russian" + << "Spanish"; + + languageComboBox->addItems(languages); } int Wizard::LanguageSelectionPage::nextId() const diff --git a/apps/wizard/languageselectionpage.hpp b/apps/wizard/languageselectionpage.hpp index aa7a81ff1..3c17514f9 100644 --- a/apps/wizard/languageselectionpage.hpp +++ b/apps/wizard/languageselectionpage.hpp @@ -19,6 +19,9 @@ namespace Wizard private: MainWizard *mWizard; + + protected: + void initializePage(); }; } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index e752d0455..9b133cee1 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -38,21 +38,41 @@ void Wizard::MainWizard::setupInstallations() { // TODO: detect existing installations QStringList paths; - paths << QString("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind"); + paths << QString("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind/Data Files"); paths << QString("/home/pvdk/openmw/Data Files"); paths << QString("/usr/games/morrowind"); foreach (const QString &path, paths) { - Installation* install = new Installation(); + addInstallation(path); + } + +} - install->hasMorrowind = (findFiles(QString("Morrowind"), path)); - install->hasTribunal = true; - install->hasBloodmoon = false; +void Wizard::MainWizard::addInstallation(const QString &path) +{ + qDebug() << "add installation in: " << path; + Installation* install = new Installation(); + + install->hasMorrowind = findFiles(QLatin1String("Morrowind"), path); + install->hasTribunal = findFiles(QLatin1String("Tribunal"), path); + install->hasBloodmoon = findFiles(QLatin1String("Bloodmoon"), path); - mInstallations.insert(QDir::toNativeSeparators(path), install); + // Try to autodetect the Morrowind.ini location + QDir dir(path); + QFile file(dir.filePath("Morrowind.ini")); + + // Try the parent directory + // In normal Morrowind installations that's where Morrowind.ini is + if (!file.exists()) { + dir.cdUp(); + file.setFileName(dir.filePath(QLatin1String("Morrowind.ini"))); } + if (file.exists()) + install->iniPath = file.fileName(); + + mInstallations.insert(QDir::toNativeSeparators(path), install); } void Wizard::MainWizard::setupPages() @@ -76,16 +96,13 @@ bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) if (!dir.exists()) return false; - if (!dir.cd(QString("Data Files"))) - return false; - - qDebug() << "name: " << name + QString(".esm") << dir.absolutePath(); - // TODO: add MIME handling to make sure the files are real - if (dir.exists(name + QString(".esm")) && dir.exists(name + QString(".bsa"))) + if (dir.exists(name + QLatin1String(".esm")) && dir.exists(name + QLatin1String(".bsa"))) { + qDebug() << name << " exists!"; return true; } else { + qDebug() << name << " doesn't exist!"; return false; } diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index c6b541d35..c025c729e 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -16,6 +16,8 @@ namespace Wizard bool hasMorrowind; bool hasTribunal; bool hasBloodmoon; + + QString iniPath; }; enum { @@ -32,7 +34,9 @@ namespace Wizard MainWizard(QWidget *parent = 0); - static bool findFiles(const QString &name, const QString &path); + bool findFiles(const QString &name, const QString &path); + void addInstallation(const QString &path); + QMap mInstallations; private: diff --git a/files/ui/wizard/languageselectionpage.ui b/files/ui/wizard/languageselectionpage.ui index a76e3004b..fccd2aa42 100644 --- a/files/ui/wizard/languageselectionpage.ui +++ b/files/ui/wizard/languageselectionpage.ui @@ -6,8 +6,8 @@ 0 0 - 398 - 298 + 396 + 296 @@ -69,48 +69,13 @@ - + 250 0 - - - English - - - - - French - - - - - Russian - - - - - Polish - - - - - Italian - - - - - Spanish - - - - - German - - @@ -143,8 +108,6 @@ - - - + From 3a37761d76147bad3467e74c84911fe111ead020 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 24 Dec 2013 23:47:04 +0100 Subject: [PATCH 12/69] Some minor cleanups --- apps/wizard/existinginstallationpage.cpp | 19 ++++++++++--------- apps/wizard/mainwizard.cpp | 1 + apps/wizard/utils/componentlistwidget.hpp | 2 -- files/ui/wizard/existinginstallationpage.ui | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index aeaa5a1f6..5c66ceb4f 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -14,10 +14,10 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : { setupUi(this); - connect(detectedList, SIGNAL(currentTextChanged(QString)), + connect(installationsList, SIGNAL(currentTextChanged(QString)), this, SLOT(textChanged(QString))); - connect(detectedList,SIGNAL(itemSelectionChanged()), + connect(installationsList,SIGNAL(itemSelectionChanged()), this, SIGNAL(completeChanged())); } @@ -36,7 +36,7 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() return; QString path(QDir::toNativeSeparators(info.absolutePath())); - QList items = detectedList->findItems(path, Qt::MatchExactly); + QList items = installationsList->findItems(path, Qt::MatchExactly); if (items.isEmpty()) { @@ -44,10 +44,10 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() mWizard->addInstallation(path); QListWidgetItem *item = new QListWidgetItem(path); - detectedList->addItem(item); - detectedList->setCurrentItem(item); // Select it too + installationsList->addItem(item); + installationsList->setCurrentItem(item); // Select it too } else { - detectedList->setCurrentItem(items.first()); + installationsList->setCurrentItem(items.first()); } } @@ -55,6 +55,7 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() void Wizard::ExistingInstallationPage::textChanged(const QString &text) { // Set the installation path manually, as registerField doesn't work + // Because it doesn't accept two widgets operating on a single field if (!text.isEmpty()) mWizard->setField("installation.path", text); } @@ -67,11 +68,11 @@ void Wizard::ExistingInstallationPage::initializePage() if (paths.isEmpty()) return; - detectedList->clear(); + installationsList->clear(); foreach (const QString &path, paths) { QListWidgetItem *item = new QListWidgetItem(path); - detectedList->addItem(item); + installationsList->addItem(item); } } @@ -122,7 +123,7 @@ bool Wizard::ExistingInstallationPage::validatePage() bool Wizard::ExistingInstallationPage::isComplete() const { - if (detectedList->selectionModel()->hasSelection()) { + if (installationsList->selectionModel()->hasSelection()) { return true; } else { return false; diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 9b133cee1..fde82e998 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -29,6 +29,7 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); + setDefaultProperty("InstallationListWidget", "mCurrentText", "currentTextChanged"); setupInstallations(); setupPages(); diff --git a/apps/wizard/utils/componentlistwidget.hpp b/apps/wizard/utils/componentlistwidget.hpp index c9c782d45..6869629d3 100644 --- a/apps/wizard/utils/componentlistwidget.hpp +++ b/apps/wizard/utils/componentlistwidget.hpp @@ -24,6 +24,4 @@ private slots: void updateCheckedItems(QListWidgetItem *item); }; - - #endif // COMPONENTLISTWIDGET_HPP diff --git a/files/ui/wizard/existinginstallationpage.ui b/files/ui/wizard/existinginstallationpage.ui index 3ca831636..b887fb06c 100644 --- a/files/ui/wizard/existinginstallationpage.ui +++ b/files/ui/wizard/existinginstallationpage.ui @@ -6,8 +6,8 @@ 0 0 - 396 - 296 + 394 + 294 @@ -21,14 +21,14 @@ - + Detected installations: - + No existing installations detected From 095ff4e17a54176716ee35b3f4c14082b5da6567 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 25 Dec 2013 00:50:25 +0100 Subject: [PATCH 13/69] Moved launcher settings stuff into components, so they can be reused in the wizard --- apps/launcher/CMakeLists.txt | 5 ----- apps/launcher/datafilespage.cpp | 12 +++++------- apps/launcher/datafilespage.hpp | 12 ++++++------ apps/launcher/maindialog.hpp | 9 +++++---- apps/launcher/settings/graphicssettings.hpp | 4 ++-- components/CMakeLists.txt | 13 +++++++++---- .../config}/gamesettings.cpp | 14 +++++++------- .../config}/gamesettings.hpp | 2 +- .../config}/launchersettings.cpp | 10 +++++----- .../config}/launchersettings.hpp | 2 +- .../config}/settingsbase.hpp | 2 +- 11 files changed, 42 insertions(+), 43 deletions(-) rename {apps/launcher/settings => components/config}/gamesettings.cpp (91%) rename {apps/launcher/settings => components/config}/gamesettings.hpp (98%) rename {apps/launcher/settings => components/config}/launchersettings.cpp (86%) rename {apps/launcher/settings => components/config}/launchersettings.hpp (96%) rename {apps/launcher/settings => components/config}/settingsbase.hpp (99%) diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 18c555a24..279474406 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -6,9 +6,7 @@ set(LAUNCHER playpage.cpp textslotmsgbox.cpp - settings/gamesettings.cpp settings/graphicssettings.cpp - settings/launchersettings.cpp utils/checkablemessagebox.cpp utils/profilescombobox.cpp @@ -28,10 +26,7 @@ set(LAUNCHER_HEADER playpage.hpp textslotmsgbox.hpp - settings/gamesettings.hpp settings/graphicssettings.hpp - settings/launchersettings.hpp - settings/settingsbase.hpp utils/checkablemessagebox.hpp utils/profilescombobox.hpp diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 362d7562c..db5975102 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -9,18 +9,16 @@ #include #include - #include +#include + +#include +#include #include "utils/textinputdialog.hpp" #include "utils/profilescombobox.hpp" -#include "settings/gamesettings.hpp" -#include "settings/launchersettings.hpp" - -#include "components/contentselector/view/contentselector.hpp" - -Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, GameSettings &gameSettings, LauncherSettings &launcherSettings, QWidget *parent) +Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, QWidget *parent) : mCfgMgr(cfg) , mGameSettings(gameSettings) , mLauncherSettings(launcherSettings) diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 37603a210..642668cb7 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -14,12 +14,12 @@ class QMenu; namespace Files { struct ConfigurationManager; } namespace ContentSelectorView { class ContentSelector; } +namespace Config { class GameSettings; + class LauncherSettings; } namespace Launcher { class TextInputDialog; - class GameSettings; - class LauncherSettings; class ProfilesComboBox; class DataFilesPage : public QWidget @@ -30,8 +30,8 @@ namespace Launcher Ui::DataFilesPage ui; public: - explicit DataFilesPage (Files::ConfigurationManager &cfg, GameSettings &gameSettings, - LauncherSettings &launcherSettings, QWidget *parent = 0); + explicit DataFilesPage (Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, QWidget *parent = 0); QAbstractItemModel* profilesModel() const; @@ -62,8 +62,8 @@ namespace Launcher Files::ConfigurationManager &mCfgMgr; - GameSettings &mGameSettings; - LauncherSettings &mLauncherSettings; + Config::GameSettings &mGameSettings; + Config::LauncherSettings &mLauncherSettings; QString mDataLocal; diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index 5b8e4908e..a4fb4cd9a 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -5,9 +5,10 @@ #ifndef Q_MOC_RUN #include #endif -#include "settings/gamesettings.hpp" +#include +#include + #include "settings/graphicssettings.hpp" -#include "settings/launchersettings.hpp" #include "ui_mainwindow.h" @@ -60,9 +61,9 @@ namespace Launcher Files::ConfigurationManager mCfgMgr; - GameSettings mGameSettings; + Config::GameSettings mGameSettings; GraphicsSettings mGraphicsSettings; - LauncherSettings mLauncherSettings; + Config::LauncherSettings mLauncherSettings; }; } diff --git a/apps/launcher/settings/graphicssettings.hpp b/apps/launcher/settings/graphicssettings.hpp index 6f7c13547..a52e0aa84 100644 --- a/apps/launcher/settings/graphicssettings.hpp +++ b/apps/launcher/settings/graphicssettings.hpp @@ -1,11 +1,11 @@ #ifndef GRAPHICSSETTINGS_HPP #define GRAPHICSSETTINGS_HPP -#include "settingsbase.hpp" +#include namespace Launcher { - class GraphicsSettings : public SettingsBase > + class GraphicsSettings : public Config::SettingsBase > { public: GraphicsSettings(); diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 59fb084a8..bfa25072a 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -75,8 +75,8 @@ add_component_dir (loadinglistener ) add_component_dir (ogreinit - ogreinit ogreplugin - ) + ogreinit ogreplugin + ) set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ) @@ -84,11 +84,16 @@ set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui find_package(Qt4 COMPONENTS QtCore QtGui) if(QT_QTGUI_LIBRARY AND QT_QTCORE_LIBRARY) - add_component_qt_dir (contentselector + add_component_qt_dir (contentselector model/modelitem model/esmfile model/naturalsort model/contentmodel view/combobox view/contentselector - ) + ) + add_component_qt_dir (config + gamesettings + launchersettings + settingsbase + ) include(${QT_USE_FILE}) QT4_WRAP_UI(ESM_UI_HDR ${ESM_UI}) diff --git a/apps/launcher/settings/gamesettings.cpp b/components/config/gamesettings.cpp similarity index 91% rename from apps/launcher/settings/gamesettings.cpp rename to components/config/gamesettings.cpp index 41113c35a..62ae65df6 100644 --- a/apps/launcher/settings/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -27,16 +27,16 @@ namespace boost #endif /* (BOOST_VERSION <= 104600) */ -Launcher::GameSettings::GameSettings(Files::ConfigurationManager &cfg) +Config::GameSettings::GameSettings(Files::ConfigurationManager &cfg) : mCfgMgr(cfg) { } -Launcher::GameSettings::~GameSettings() +Config::GameSettings::~GameSettings() { } -void Launcher::GameSettings::validatePaths() +void Config::GameSettings::validatePaths() { if (mSettings.isEmpty() || !mDataDirs.isEmpty()) return; // Don't re-validate paths if they are already parsed @@ -82,14 +82,14 @@ void Launcher::GameSettings::validatePaths() } } -QStringList Launcher::GameSettings::values(const QString &key, const QStringList &defaultValues) +QStringList Config::GameSettings::values(const QString &key, const QStringList &defaultValues) { if (!mSettings.values(key).isEmpty()) return mSettings.values(key); return defaultValues; } -bool Launcher::GameSettings::readFile(QTextStream &stream) +bool Config::GameSettings::readFile(QTextStream &stream) { QMap cache; QRegExp keyRe("^([^=]+)\\s*=\\s*(.+)$"); @@ -131,7 +131,7 @@ bool Launcher::GameSettings::readFile(QTextStream &stream) return true; } -bool Launcher::GameSettings::writeFile(QTextStream &stream) +bool Config::GameSettings::writeFile(QTextStream &stream) { // Iterate in reverse order to preserve insertion order QMapIterator i(mSettings); @@ -170,7 +170,7 @@ bool Launcher::GameSettings::writeFile(QTextStream &stream) return true; } -bool Launcher::GameSettings::hasMaster() +bool Config::GameSettings::hasMaster() { bool result = false; QStringList content = mSettings.values(QString("content")); diff --git a/apps/launcher/settings/gamesettings.hpp b/components/config/gamesettings.hpp similarity index 98% rename from apps/launcher/settings/gamesettings.hpp rename to components/config/gamesettings.hpp index 60236200a..3ec32c1a5 100644 --- a/apps/launcher/settings/gamesettings.hpp +++ b/components/config/gamesettings.hpp @@ -14,7 +14,7 @@ namespace Files struct ConfigurationManager; } -namespace Launcher +namespace Config { class GameSettings { diff --git a/apps/launcher/settings/launchersettings.cpp b/components/config/launchersettings.cpp similarity index 86% rename from apps/launcher/settings/launchersettings.cpp rename to components/config/launchersettings.cpp index 705453555..14d66bba7 100644 --- a/apps/launcher/settings/launchersettings.cpp +++ b/components/config/launchersettings.cpp @@ -7,15 +7,15 @@ #include -Launcher::LauncherSettings::LauncherSettings() +Config::LauncherSettings::LauncherSettings() { } -Launcher::LauncherSettings::~LauncherSettings() +Config::LauncherSettings::~LauncherSettings() { } -QStringList Launcher::LauncherSettings::values(const QString &key, Qt::MatchFlags flags) +QStringList Config::LauncherSettings::values(const QString &key, Qt::MatchFlags flags) { QMap settings = SettingsBase::getSettings(); @@ -36,7 +36,7 @@ QStringList Launcher::LauncherSettings::values(const QString &key, Qt::MatchFlag return result; } -QStringList Launcher::LauncherSettings::subKeys(const QString &key) +QStringList Config::LauncherSettings::subKeys(const QString &key) { QMap settings = SettingsBase::getSettings(); QStringList keys = settings.uniqueKeys(); @@ -60,7 +60,7 @@ QStringList Launcher::LauncherSettings::subKeys(const QString &key) return result; } -bool Launcher::LauncherSettings::writeFile(QTextStream &stream) +bool Config::LauncherSettings::writeFile(QTextStream &stream) { QString sectionPrefix; QRegExp sectionRe("([^/]+)/(.+)$"); diff --git a/apps/launcher/settings/launchersettings.hpp b/components/config/launchersettings.hpp similarity index 96% rename from apps/launcher/settings/launchersettings.hpp rename to components/config/launchersettings.hpp index 8acc389a9..042823ca9 100644 --- a/apps/launcher/settings/launchersettings.hpp +++ b/components/config/launchersettings.hpp @@ -3,7 +3,7 @@ #include "settingsbase.hpp" -namespace Launcher +namespace Config { class LauncherSettings : public SettingsBase > { diff --git a/apps/launcher/settings/settingsbase.hpp b/components/config/settingsbase.hpp similarity index 99% rename from apps/launcher/settings/settingsbase.hpp rename to components/config/settingsbase.hpp index 3a1cf8e30..92ca34cdf 100644 --- a/apps/launcher/settings/settingsbase.hpp +++ b/components/config/settingsbase.hpp @@ -7,7 +7,7 @@ #include #include -namespace Launcher +namespace Config { template class SettingsBase From dddd9cba57833124994c19106ad2a32262904bf2 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 25 Dec 2013 15:50:02 +0100 Subject: [PATCH 14/69] Added data path retrieval from openmw.cfg, to use as existing installs --- apps/wizard/CMakeLists.txt | 1 + apps/wizard/mainwizard.cpp | 88 +++++++++++++++++++++++++++++++++----- apps/wizard/mainwizard.hpp | 12 ++++++ 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 7639cc8e1..4d1563ee7 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -94,6 +94,7 @@ target_link_libraries(openmw-wizard ${Boost_LIBRARIES} ${QT_LIBRARIES} ${LIBUNSHIELD_LIBRARY} + components ) if(DPKG_PROGRAM) diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index fde82e998..cbc9253ed 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -1,6 +1,10 @@ #include "mainwizard.hpp" #include + +#include +#include +#include #include #include "intropage.hpp" @@ -14,6 +18,7 @@ #include "conclusionpage.hpp" Wizard::MainWizard::MainWizard(QWidget *parent) : + mGameSettings(mCfgMgr), QWizard(parent) { @@ -29,7 +34,6 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); - setDefaultProperty("InstallationListWidget", "mCurrentText", "currentTextChanged"); setupInstallations(); setupPages(); @@ -37,15 +41,47 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : void Wizard::MainWizard::setupInstallations() { - // TODO: detect existing installations + QString userPath(QFile::decodeName(mCfgMgr.getUserPath().string().c_str())); + QString globalPath(QFile::decodeName(mCfgMgr.getGlobalPath().string().c_str())); + QStringList paths; - paths << QString("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind/Data Files"); - paths << QString("/home/pvdk/openmw/Data Files"); - paths << QString("/usr/games/morrowind"); + paths.append(userPath + QLatin1String("openmw.cfg")); + paths.append(QLatin1String("openmw.cfg")); + paths.append(globalPath + QLatin1String("openmw.cfg")); + + foreach (const QString &path, paths) { + qDebug() << "Loading config file:" << qPrintable(path); + + QFile file(path); + if (file.exists()) { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + Please make sure you have the right permissions \ + and try again.
").arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mGameSettings.readFile(stream); + } + file.close(); + } - foreach (const QString &path, paths) - { - addInstallation(path); + // Check if the paths actually contain data files + foreach (const QString path, mGameSettings.getDataDirs()) { + QDir dir(path); + QStringList filters; + filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; + + // Add to Wizard installations + if (!dir.entryList(filters).isEmpty()) + addInstallation(path); } } @@ -74,6 +110,10 @@ void Wizard::MainWizard::addInstallation(const QString &path) install->iniPath = file.fileName(); mInstallations.insert(QDir::toNativeSeparators(path), install); + + // Add it to the openmw.cfg too + mGameSettings.setMultiValue(QString("data"), path); + mGameSettings.addDataDir(path); } void Wizard::MainWizard::setupPages() @@ -89,6 +129,36 @@ void Wizard::MainWizard::setupPages() setPage(Page_Conclusion, new ConclusionPage(this)); setStartId(Page_Intro); } +void Wizard::MainWizard::accept() +{ + writeSettings(); + QWizard::accept(); +} + +void Wizard::MainWizard::writeSettings() +{ + QString userPath(QFile::decodeName(mCfgMgr.getUserPath().string().c_str())); + QFile file(userPath + QLatin1String("openmw.cfg")); + + if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { + // File cannot be opened or created + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("
Could not open or create %0 for writing

\ + Please make sure you have the right permissions \ + and try again.
").arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mGameSettings.writeFile(stream); + file.close(); +} bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) { @@ -100,10 +170,8 @@ bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) // TODO: add MIME handling to make sure the files are real if (dir.exists(name + QLatin1String(".esm")) && dir.exists(name + QLatin1String(".bsa"))) { - qDebug() << name << " exists!"; return true; } else { - qDebug() << name << " doesn't exist!"; return false; } diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index c025c729e..0d4c05fce 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -4,6 +4,9 @@ #include #include +#include +#include + namespace Wizard { @@ -39,10 +42,19 @@ namespace Wizard QMap mInstallations; + Files::ConfigurationManager mCfgMgr; + private: + void setupInstallations(); void setupPages(); + void writeSettings(); + + Config::GameSettings mGameSettings; + + private slots: + void accept(); }; From 40486370d951fd68a29e30d078e43f2e13c407e4 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 25 Dec 2013 18:52:34 +0100 Subject: [PATCH 15/69] Working on the Unshield functionality --- apps/wizard/installationpage.cpp | 71 ++++++++++++++++++++++++++++- apps/wizard/installationpage.hpp | 6 +++ apps/wizard/unshieldthread.cpp | 61 +++++++++++++++++++++++++ apps/wizard/unshieldthread.hpp | 47 +++++++++++++++++++ files/ui/wizard/installationpage.ui | 6 +-- 5 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 apps/wizard/unshieldthread.cpp create mode 100644 apps/wizard/unshieldthread.hpp diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index e98d75ba4..bff465fee 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -9,6 +9,7 @@ #include "mainwizard.hpp" #include "inisettings.hpp" +#include "unshieldthread.hpp" Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : QWizardPage(wizard), @@ -37,10 +38,12 @@ void Wizard::InstallationPage::initializePage() } else { - if (components.contains("Tribunal") && mWizard->mInstallations[path]->hasTribunal == false) + if (components.contains(QLatin1String("Tribunal")) + && mWizard->mInstallations[path]->hasTribunal == false) installProgressBar->setMaximum(100); - if (components.contains("Bloodmoon") && mWizard->mInstallations[path]->hasBloodmoon == false) + if (components.contains(QLatin1String("Bloodmoon")) + && mWizard->mInstallations[path]->hasBloodmoon == false) installProgressBar->setMaximum(installProgressBar->maximum() + 100); } @@ -48,6 +51,9 @@ void Wizard::InstallationPage::initializePage() if (field("installation.new").toBool() == false) setupSettings(); + + startInstallation(); + } void Wizard::InstallationPage::setupSettings() @@ -90,6 +96,67 @@ void Wizard::InstallationPage::setupSettings() qDebug() << iniSettings.value("Game Files/GameFile0"); } +void Wizard::InstallationPage::startInstallation() +{ + QStringList components(field("installation.components").toStringList()); + QString path(field("installation.path").toString()); + + UnshieldThread *unshield = new UnshieldThread(); + + connect(unshield, SIGNAL(finished()), + unshield, SLOT(deleteLater())); + + connect(unshield, SIGNAL(finished()), + this, SLOT(installationFinished())); + + connect(unshield, SIGNAL(textChanged(QString)), + installProgressLabel, SLOT(setText(QString))); + + connect(unshield, SIGNAL(textChanged(QString)), + logTextEdit, SLOT(append(QString))); + + if (field("installation.new").toBool() == true) + { + // Always install Morrowind + unshield->setInstallMorrowind(true); + + if (components.contains(QLatin1String("Tribunal"))) + unshield->setInstallTribunal(true); + + if (components.contains(QLatin1String("Bloodmoon"))) + unshield->setInstallBloodmoon(true); + } else { + // Morrowind should already be installed + unshield->setInstallMorrowind(false); + + if (components.contains(QLatin1String("Tribunal")) + && mWizard->mInstallations[path]->hasTribunal == false) + unshield->setInstallTribunal(false); + + if (components.contains(QLatin1String("Bloodmoon")) + && mWizard->mInstallations[path]->hasBloodmoon == false) + unshield->setInstallBloodmoon(true); + } + + + // Set the installation target path + unshield->setPath(path); + + unshield->start(); + +} + +void Wizard::InstallationPage::installationFinished() +{ + qDebug() << "Installation finished!"; + mFinished = true; +} + +bool Wizard::InstallationPage::isComplete() const +{ + return mFinished; +} + int Wizard::InstallationPage::nextId() const { return MainWizard::Page_Import; diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index b4fecc3ba..022b34c64 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -16,11 +16,17 @@ namespace Wizard InstallationPage(MainWizard *wizard); int nextId() const; + virtual bool isComplete() const; private: MainWizard *mWizard; + bool mFinished; void setupSettings(); + void startInstallation(); + + private slots: + void installationFinished(); protected: void initializePage(); diff --git a/apps/wizard/unshieldthread.cpp b/apps/wizard/unshieldthread.cpp new file mode 100644 index 000000000..23257e2d8 --- /dev/null +++ b/apps/wizard/unshieldthread.cpp @@ -0,0 +1,61 @@ +#include "unshieldthread.hpp" + +#include +#include + +Wizard::UnshieldThread::UnshieldThread(QObject *parent) : + QThread(parent) +{ + unshield_set_log_level(0); + + mInstallMorrowind = false; + mInstallTribunal = false; + mInstallBloodmoon = false; + +} + +void Wizard::UnshieldThread::setInstallMorrowind(bool install) +{ + mInstallMorrowind = install; +} + +void Wizard::UnshieldThread::setInstallTribunal(bool install) +{ + mInstallTribunal = install; +} + +void Wizard::UnshieldThread::setInstallBloodmoon(bool install) +{ + mInstallBloodmoon = install; +} + +void Wizard::UnshieldThread::setPath(const QString &path) +{ + mPath = path; +} + +void Wizard::UnshieldThread::extract() +{ + emit textChanged(QLatin1String("Starting installation")); + emit textChanged(QLatin1String("Installation target: ") + mPath); + + QStringList components; + if (mInstallMorrowind) + components << QLatin1String("Morrowind"); + + if (mInstallTribunal) + components << QLatin1String("Tribunal"); + + if (mInstallBloodmoon) + components << QLatin1String("Bloodmoon"); + + emit textChanged(QLatin1String("Components: ") + components.join(QLatin1String(", "))); + +} + +void Wizard::UnshieldThread::run() +{ + qDebug() << "From worker thread: " << currentThreadId(); + + extract(); +} diff --git a/apps/wizard/unshieldthread.hpp b/apps/wizard/unshieldthread.hpp new file mode 100644 index 000000000..764a99e6a --- /dev/null +++ b/apps/wizard/unshieldthread.hpp @@ -0,0 +1,47 @@ +#ifndef UNSHIELDTHREAD_HPP +#define UNSHIELDTHREAD_HPP + +#include + +#include + +namespace Wizard +{ + + class UnshieldThread : public QThread + { + Q_OBJECT + public: + explicit UnshieldThread(QObject *parent = 0); + + void setInstallMorrowind(bool install); + void setInstallTribunal(bool install); + void setInstallBloodmoon(bool install); + + void setPath(const QString &path); + + private: + void extract(); + + void extractCab(const QString &cabFile, + const QString &outputDir, bool extractIni); + //void extractFile(Unshield *unshield, + // ) + + bool mInstallMorrowind; + bool mInstallTribunal; + bool mInstallBloodmoon; + + QString mPath; + + protected: + virtual void run(); + + signals: + void textChanged(const QString &text); + + }; + +} + +#endif // UNSHIELDTHREAD_HPP diff --git a/files/ui/wizard/installationpage.ui b/files/ui/wizard/installationpage.ui index 3e94c1e30..6c1a3c493 100644 --- a/files/ui/wizard/installationpage.ui +++ b/files/ui/wizard/installationpage.ui @@ -6,8 +6,8 @@ 0 0 - 518 - 423 + 516 + 421
@@ -21,7 +21,7 @@ - + Extracting: %1 From 3a55d88bac1ed3cbb4a10b02a9698f8e74d0cc23 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 26 Dec 2013 18:02:34 +0100 Subject: [PATCH 16/69] Working on the installshield thread --- apps/wizard/installationpage.cpp | 74 ++++-------- apps/wizard/installationpage.hpp | 3 +- apps/wizard/unshieldthread.cpp | 187 ++++++++++++++++++++++++++++++- apps/wizard/unshieldthread.hpp | 22 +++- 4 files changed, 229 insertions(+), 57 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index bff465fee..3ac9b05c8 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,11 +1,7 @@ #include "installationpage.hpp" #include -#include -#include -#include #include -#include #include "mainwizard.hpp" #include "inisettings.hpp" @@ -16,6 +12,8 @@ Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : mWizard(wizard) { setupUi(this); + + mFinished = false; } void Wizard::InstallationPage::initializePage() @@ -49,51 +47,7 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setValue(100); - if (field("installation.new").toBool() == false) - setupSettings(); - startInstallation(); - -} - -void Wizard::InstallationPage::setupSettings() -{ - // Test settings - IniSettings iniSettings; - - QString path(field("installation.path").toString()); - QFile file(mWizard->mInstallations[path]->iniPath); - - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error opening Morrowind configuration file")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); - mWizard->close(); - return; - } - - QTextStream stream(&file); - - QString language(field("installation.language").toString()); - - if (language == QLatin1String("Polish")) { - stream.setCodec(QTextCodec::codecForName("windows-1250")); - } - else if (language == QLatin1String("Russian")) { - stream.setCodec(QTextCodec::codecForName("windows-1251")); - } - else { - stream.setCodec(QTextCodec::codecForName("windows-1252")); - } - - iniSettings.readFile(stream); - - qDebug() << iniSettings.value("Game Files/GameFile0"); } void Wizard::InstallationPage::startInstallation() @@ -115,6 +69,9 @@ void Wizard::InstallationPage::startInstallation() connect(unshield, SIGNAL(textChanged(QString)), logTextEdit, SLOT(append(QString))); + connect(unshield, SIGNAL(progressChanged(int)), + installProgressBar, SLOT(setValue(int))); + if (field("installation.new").toBool() == true) { // Always install Morrowind @@ -131,25 +88,40 @@ void Wizard::InstallationPage::startInstallation() if (components.contains(QLatin1String("Tribunal")) && mWizard->mInstallations[path]->hasTribunal == false) - unshield->setInstallTribunal(false); + unshield->setInstallTribunal(true); if (components.contains(QLatin1String("Bloodmoon")) && mWizard->mInstallations[path]->hasBloodmoon == false) unshield->setInstallBloodmoon(true); - } + // Set the location of the Morrowind.ini to update + unshield->setIniPath(mWizard->mInstallations[path]->iniPath); + } // Set the installation target path unshield->setPath(path); + // Set the right codec to use for Morrowind.ini + QString language(field("installation.language").toString()); + + if (language == QLatin1String("Polish")) { + unshield->setIniCodec(QTextCodec::codecForName("windows-1250")); + } + else if (language == QLatin1String("Russian")) { + unshield->setIniCodec(QTextCodec::codecForName("windows-1251")); + } + else { + unshield->setIniCodec(QTextCodec::codecForName("windows-1252")); + } + unshield->start(); } void Wizard::InstallationPage::installationFinished() { - qDebug() << "Installation finished!"; mFinished = true; + emit completeChanged(); } bool Wizard::InstallationPage::isComplete() const diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 022b34c64..42b2be819 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -4,10 +4,12 @@ #include #include "ui_installationpage.h" +#include "inisettings.hpp" namespace Wizard { class MainWizard; + class IniSettings; class InstallationPage : public QWizardPage, private Ui::InstallationPage { @@ -22,7 +24,6 @@ namespace Wizard MainWizard *mWizard; bool mFinished; - void setupSettings(); void startInstallation(); private slots: diff --git a/apps/wizard/unshieldthread.cpp b/apps/wizard/unshieldthread.cpp index 23257e2d8..d759b56a6 100644 --- a/apps/wizard/unshieldthread.cpp +++ b/apps/wizard/unshieldthread.cpp @@ -1,13 +1,28 @@ #include "unshieldthread.hpp" #include + +#include #include +#include +#include +#include +#include + +#include Wizard::UnshieldThread::UnshieldThread(QObject *parent) : - QThread(parent) + QThread(parent), + mIniSettings() { unshield_set_log_level(0); + mPath = QString(); + mIniPath = QString(); + + // Default to Latin encoding + mIniCodec = QTextCodec::codecForName("windows-1252"); + mInstallMorrowind = false; mInstallTribunal = false; mInstallBloodmoon = false; @@ -34,6 +49,43 @@ void Wizard::UnshieldThread::setPath(const QString &path) mPath = path; } +void Wizard::UnshieldThread::setIniPath(const QString &path) +{ + mIniPath = path; +} + +void Wizard::UnshieldThread::setIniCodec(QTextCodec *codec) +{ + mIniCodec = codec; +} + +void Wizard::UnshieldThread::setupSettings() +{ + // Create Morrowind.ini settings map + if (mIniPath.isEmpty()) + return; + + QFile file(mIniPath); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + // TODO: Emit error signal + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening Morrowind configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + Please make sure you have the right permissions \ + and try again.
").arg(file.fileName())); + msgBox.exec(); + return; + } + + QTextStream stream(&file); + stream.setCodec(mIniCodec); + + mIniSettings.readFile(stream); +} + void Wizard::UnshieldThread::extract() { emit textChanged(QLatin1String("Starting installation")); @@ -51,11 +103,144 @@ void Wizard::UnshieldThread::extract() emit textChanged(QLatin1String("Components: ") + components.join(QLatin1String(", "))); + emit textChanged(QLatin1String("Updating Morrowind.ini: ") + mIniPath); + + //emit progressChanged(45); + + /// +// bfs::path outputDataFilesDir = mOutputPath; +// outputDataFilesDir /= "Data Files"; +// bfs::path extractPath = mOutputPath; +// extractPath /= "extract-temp"; + + // Create temporary extract directory + // TODO: Use QTemporaryDir in Qt 5.0 + QString tempPath(mPath + QLatin1String("/extract-temp")); + QDir dir; + dir.mkpath(tempPath); + + if (mInstallMorrowind) + { + QString morrowindTempPath(tempPath + QLatin1String("/morrowind")); + QString morrowindCab(QLatin1String("/mnt/cdrom/data1.hdr")); + + extractCab(morrowindCab, morrowindTempPath); + + // TODO: Throw error; + // Move the files from the temporary path to the destination folder + + qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; + if (!dir.rename(morrowindTempPath, mPath)) + qDebug() << "failed!"; + + } + +// if(!mMorrowindDone && mMorrowindPath.string().length() > 0) +// { +// mMorrowindDone = true; + +// bfs::path mwExtractPath = extractPath / "morrowind"; +// extract_cab(mMorrowindPath, mwExtractPath, true); + +// bfs::path dFilesDir = findFile(mwExtractPath, "morrowind.esm").parent_path(); + +// installToPath(dFilesDir, outputDataFilesDir); + +// install_dfiles_outside(mwExtractPath, outputDataFilesDir); + +// // Videos are often kept uncompressed on the cd +// bfs::path videosPath = findFile(mMorrowindPath.parent_path(), "video", false); +// if(videosPath.string() != "") +// { +// emit signalGUI(QString("Installing Videos...")); +// installToPath(videosPath, outputDataFilesDir / "Video", true); +// } + +// bfs::path cdDFiles = findFile(mMorrowindPath.parent_path(), "data files", false); +// if(cdDFiles.string() != "") +// { +// emit signalGUI(QString("Installing Uncompressed Data files from CD...")); +// installToPath(cdDFiles, outputDataFilesDir, true); +// } + + +// bfs::rename(findFile(mwExtractPath, "morrowind.ini"), outputDataFilesDir / "Morrowind.ini"); + +// mTribunalDone = contains(outputDataFilesDir, "tribunal.esm"); +// mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); + +// } + + + + /// + +} + +bool Wizard::UnshieldThread::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) +{ + bool success; + QString path(outputDir); + path.append('/'); + + int directory = unshield_file_directory(unshield, index); + + if (!prefix.isEmpty()) + path.append(prefix + QLatin1Char('/')); + + if (directory >= 0) + path.append(QString::fromLatin1(unshield_directory_name(unshield, directory)) + QLatin1Char('/')); + + // Ensure the target path exists + QDir dir; + dir.mkpath(path); + + QString fileName(path); + fileName.append(QString::fromLatin1(unshield_file_name(unshield, index))); + + // Calculate the percentage done + int progress = qCeil(((float) counter / (float) unshield_file_count(unshield)) * 100); + + qDebug() << progress << counter << unshield_file_count(unshield); + + emit textChanged(QLatin1String("Extracting: ") + QString::fromLatin1(unshield_file_name(unshield, index))); + emit progressChanged(progress); + + success = unshield_file_save(unshield, index, fileName.toLatin1().constData()); + + if (!success) + dir.remove(fileName); + + return success; +} + +void Wizard::UnshieldThread::extractCab(const QString &cabFile, const QString &outputDir) +{ + Unshield *unshield; + unshield = unshield_open(cabFile.toLatin1().constData()); + + int counter = 0; + + for (int i=0; ifirst_file; j<=group->last_file; ++j) + { + if (unshield_file_is_valid(unshield, j)) { + extractFile(unshield, outputDir, group->name, j, counter); + ++counter; + } + } + } + + unshield_close(unshield); } void Wizard::UnshieldThread::run() { qDebug() << "From worker thread: " << currentThreadId(); + setupSettings(); extract(); } diff --git a/apps/wizard/unshieldthread.hpp b/apps/wizard/unshieldthread.hpp index 764a99e6a..7f7250875 100644 --- a/apps/wizard/unshieldthread.hpp +++ b/apps/wizard/unshieldthread.hpp @@ -5,6 +5,10 @@ #include +#include "inisettings.hpp" + +class QTextCodec; + namespace Wizard { @@ -19,26 +23,36 @@ namespace Wizard void setInstallBloodmoon(bool install); void setPath(const QString &path); + void setIniPath(const QString &path); + + void setIniCodec(QTextCodec *codec); private: + + void setupSettings(); void extract(); - void extractCab(const QString &cabFile, - const QString &outputDir, bool extractIni); - //void extractFile(Unshield *unshield, - // ) + void extractCab(const QString &cabFile, const QString &outputDir); + bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); + bool mInstallMorrowind; bool mInstallTribunal; bool mInstallBloodmoon; QString mPath; + QString mIniPath; + + IniSettings mIniSettings; + + QTextCodec *mIniCodec; protected: virtual void run(); signals: void textChanged(const QString &text); + void progressChanged(int progress); }; From a2ca7b92a227530579c442cf553631dcccdb7303 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 1 Jan 2014 17:15:34 +0100 Subject: [PATCH 17/69] Working on the unshield implementation, added support for moving temp files --- apps/wizard/unshieldthread.cpp | 140 +++++++++++++++++++++++++++++++-- apps/wizard/unshieldthread.hpp | 5 ++ 2 files changed, 140 insertions(+), 5 deletions(-) diff --git a/apps/wizard/unshieldthread.cpp b/apps/wizard/unshieldthread.cpp index d759b56a6..0f8dbc477 100644 --- a/apps/wizard/unshieldthread.cpp +++ b/apps/wizard/unshieldthread.cpp @@ -2,6 +2,8 @@ #include +#include +#include #include #include #include @@ -86,6 +88,91 @@ void Wizard::UnshieldThread::setupSettings() mIniSettings.readFile(stream); } +bool Wizard::UnshieldThread::removeDirectory(const QString &dirName) +{ + bool result = true; + QDir dir(dirName); + + if (dir.exists(dirName)) + { + QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, QDir::DirsFirst)); + foreach(QFileInfo info, list) { + if (info.isDir()) { + result = removeDirectory(info.absoluteFilePath()); + } else { + result = QFile::remove(info.absoluteFilePath()); + } + + if (!result) + return result; + } + + result = dir.rmdir(dirName); + } + return result; +} + +bool Wizard::UnshieldThread::moveFile(const QString &source, const QString &destination) +{ + QDir dir; + QFile file; + + if (dir.rename(source, destination)) { + return true; + } else { + if (file.copy(source, destination)) { + return file.remove(source); + } else { + qDebug() << "copy failed! " << file.errorString(); + } + } + + return false; +} + +bool Wizard::UnshieldThread::moveDirectory(const QString &source, const QString &destination) +{ + QDir sourceDir(source); + QDir destDir(destination); + + if (!destDir.exists()) { + sourceDir.mkpath(destination); + destDir.refresh(); + } + + if (!destDir.exists()) + return false; + + bool result = true; + + QFileInfoList list(sourceDir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, QDir::DirsFirst)); + + foreach (const QFileInfo &info, list) { + QString relativePath(info.absoluteFilePath()); + relativePath.remove(source); + + if (info.isSymLink()) + continue; + + if (info.isDir()) { + result = moveDirectory(info.absoluteFilePath(), destDir.absolutePath() + relativePath); + } else { + qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; + + result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); + } + + //if (!result) + // return result; + } + + return result && removeDirectory(sourceDir.absolutePath()); +} + void Wizard::UnshieldThread::extract() { emit textChanged(QLatin1String("Starting installation")); @@ -129,10 +216,48 @@ void Wizard::UnshieldThread::extract() // TODO: Throw error; // Move the files from the temporary path to the destination folder - qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; - if (!dir.rename(morrowindTempPath, mPath)) + //qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; + morrowindTempPath.append(QDir::separator() + QLatin1String("Data Files")); + if (!moveDirectory(morrowindTempPath, mPath)) qDebug() << "failed!"; + QDir source(QLatin1String("/mnt/cdrom/")); + QStringList directories; + directories << QLatin1String("Fonts") + << QLatin1String("Music") + << QLatin1String("Sound") + << QLatin1String("Splash"); + + QFileInfoList list(sourceDir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs)); + + + foreach(QFileInfo info, list) { + if (info.isSymLink()) + continue; + + if (directories.contains(info.fileName())) + copyDirectory(info.absoluteFilePath(), mPath); + } + + + bfs::path fonts = findFile(from, "fonts", false); + if(fonts.string() != "") + installToPath(fonts, dFiles / "Fonts"); + + bfs::path music = findFile(from, "music", false); + if(music.string() != "") + installToPath(music, dFiles / "Music"); + + bfs::path sound = findFile(from, "sound", false); + if(sound.string() != "") + installToPath(sound, dFiles / "Sound"); + + bfs::path splash = findFile(from, "splash", false); + if(splash.string() != "") + installToPath(splash, dFiles / "Splash"); + } // if(!mMorrowindDone && mMorrowindPath.string().length() > 0) @@ -181,16 +306,21 @@ bool Wizard::UnshieldThread::extractFile(Unshield *unshield, const QString &outp { bool success; QString path(outputDir); - path.append('/'); + path.append(QDir::separator()); int directory = unshield_file_directory(unshield, index); if (!prefix.isEmpty()) - path.append(prefix + QLatin1Char('/')); + path.append(prefix + QDir::separator()); if (directory >= 0) - path.append(QString::fromLatin1(unshield_directory_name(unshield, directory)) + QLatin1Char('/')); + path.append(QString::fromLatin1(unshield_directory_name(unshield, directory)) + QDir::separator()); + + // Ensure the path has the right separators + path.replace(QLatin1Char('\\'), QDir::separator()); + path = QDir::toNativeSeparators(path); + qDebug() << "path is: " << path << QString::fromLatin1(unshield_directory_name(unshield, directory)) + QLatin1Char('/'); // Ensure the target path exists QDir dir; dir.mkpath(path); diff --git a/apps/wizard/unshieldthread.hpp b/apps/wizard/unshieldthread.hpp index 7f7250875..cc3740ad8 100644 --- a/apps/wizard/unshieldthread.hpp +++ b/apps/wizard/unshieldthread.hpp @@ -29,6 +29,11 @@ namespace Wizard private: + bool removeDirectory(const QString &dirName); + + bool moveFile(const QString &source, const QString &destination); + bool moveDirectory(const QString &source, const QString &destination); + void setupSettings(); void extract(); From 691e007524591eec04d43fb8faabc38cc0046973 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 1 Jan 2014 22:46:29 +0100 Subject: [PATCH 18/69] Redid the unshield thread stuff, this time the way it should be done --- apps/wizard/CMakeLists.txt | 9 ++- apps/wizard/installationpage.cpp | 19 ++++- .../unshieldworker.cpp} | 79 ++++++++----------- .../unshieldworker.hpp} | 28 ++++--- 4 files changed, 69 insertions(+), 66 deletions(-) rename apps/wizard/{unshieldthread.cpp => unshield/unshieldworker.cpp} (82%) rename apps/wizard/{unshieldthread.hpp => unshield/unshieldworker.hpp} (78%) diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 4d1563ee7..f2defd031 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -11,7 +11,8 @@ set(WIZARD main.cpp mainwizard.cpp methodselectionpage.cpp - unshieldthread.cpp + + unshield/unshieldworker.cpp utils/componentlistwidget.cpp ) @@ -28,7 +29,8 @@ set(WIZARD_HEADER languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp - unshieldthread.hpp + + unshield/unshieldworker.hpp utils/componentlistwidget.hpp ) @@ -45,7 +47,8 @@ set(WIZARD_HEADER_MOC languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp - unshieldthread.hpp + + unshield/unshieldworker.hpp utils/componentlistwidget.hpp ) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 3ac9b05c8..374c8180c 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -5,7 +5,7 @@ #include "mainwizard.hpp" #include "inisettings.hpp" -#include "unshieldthread.hpp" +#include "unshield/unshieldworker.hpp" Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : QWizardPage(wizard), @@ -55,11 +55,23 @@ void Wizard::InstallationPage::startInstallation() QStringList components(field("installation.components").toStringList()); QString path(field("installation.path").toString()); - UnshieldThread *unshield = new UnshieldThread(); + QThread* thread = new QThread(); + UnshieldWorker* unshield = new UnshieldWorker(); + + unshield->moveToThread(thread); + + connect(thread, SIGNAL(started()), + unshield, SLOT(extract())); + + connect(unshield, SIGNAL(finished()), + thread, SLOT(quit())); connect(unshield, SIGNAL(finished()), unshield, SLOT(deleteLater())); + connect(unshield, SIGNAL(finished()), + thread, SLOT(deleteLater())); + connect(unshield, SIGNAL(finished()), this, SLOT(installationFinished())); @@ -114,12 +126,13 @@ void Wizard::InstallationPage::startInstallation() unshield->setIniCodec(QTextCodec::codecForName("windows-1252")); } - unshield->start(); + thread->start(); } void Wizard::InstallationPage::installationFinished() { + qDebug() << "finished!"; mFinished = true; emit completeChanged(); } diff --git a/apps/wizard/unshieldthread.cpp b/apps/wizard/unshield/unshieldworker.cpp similarity index 82% rename from apps/wizard/unshieldthread.cpp rename to apps/wizard/unshield/unshieldworker.cpp index 0f8dbc477..71beaf9b8 100644 --- a/apps/wizard/unshieldthread.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -1,4 +1,4 @@ -#include "unshieldthread.hpp" +#include "unshieldworker.hpp" #include @@ -10,11 +10,12 @@ #include #include #include +#include #include -Wizard::UnshieldThread::UnshieldThread(QObject *parent) : - QThread(parent), +Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : + QObject(parent), mIniSettings() { unshield_set_log_level(0); @@ -28,40 +29,44 @@ Wizard::UnshieldThread::UnshieldThread(QObject *parent) : mInstallMorrowind = false; mInstallTribunal = false; mInstallBloodmoon = false; +} + +Wizard::UnshieldWorker::~UnshieldWorker() +{ } -void Wizard::UnshieldThread::setInstallMorrowind(bool install) +void Wizard::UnshieldWorker::setInstallMorrowind(bool install) { mInstallMorrowind = install; } -void Wizard::UnshieldThread::setInstallTribunal(bool install) +void Wizard::UnshieldWorker::setInstallTribunal(bool install) { mInstallTribunal = install; } -void Wizard::UnshieldThread::setInstallBloodmoon(bool install) +void Wizard::UnshieldWorker::setInstallBloodmoon(bool install) { mInstallBloodmoon = install; } -void Wizard::UnshieldThread::setPath(const QString &path) +void Wizard::UnshieldWorker::setPath(const QString &path) { mPath = path; } -void Wizard::UnshieldThread::setIniPath(const QString &path) +void Wizard::UnshieldWorker::setIniPath(const QString &path) { mIniPath = path; } -void Wizard::UnshieldThread::setIniCodec(QTextCodec *codec) +void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) { mIniCodec = codec; } -void Wizard::UnshieldThread::setupSettings() +void Wizard::UnshieldWorker::setupSettings() { // Create Morrowind.ini settings map if (mIniPath.isEmpty()) @@ -88,7 +93,7 @@ void Wizard::UnshieldThread::setupSettings() mIniSettings.readFile(stream); } -bool Wizard::UnshieldThread::removeDirectory(const QString &dirName) +bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) { bool result = true; QDir dir(dirName); @@ -114,7 +119,7 @@ bool Wizard::UnshieldThread::removeDirectory(const QString &dirName) return result; } -bool Wizard::UnshieldThread::moveFile(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::moveFile(const QString &source, const QString &destination) { QDir dir; QFile file; @@ -132,7 +137,7 @@ bool Wizard::UnshieldThread::moveFile(const QString &source, const QString &dest return false; } -bool Wizard::UnshieldThread::moveDirectory(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString &destination) { QDir sourceDir(source); QDir destDir(destination); @@ -173,7 +178,7 @@ bool Wizard::UnshieldThread::moveDirectory(const QString &source, const QString return result && removeDirectory(sourceDir.absolutePath()); } -void Wizard::UnshieldThread::extract() +void Wizard::UnshieldWorker::extract() { emit textChanged(QLatin1String("Starting installation")); emit textChanged(QLatin1String("Installation target: ") + mPath); @@ -211,22 +216,23 @@ void Wizard::UnshieldThread::extract() QString morrowindTempPath(tempPath + QLatin1String("/morrowind")); QString morrowindCab(QLatin1String("/mnt/cdrom/data1.hdr")); - extractCab(morrowindCab, morrowindTempPath); + //extractCab(morrowindCab, morrowindTempPath); // TODO: Throw error; // Move the files from the temporary path to the destination folder //qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; morrowindTempPath.append(QDir::separator() + QLatin1String("Data Files")); - if (!moveDirectory(morrowindTempPath, mPath)) - qDebug() << "failed!"; +// if (!moveDirectory(morrowindTempPath, mPath)) +// qDebug() << "failed!"; - QDir source(QLatin1String("/mnt/cdrom/")); + QDir sourceDir(QLatin1String("/mnt/cdrom/")); QStringList directories; directories << QLatin1String("Fonts") << QLatin1String("Music") << QLatin1String("Sound") - << QLatin1String("Splash"); + << QLatin1String("Splash") + << QLatin1String("Video"); QFileInfoList list(sourceDir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | @@ -237,27 +243,13 @@ void Wizard::UnshieldThread::extract() if (info.isSymLink()) continue; + qDebug() << "not found " << info.fileName(); + if (directories.contains(info.fileName())) - copyDirectory(info.absoluteFilePath(), mPath); + qDebug() << "found " << info.fileName(); +// copyDirectory(info.absoluteFilePath(), mPath); } - - bfs::path fonts = findFile(from, "fonts", false); - if(fonts.string() != "") - installToPath(fonts, dFiles / "Fonts"); - - bfs::path music = findFile(from, "music", false); - if(music.string() != "") - installToPath(music, dFiles / "Music"); - - bfs::path sound = findFile(from, "sound", false); - if(sound.string() != "") - installToPath(sound, dFiles / "Sound"); - - bfs::path splash = findFile(from, "splash", false); - if(splash.string() != "") - installToPath(splash, dFiles / "Splash"); - } // if(!mMorrowindDone && mMorrowindPath.string().length() > 0) @@ -299,10 +291,11 @@ void Wizard::UnshieldThread::extract() /// + emit finished(); } -bool Wizard::UnshieldThread::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) +bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) { bool success; QString path(outputDir); @@ -344,7 +337,7 @@ bool Wizard::UnshieldThread::extractFile(Unshield *unshield, const QString &outp return success; } -void Wizard::UnshieldThread::extractCab(const QString &cabFile, const QString &outputDir) +void Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &outputDir) { Unshield *unshield; unshield = unshield_open(cabFile.toLatin1().constData()); @@ -366,11 +359,3 @@ void Wizard::UnshieldThread::extractCab(const QString &cabFile, const QString &o unshield_close(unshield); } - -void Wizard::UnshieldThread::run() -{ - qDebug() << "From worker thread: " << currentThreadId(); - - setupSettings(); - extract(); -} diff --git a/apps/wizard/unshieldthread.hpp b/apps/wizard/unshield/unshieldworker.hpp similarity index 78% rename from apps/wizard/unshieldthread.hpp rename to apps/wizard/unshield/unshieldworker.hpp index cc3740ad8..61e021865 100644 --- a/apps/wizard/unshieldthread.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -1,22 +1,22 @@ -#ifndef UNSHIELDTHREAD_HPP -#define UNSHIELDTHREAD_HPP +#ifndef UNSHIELDWORKER_HPP +#define UNSHIELDWORKER_HPP +#include #include #include -#include "inisettings.hpp" - -class QTextCodec; +#include "../inisettings.hpp" namespace Wizard { - - class UnshieldThread : public QThread + class UnshieldWorker : public QObject { Q_OBJECT + public: - explicit UnshieldThread(QObject *parent = 0); + UnshieldWorker(QObject *parent = 0); + ~UnshieldWorker(); void setInstallMorrowind(bool install); void setInstallTribunal(bool install); @@ -35,7 +35,6 @@ namespace Wizard bool moveDirectory(const QString &source, const QString &destination); void setupSettings(); - void extract(); void extractCab(const QString &cabFile, const QString &outputDir); bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); @@ -52,15 +51,18 @@ namespace Wizard QTextCodec *mIniCodec; - protected: - virtual void run(); + + public slots: + void extract(); signals: + void finished(); void textChanged(const QString &text); + void error(const QString &text); void progressChanged(int progress); - }; + }; } -#endif // UNSHIELDTHREAD_HPP +#endif // UNSHIELDWORKER_HPP From 0f7f3391f7528ec01882d433ac369f8195eb3e64 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 17 Jan 2014 13:21:44 +0100 Subject: [PATCH 19/69] WIP: Working on the installation of addons --- apps/wizard/installationpage.cpp | 11 +- apps/wizard/installationpage.hpp | 1 + apps/wizard/unshield/unshieldworker.cpp | 275 ++++++++++++++++-------- apps/wizard/unshield/unshieldworker.hpp | 11 + files/ui/wizard/installationpage.ui | 6 +- 5 files changed, 215 insertions(+), 89 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 374c8180c..045138ab7 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -45,8 +45,6 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setMaximum(installProgressBar->maximum() + 100); } - installProgressBar->setValue(100); - startInstallation(); } @@ -75,6 +73,9 @@ void Wizard::InstallationPage::startInstallation() connect(unshield, SIGNAL(finished()), this, SLOT(installationFinished())); + connect(unshield, SIGNAL(error(QString)), + this, SLOT(installationError(QString))); + connect(unshield, SIGNAL(textChanged(QString)), installProgressLabel, SLOT(setText(QString))); @@ -135,6 +136,12 @@ void Wizard::InstallationPage::installationFinished() qDebug() << "finished!"; mFinished = true; emit completeChanged(); + +} + +void Wizard::InstallationPage::installationError(const QString &text) +{ + qDebug() << "error: " << text; } bool Wizard::InstallationPage::isComplete() const diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 42b2be819..f55ab8ab5 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -28,6 +28,7 @@ namespace Wizard private slots: void installationFinished(); + void installationError(const QString &text); protected: void initializePage(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 71beaf9b8..4f92b1cf9 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -29,6 +29,10 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : mInstallMorrowind = false; mInstallTribunal = false; mInstallBloodmoon = false; + + mMorrowindDone = false; + mTribunalDone = false; + mBloodmoonDone = false; } Wizard::UnshieldWorker::~UnshieldWorker() @@ -119,25 +123,30 @@ bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) return result; } -bool Wizard::UnshieldWorker::moveFile(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::copyFile(const QString &source, const QString &destination, bool keepSource) { QDir dir; QFile file; - if (dir.rename(source, destination)) { - return true; - } else { - if (file.copy(source, destination)) { + QFileInfo info(destination); + + if (info.exists()) + dir.remove(info.absoluteFilePath()); + + if (file.copy(source, destination)) { + if (!keepSource) { return file.remove(source); } else { - qDebug() << "copy failed! " << file.errorString(); + return true; } + } else { + qDebug() << "copy failed! " << file.errorString(); } return false; } -bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString &destination, bool keepSource) { QDir sourceDir(source); QDir destDir(destination); @@ -170,127 +179,217 @@ bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } - - //if (!result) - // return result; } - return result && removeDirectory(sourceDir.absolutePath()); + if (!keepSource) + return result && removeDirectory(sourceDir.absolutePath()); + + return result; } -void Wizard::UnshieldWorker::extract() +bool Wizard::UnshieldWorker::moveFile(const QString &source, const QString &destination) { - emit textChanged(QLatin1String("Starting installation")); - emit textChanged(QLatin1String("Installation target: ") + mPath); + return copyFile(source, destination, false); +} - QStringList components; - if (mInstallMorrowind) - components << QLatin1String("Morrowind"); +bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString &destination) +{ + return copyDirectory(source, destination, false); +} - if (mInstallTribunal) - components << QLatin1String("Tribunal"); +void Wizard::UnshieldWorker::installDirectories(const QString &source) +{ + QDir dir(source); - if (mInstallBloodmoon) - components << QLatin1String("Bloodmoon"); + if (!dir.exists()) + return; - emit textChanged(QLatin1String("Components: ") + components.join(QLatin1String(", "))); + QStringList directories; + directories << QLatin1String("Fonts") + << QLatin1String("Music") + << QLatin1String("Sound") + << QLatin1String("Splash") + << QLatin1String("Video"); + + QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs)); + foreach(QFileInfo info, list) { + if (info.isSymLink()) + continue; - emit textChanged(QLatin1String("Updating Morrowind.ini: ") + mIniPath); + if (directories.contains(info.fileName())) { + qDebug() << "found " << info.fileName(); + emit textChanged(tr("Extracting: %1 directory").arg(info.fileName())); + copyDirectory(info.absoluteFilePath(), mPath + QDir::separator() + info.fileName()); + } + } +} - //emit progressChanged(45); +void Wizard::UnshieldWorker::extract() +{ + emit textChanged(QLatin1String("Starting installation")); + emit textChanged(QLatin1String("Installation target: ") + mPath); - /// -// bfs::path outputDataFilesDir = mOutputPath; -// outputDataFilesDir /= "Data Files"; -// bfs::path extractPath = mOutputPath; -// extractPath /= "extract-temp"; + QString diskPath("/mnt/cdrom/"); + QDir disk(diskPath); // Create temporary extract directory // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(mPath + QLatin1String("/extract-temp")); - QDir dir; - dir.mkpath(tempPath); + QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QDir temp; + + // Make sure the temporary folder is empty + removeDirectory(tempPath); + + if (!temp.mkpath(tempPath)) { + qDebug() << "Can't make path"; + return; + } + + temp.setPath(tempPath); + disk.setPath(diskPath); if (mInstallMorrowind) { - QString morrowindTempPath(tempPath + QLatin1String("/morrowind")); - QString morrowindCab(QLatin1String("/mnt/cdrom/data1.hdr")); + emit textChanged(QLatin1String("Installing Morrowind\n")); - //extractCab(morrowindCab, morrowindTempPath); + if (!temp.mkdir(QLatin1String("morrowind"))) { + qDebug() << "Can't make dir"; + return; + } + + if (!temp.cd(QLatin1String("morrowind"))) { + qDebug() << "Can't cd to dir"; + return; + } + + if (!disk.exists(QLatin1String("data1.hdr"))) { + qDebug() << "No data found!"; + return; + } + + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); // TODO: Throw error; // Move the files from the temporary path to the destination folder //qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; - morrowindTempPath.append(QDir::separator() + QLatin1String("Data Files")); -// if (!moveDirectory(morrowindTempPath, mPath)) -// qDebug() << "failed!"; + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) + qDebug() << "failed!"; - QDir sourceDir(QLatin1String("/mnt/cdrom/")); - QStringList directories; - directories << QLatin1String("Fonts") - << QLatin1String("Music") - << QLatin1String("Sound") - << QLatin1String("Splash") - << QLatin1String("Video"); + // Install files outside of cab archives + qDebug() << temp.absolutePath() << disk.absolutePath(); + installDirectories(disk.absolutePath()); - QFileInfoList list(sourceDir.entryInfoList(QDir::NoDotAndDotDot | - QDir::System | QDir::Hidden | - QDir::AllDirs)); + // Copy Morrowind configuration file + QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); + iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); + QFileInfo info(iniPath); - foreach(QFileInfo info, list) { - if (info.isSymLink()) - continue; + qDebug() << info.absoluteFilePath() << mPath; + + if (info.exists()) { + emit textChanged(tr("Extracting: Morrowind.ini")); + moveFile(info.absoluteFilePath(), mPath + QDir::separator() + QLatin1String("Morrowind.ini")); + } else { + qDebug() << "Could not find ini file!"; + } + + mMorrowindDone = true; + + } + + temp.setPath(tempPath); + disk.setPath(diskPath); + + if (mInstallTribunal) + { + emit textChanged(QLatin1String("Installing Tribunal\n")); + + if (!temp.mkdir(QLatin1String("tribunal"))) { + qDebug() << "Can't make dir"; + return; + } - qDebug() << "not found " << info.fileName(); + if (!temp.cd(QLatin1String("tribunal"))) { + qDebug() << "Can't cd to dir"; + return; + } + + if (!disk.cd(QLatin1String("Tribunal"))) + qDebug() << "Show file selector"; - if (directories.contains(info.fileName())) - qDebug() << "found " << info.fileName(); -// copyDirectory(info.absoluteFilePath(), mPath); + if (!disk.exists(QLatin1String("data1.hdr"))) { + qDebug() << "No data found!"; + return; } + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) + qDebug() << "failed!"; + + // Install files outside of cab archives + installDirectories(disk.absolutePath()); + + mTribunalDone = true; } -// if(!mMorrowindDone && mMorrowindPath.string().length() > 0) -// { -// mMorrowindDone = true; + temp.setPath(tempPath); + disk.setPath(diskPath); -// bfs::path mwExtractPath = extractPath / "morrowind"; -// extract_cab(mMorrowindPath, mwExtractPath, true); + if (mInstallBloodmoon) + { + emit textChanged(QLatin1String("Installing Bloodmoon\n")); -// bfs::path dFilesDir = findFile(mwExtractPath, "morrowind.esm").parent_path(); + if (!temp.mkdir(QLatin1String("bloodmoon"))) { + qDebug() << "Can't make dir"; + return; + } -// installToPath(dFilesDir, outputDataFilesDir); + if (!temp.cd(QLatin1String("bloodmoon"))) { + qDebug() << "Can't cd to dir"; + return; + } -// install_dfiles_outside(mwExtractPath, outputDataFilesDir); + if (!disk.cd(QLatin1String("Bloodmoon"))) + qDebug() << "Show file selector"; -// // Videos are often kept uncompressed on the cd -// bfs::path videosPath = findFile(mMorrowindPath.parent_path(), "video", false); -// if(videosPath.string() != "") -// { -// emit signalGUI(QString("Installing Videos...")); -// installToPath(videosPath, outputDataFilesDir / "Video", true); -// } + if (!disk.exists(QLatin1String("data1.hdr"))) { + qDebug() << "No data found!"; + return; + } -// bfs::path cdDFiles = findFile(mMorrowindPath.parent_path(), "data files", false); -// if(cdDFiles.string() != "") -// { -// emit signalGUI(QString("Installing Uncompressed Data files from CD...")); -// installToPath(cdDFiles, outputDataFilesDir, true); -// } + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) + qDebug() << "failed!"; -// bfs::rename(findFile(mwExtractPath, "morrowind.ini"), outputDataFilesDir / "Morrowind.ini"); + // Install files outside of cab archives + installDirectories(disk.absolutePath()); -// mTribunalDone = contains(outputDataFilesDir, "tribunal.esm"); -// mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); + mBloodmoonDone = true; + } -// } + int total = 0; + if (mInstallMorrowind) + total = 100; - /// + if (mInstallTribunal) + total = total + 100; + + if (mInstallBloodmoon) + total = total + 100; + + emit textChanged(tr("Installation finished!")); + emit progressChanged(total); emit finished(); } @@ -322,17 +421,25 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp fileName.append(QString::fromLatin1(unshield_file_name(unshield, index))); // Calculate the percentage done - int progress = qCeil(((float) counter / (float) unshield_file_count(unshield)) * 100); + int progress = qFloor(((float) counter / (float) unshield_file_count(unshield)) * 100); + + if (mMorrowindDone) + progress = progress + 100; + + if (mTribunalDone) + progress = progress + 100; qDebug() << progress << counter << unshield_file_count(unshield); - emit textChanged(QLatin1String("Extracting: ") + QString::fromLatin1(unshield_file_name(unshield, index))); + emit textChanged(tr("Extracting: %1").arg(QString::fromLatin1(unshield_file_name(unshield, index)))); emit progressChanged(progress); success = unshield_file_save(unshield, index, fileName.toLatin1().constData()); - if (!success) + if (!success) { + emit error(tr("Failed to extract %1").arg(fileName)); dir.remove(fileName); + } return success; } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 61e021865..b268727ce 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -31,6 +31,9 @@ namespace Wizard bool removeDirectory(const QString &dirName); + bool copyFile(const QString &source, const QString &destination, bool keepSource = true); + bool copyDirectory(const QString &source, const QString &destination, bool keepSource = true); + bool moveFile(const QString &source, const QString &destination); bool moveDirectory(const QString &source, const QString &destination); @@ -39,11 +42,17 @@ namespace Wizard void extractCab(const QString &cabFile, const QString &outputDir); bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); + void installDirectories(const QString &source); + bool mInstallMorrowind; bool mInstallTribunal; bool mInstallBloodmoon; + bool mMorrowindDone; + bool mTribunalDone; + bool mBloodmoonDone; + QString mPath; QString mIniPath; @@ -58,6 +67,8 @@ namespace Wizard signals: void finished(); void textChanged(const QString &text); + void logTextChanged(const QString &text); + void error(const QString &text); void progressChanged(int progress); diff --git a/files/ui/wizard/installationpage.ui b/files/ui/wizard/installationpage.ui index 6c1a3c493..664e10556 100644 --- a/files/ui/wizard/installationpage.ui +++ b/files/ui/wizard/installationpage.ui @@ -6,8 +6,8 @@ 0 0 - 516 - 421 + 514 + 419 @@ -30,7 +30,7 @@ - 24 + 0 From cdbf5c68b0fccceeb9a3a22fc654934228958f89 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 24 Jan 2014 22:25:22 +0100 Subject: [PATCH 20/69] Implemented selection of installation media and added forgotten file --- apps/wizard/installationpage.cpp | 129 ++++- apps/wizard/installationpage.hpp | 8 + apps/wizard/unshield/unshieldworker.cpp | 506 ++++++++++++++---- apps/wizard/unshield/unshieldworker.hpp | 34 ++ .../48x48/preferences-desktop-locale.png | Bin 0 -> 1761 bytes 5 files changed, 546 insertions(+), 131 deletions(-) create mode 100644 files/wizard/icons/tango/48x48/preferences-desktop-locale.png diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 045138ab7..860cfbb56 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include "mainwizard.hpp" #include "inisettings.hpp" @@ -53,82 +55,161 @@ void Wizard::InstallationPage::startInstallation() QStringList components(field("installation.components").toStringList()); QString path(field("installation.path").toString()); - QThread* thread = new QThread(); - UnshieldWorker* unshield = new UnshieldWorker(); + QThread *thread = new QThread(); + mUnshield = new UnshieldWorker(); - unshield->moveToThread(thread); + mUnshield->moveToThread(thread); connect(thread, SIGNAL(started()), - unshield, SLOT(extract())); + mUnshield, SLOT(extract())); - connect(unshield, SIGNAL(finished()), + connect(mUnshield, SIGNAL(finished()), thread, SLOT(quit())); - connect(unshield, SIGNAL(finished()), - unshield, SLOT(deleteLater())); + connect(mUnshield, SIGNAL(finished()), + mUnshield, SLOT(deleteLater())); - connect(unshield, SIGNAL(finished()), + connect(mUnshield, SIGNAL(finished()), thread, SLOT(deleteLater())); - connect(unshield, SIGNAL(finished()), + connect(mUnshield, SIGNAL(finished()), this, SLOT(installationFinished())); - connect(unshield, SIGNAL(error(QString)), + connect(mUnshield, SIGNAL(error(QString)), this, SLOT(installationError(QString))); - connect(unshield, SIGNAL(textChanged(QString)), + connect(mUnshield, SIGNAL(textChanged(QString)), installProgressLabel, SLOT(setText(QString))); - connect(unshield, SIGNAL(textChanged(QString)), + connect(mUnshield, SIGNAL(textChanged(QString)), logTextEdit, SLOT(append(QString))); - connect(unshield, SIGNAL(progressChanged(int)), + connect(mUnshield, SIGNAL(progressChanged(int)), installProgressBar, SLOT(setValue(int))); + connect(mUnshield, SIGNAL(requestFileDialog(QString)), + this, SLOT(showFileDialog(QString))); + if (field("installation.new").toBool() == true) { // Always install Morrowind - unshield->setInstallMorrowind(true); + mUnshield->setInstallMorrowind(true); if (components.contains(QLatin1String("Tribunal"))) - unshield->setInstallTribunal(true); + mUnshield->setInstallTribunal(true); if (components.contains(QLatin1String("Bloodmoon"))) - unshield->setInstallBloodmoon(true); + mUnshield->setInstallBloodmoon(true); } else { // Morrowind should already be installed - unshield->setInstallMorrowind(false); + mUnshield->setInstallMorrowind(false); if (components.contains(QLatin1String("Tribunal")) && mWizard->mInstallations[path]->hasTribunal == false) - unshield->setInstallTribunal(true); + mUnshield->setInstallTribunal(true); if (components.contains(QLatin1String("Bloodmoon")) && mWizard->mInstallations[path]->hasBloodmoon == false) - unshield->setInstallBloodmoon(true); + mUnshield->setInstallBloodmoon(true); // Set the location of the Morrowind.ini to update - unshield->setIniPath(mWizard->mInstallations[path]->iniPath); + mUnshield->setIniPath(mWizard->mInstallations[path]->iniPath); } // Set the installation target path - unshield->setPath(path); + mUnshield->setPath(path); // Set the right codec to use for Morrowind.ini QString language(field("installation.language").toString()); if (language == QLatin1String("Polish")) { - unshield->setIniCodec(QTextCodec::codecForName("windows-1250")); + mUnshield->setIniCodec(QTextCodec::codecForName("windows-1250")); } else if (language == QLatin1String("Russian")) { - unshield->setIniCodec(QTextCodec::codecForName("windows-1251")); + mUnshield->setIniCodec(QTextCodec::codecForName("windows-1251")); } else { - unshield->setIniCodec(QTextCodec::codecForName("windows-1252")); + mUnshield->setIniCodec(QTextCodec::codecForName("windows-1252")); } thread->start(); +} + + + +//void Wizard::InstallationPage::installAddons() +//{ +// qDebug() << "component finished"; + +// QStringList components(field("installation.components").toStringList()); + +// if (components.contains(QLatin1String("Tribunal")) && !mUnshield->tribunalDone()) +// { +// QString fileName = QFileDialog::getOpenFileName( +// this, +// tr("Select Tribunal installation file"), +// QDir::rootPath(), +// tr("InstallShield header files (*.hdr)")); + +// if (fileName.isEmpty()) { +// qDebug() << "Cancel was clicked!"; +// return; +// } + +// QFileInfo info(fileName); +// mUnshield->installTribunal(info.absolutePath()); +// } +// if (components.contains(QLatin1String("Bloodmoon")) && !mUnshield->bloodmoonDone()) +// { +// QString fileName = QFileDialog::getOpenFileName( +// this, +// tr("Select Bloodmoon installation file"), +// QDir::rootPath(), +// tr("InstallShield header files (*.hdr)")); + +// if (fileName.isEmpty()) { +// qDebug() << "Cancel was clicked!"; +// return; +// } + +// QFileInfo info(fileName); +// mUnshield->installBloodmoon(info.absolutePath()); +// } +//} + +void Wizard::InstallationPage::showFileDialog(const QString &component) +{ + QString fileName; + + if (field("installation.new").toBool() == true) + { + fileName = QFileDialog::getOpenFileName( + this, + tr("Select %0 installation file").arg(component), + QDir::rootPath(), + tr("InstallShield header files (*.hdr)")); + + if (fileName.isEmpty()) { + qDebug() << "Cancel was clicked!"; + return; + } + + QFileInfo info(fileName); + + if (component == QLatin1String("Morrowind")) + { + mUnshield->setMorrowindPath(info.absolutePath()); + } + else if (component == QLatin1String("Tribunal")) + { + mUnshield->setTribunalPath(info.absolutePath()); + } + else if (component == QLatin1String("Bloodmoon")) + { + mUnshield->setBloodmoonPath(info.absolutePath()); + } + } } void Wizard::InstallationPage::installationFinished() diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index f55ab8ab5..e125eadc4 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -6,10 +6,13 @@ #include "ui_installationpage.h" #include "inisettings.hpp" +class QThread; + namespace Wizard { class MainWizard; class IniSettings; + class UnshieldWorker; class InstallationPage : public QWizardPage, private Ui::InstallationPage { @@ -24,9 +27,14 @@ namespace Wizard MainWizard *mWizard; bool mFinished; + QThread* mThread; + UnshieldWorker *mUnshield; + void startInstallation(); private slots: + void showFileDialog(const QString &component); + void installationFinished(); void installationError(const QString &text); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 4f92b1cf9..7d536affa 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -20,6 +21,10 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : { unshield_set_log_level(0); + mMorrowindPath = QString(); + mTribunalPath = QString(); + mBloodmoonPath = QString(); + mPath = QString(); mIniPath = QString(); @@ -42,19 +47,101 @@ Wizard::UnshieldWorker::~UnshieldWorker() void Wizard::UnshieldWorker::setInstallMorrowind(bool install) { +// QMutexLocker locker(&mMutex); mInstallMorrowind = install; } void Wizard::UnshieldWorker::setInstallTribunal(bool install) { +// QMutexLocker locker(&mMutex); mInstallTribunal = install; } void Wizard::UnshieldWorker::setInstallBloodmoon(bool install) { +// QMutexLocker locker(&mMutex); mInstallBloodmoon = install; } +bool Wizard::UnshieldWorker::getInstallMorrowind() +{ +// QMutexLocker locker(&mMutex); + return mInstallMorrowind; +} + +bool Wizard::UnshieldWorker::getInstallTribunal() +{ +// QMutexLocker locker(&mMutex); + return mInstallTribunal; +} + +bool Wizard::UnshieldWorker::getInstallBloodmoon() +{ +// QMutexLocker locker(&mMutex); + return mInstallBloodmoon; +} + +//bool Wizard::UnshieldWorker::getMorrowindDone() +//{ +// QMutexLocker locker(&mMutex); +// return mMorrowindDone; +//} + +//bool Wizard::UnshieldWorker::tribunalDone() +//{ +// QMutexLocker locker(&mMutex); +// return mTribunalDone; +//} + +//bool Wizard::UnshieldWorker::bloodmoonDone() +//{ +// return mBloodmoonDone; +//} + +void Wizard::UnshieldWorker::setMorrowindPath(const QString &path) +{ + qDebug() << "setmorrowindpath!"; + QMutexLocker locker(&mMutex); + mMorrowindPath = path; + mWait.wakeAll(); +} + +void Wizard::UnshieldWorker::setTribunalPath(const QString &path) +{ + //QMutexLocker locker(&mMutex); + mTribunalPath = path; + mWait.wakeAll(); + +} + +void Wizard::UnshieldWorker::setBloodmoonPath(const QString &path) +{ + //QMutexLocker locker(&mMutex); + mBloodmoonPath = path; + mWait.wakeAll(); + +} + +QString Wizard::UnshieldWorker::getMorrowindPath() +{ + qDebug() << "getmorrowindpath!"; + //QMutexLocker locker(&mMutex); + //mWait.wakeAll(); + return mMorrowindPath; +} + +QString Wizard::UnshieldWorker::getTribunalPath() +{ + //QMutexLocker locker(&mMutex); + return mTribunalPath; +} + +QString Wizard::UnshieldWorker::getBloodmoonPath() +{ + //QMutexLocker locker(&mMutex); + return mBloodmoonPath; +} + void Wizard::UnshieldWorker::setPath(const QString &path) { mPath = path; @@ -224,15 +311,173 @@ void Wizard::UnshieldWorker::installDirectories(const QString &source) copyDirectory(info.absoluteFilePath(), mPath + QDir::separator() + info.fileName()); } } + + // Copy the Data Files dir too, but only the subdirectories + QFileInfo info(dir.absoluteFilePath("Data Files")); + if (info.exists()) { + emit textChanged(tr("Extracting: Data Files directory")); + copyDirectory(info.absoluteFilePath(), mPath); + } + } + void Wizard::UnshieldWorker::extract() { - emit textChanged(QLatin1String("Starting installation")); - emit textChanged(QLatin1String("Installation target: ") + mPath); + qDebug() << "extract!"; + + QMutexLocker locker(&mMutex); + + QDir disk; + + qDebug() << "hi!"; + + if (getInstallMorrowind()) + { + while (!mMorrowindDone) + { + if (getMorrowindPath().isEmpty()) { + qDebug() << "request file dialog"; + emit requestFileDialog(QLatin1String("Morrowind")); + mWait.wait(&mMutex); + } + + if (!mMorrowindDone && !getMorrowindPath().isEmpty()) { + disk.setPath(getMorrowindPath()); + + if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa"))) { + qDebug() << "found"; + emit requestFileDialog(QLatin1String("Morrowind")); + mWait.wait(&mMutex); + + } else { + qDebug() << "install morrowind!"; + + if (installMorrowind()) { + mMorrowindDone = true; + } else { + qDebug() << "Erorr installing Morrowind"; + return; + } + } + } + } + } + + if (getInstallTribunal()) + { + while (!mTribunalDone) + { + QDir tribunal(disk); + + if (!tribunal.cd(QLatin1String("Tribunal"))) { + qDebug() << "not found on cd!"; + emit requestFileDialog(QLatin1String("Tribunal")); + mWait.wait(&mMutex); + + } else if (tribunal.exists(QLatin1String("data1.hdr"))) { + qDebug() << "Exists! " << tribunal.absolutePath(); + mTribunalPath = tribunal.absolutePath(); + } + + if (getTribunalPath().isEmpty()) { + qDebug() << "request file dialog"; + emit requestFileDialog(QLatin1String("Tribunal")); + mWait.wait(&mMutex); + } + + // Make sure the dir is up-to-date + tribunal.setPath(getTribunalPath()); + + if (!findFile(tribunal.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) { + qDebug() << "file not found!"; + emit requestFileDialog(QLatin1String("Tribunal")); + mWait.wait(&mMutex); + + } else { + qDebug() << "install tribunal!"; + if (installTribunal()) { + mTribunalDone = true; + } else { + qDebug() << "Erorr installing Tribunal"; + return; + } + } + } + } + + if (getInstallBloodmoon()) + { + while (!mBloodmoonDone) + { + QDir bloodmoon(disk); + + qDebug() << "bloodmoon!: " << bloodmoon.absolutePath(); + + if (!bloodmoon.cd(QLatin1String("Bloodmoon"))) { + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mMutex); + + } else if (bloodmoon.exists(QLatin1String("data1.hdr"))) { + mBloodmoonPath = bloodmoon.absolutePath(); + } + + if (getBloodmoonPath().isEmpty()) { + qDebug() << "request file dialog"; + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mMutex); + } + + // Make sure the dir is up-to-date + bloodmoon.setPath(getBloodmoonPath()); + + if (!findFile(bloodmoon.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) { + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mMutex); + + } else { + qDebug() << "install bloodmoon!"; + if (installBloodmoon()) { + mBloodmoonDone = true; + } else { + qDebug() << "Erorr installing Bloodmoon"; + return; + } + } + } + } + + // Remove the temporary directory + removeDirectory(mPath + QDir::separator() + QLatin1String("extract-temp")); + + locker.unlock(); + + int total = 0; + + if (mInstallMorrowind) + total = 100; + + if (mInstallTribunal) + total = total + 100; + + if (mInstallBloodmoon) + total = total + 100; + + emit textChanged(tr("Installation finished!")); + emit progressChanged(total); + emit finished(); + + qDebug() << "installation finished!"; +} + +bool Wizard::UnshieldWorker::installMorrowind() +{ + emit textChanged(QLatin1String("Installing Morrowind\n")); - QString diskPath("/mnt/cdrom/"); - QDir disk(diskPath); + QDir disk(getMorrowindPath()); + + if (!disk.exists()) + return false; // Create temporary extract directory // TODO: Use QTemporaryDir in Qt 5.0 @@ -244,156 +489,168 @@ void Wizard::UnshieldWorker::extract() if (!temp.mkpath(tempPath)) { qDebug() << "Can't make path"; - return; + return false; } temp.setPath(tempPath); - disk.setPath(diskPath); - if (mInstallMorrowind) - { - emit textChanged(QLatin1String("Installing Morrowind\n")); + QString cabFile(disk.absoluteFilePath(QLatin1String("data1.hdr"))); - if (!temp.mkdir(QLatin1String("morrowind"))) { - qDebug() << "Can't make dir"; - return; - } + if (!temp.mkdir(QLatin1String("morrowind"))) { + qDebug() << "Can't make dir"; + return false; + } - if (!temp.cd(QLatin1String("morrowind"))) { - qDebug() << "Can't cd to dir"; - return; - } + if (!temp.cd(QLatin1String("morrowind"))) { + qDebug() << "Can't cd to dir"; + return false; + } - if (!disk.exists(QLatin1String("data1.hdr"))) { - qDebug() << "No data found!"; - return; - } + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); - // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + // TODO: Throw error; + // Move the files from the temporary path to the destination folder + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + qDebug() << "failed to move files!"; + return false; + } - // TODO: Throw error; - // Move the files from the temporary path to the destination folder + // Install files outside of cab archives + installDirectories(disk.absolutePath()); - //qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) - qDebug() << "failed!"; + // Copy Morrowind configuration file + QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); + iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); - // Install files outside of cab archives - qDebug() << temp.absolutePath() << disk.absolutePath(); - installDirectories(disk.absolutePath()); + QFileInfo info(iniPath); - // Copy Morrowind configuration file - QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); - iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); + qDebug() << info.absoluteFilePath() << mPath; - QFileInfo info(iniPath); + if (info.exists()) { + emit textChanged(tr("Extracting: Morrowind.ini")); + moveFile(info.absoluteFilePath(), mPath + QDir::separator() + QLatin1String("Morrowind.ini")); + } else { + qDebug() << "Could not find ini file!"; + return false; + } - qDebug() << info.absoluteFilePath() << mPath; + return true; +} - if (info.exists()) { - emit textChanged(tr("Extracting: Morrowind.ini")); - moveFile(info.absoluteFilePath(), mPath + QDir::separator() + QLatin1String("Morrowind.ini")); - } else { - qDebug() << "Could not find ini file!"; - } +bool Wizard::UnshieldWorker::installTribunal() +{ + emit textChanged(QLatin1String("Installing Tribunal")); - mMorrowindDone = true; + QDir disk(getTribunalPath()); + if (!disk.exists()) { + qDebug() << "disk does not exist! " << disk.absolutePath() << getTribunalPath(); + return false; } - temp.setPath(tempPath); - disk.setPath(diskPath); - - if (mInstallTribunal) - { - emit textChanged(QLatin1String("Installing Tribunal\n")); - - if (!temp.mkdir(QLatin1String("tribunal"))) { - qDebug() << "Can't make dir"; - return; - } + // Create temporary extract directory + // TODO: Use QTemporaryDir in Qt 5.0 + QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QDir temp; - if (!temp.cd(QLatin1String("tribunal"))) { - qDebug() << "Can't cd to dir"; - return; - } + // Make sure the temporary folder is empty + removeDirectory(tempPath); - if (!disk.cd(QLatin1String("Tribunal"))) - qDebug() << "Show file selector"; + if (!temp.mkpath(tempPath)) { + qDebug() << "Can't make path"; + return false; + } - if (!disk.exists(QLatin1String("data1.hdr"))) { - qDebug() << "No data found!"; - return; - } + temp.setPath(tempPath); - // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + if (!temp.mkdir(QLatin1String("tribunal"))) { + qDebug() << "Can't make dir"; + return false; + } - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) - qDebug() << "failed!"; + if (!temp.cd(QLatin1String("tribunal"))) { + qDebug() << "Can't cd to dir"; + return false; + } - // Install files outside of cab archives - installDirectories(disk.absolutePath()); + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); - mTribunalDone = true; + // TODO: Throw error; + // Move the files from the temporary path to the destination folder + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + qDebug() << "failed to move files!"; + return false; } - temp.setPath(tempPath); - disk.setPath(diskPath); + // Install files outside of cab archives + installDirectories(disk.absolutePath()); - if (mInstallBloodmoon) - { - emit textChanged(QLatin1String("Installing Bloodmoon\n")); + return true; +} - if (!temp.mkdir(QLatin1String("bloodmoon"))) { - qDebug() << "Can't make dir"; - return; - } +bool Wizard::UnshieldWorker::installBloodmoon() +{ + emit textChanged(QLatin1String("Installing Bloodmoon")); - if (!temp.cd(QLatin1String("bloodmoon"))) { - qDebug() << "Can't cd to dir"; - return; - } + QDir disk(getBloodmoonPath()); - if (!disk.cd(QLatin1String("Bloodmoon"))) - qDebug() << "Show file selector"; + if (!disk.exists()) { + qDebug() << "disk does not exist! " << disk.absolutePath() << getTribunalPath(); + return false; + } - if (!disk.exists(QLatin1String("data1.hdr"))) { - qDebug() << "No data found!"; - return; - } + // Create temporary extract directory + // TODO: Use QTemporaryDir in Qt 5.0 + QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QDir temp; - // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + // Make sure the temporary folder is empty + removeDirectory(tempPath); - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) - qDebug() << "failed!"; + if (!temp.mkpath(tempPath)) { + qDebug() << "Can't make path"; + return false; + } - // Install files outside of cab archives - installDirectories(disk.absolutePath()); + temp.setPath(tempPath); - mBloodmoonDone = true; + if (!temp.mkdir(QLatin1String("bloodmoon"))) { + qDebug() << "Can't make dir"; + return false; } + if (!temp.cd(QLatin1String("bloodmoon"))) { + qDebug() << "Can't cd to dir"; + return false; + } - int total = 0; + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); - if (mInstallMorrowind) - total = 100; + // TODO: Throw error; + // Move the files from the temporary path to the destination folder + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + qDebug() << "failed to move files!"; + return false; + } - if (mInstallTribunal) - total = total + 100; + // Install files outside of cab archives + installDirectories(disk.absolutePath()); - if (mInstallBloodmoon) - total = total + 100; + QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); + QFileInfo original(mPath + QDir::separator() + QLatin1String("Tribunal.esm")); - emit textChanged(tr("Installation finished!")); - emit progressChanged(total); - emit finished(); + if (original.exists() && patch.exists()) { + emit textChanged(tr("Extracting: Tribunal patch")); + copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); + } + return true; } + bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) { bool success; @@ -444,11 +701,46 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp return success; } +bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fileName) +{ + Unshield *unshield; + unshield = unshield_open(cabFile.toLatin1().constData()); + + // TODO: Proper error + if (!unshield) { + emit error(tr("Failed to open %1").arg(cabFile)); + return false; + } + + for (int i=0; ifirst_file; j<=group->last_file; ++j) + { + QString current(QString::fromLatin1(unshield_file_name(unshield, j))); + + qDebug() << "File is: " << unshield_file_name(unshield, j); + if (current == fileName) + return true; // File is found! + } + } + + unshield_close(unshield); + return false; +} + void Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &outputDir) { Unshield *unshield; unshield = unshield_open(cabFile.toLatin1().constData()); + // TODO: Proper error + if (!unshield) { + emit error(tr("Failed to open %1").arg(cabFile)); + return; + } + int counter = 0; for (int i=0; i #include +#include +#include #include @@ -22,6 +24,18 @@ namespace Wizard void setInstallTribunal(bool install); void setInstallBloodmoon(bool install); + bool getInstallMorrowind(); + bool getInstallTribunal(); + bool getInstallBloodmoon(); + + void setMorrowindPath(const QString &path); + void setTribunalPath(const QString &path); + void setBloodmoonPath(const QString &path); + + QString getMorrowindPath(); + QString getTribunalPath(); + QString getBloodmoonPath(); + void setPath(const QString &path); void setIniPath(const QString &path); @@ -29,6 +43,14 @@ namespace Wizard private: +// void setMorrowindDone(bool done); +// void setTribunalDone(bool done); +// void setBloodmoonDone(bool done); + +// bool getMorrowindDone(); +// bool getTribunalDone(); +// bool getBloodmoonDone(); + bool removeDirectory(const QString &dirName); bool copyFile(const QString &source, const QString &destination, bool keepSource = true); @@ -41,9 +63,13 @@ namespace Wizard void extractCab(const QString &cabFile, const QString &outputDir); bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); + bool findFile(const QString &cabFile, const QString &fileName); void installDirectories(const QString &source); + bool installMorrowind(); + bool installTribunal(); + bool installBloodmoon(); bool mInstallMorrowind; bool mInstallTribunal; @@ -53,6 +79,10 @@ namespace Wizard bool mTribunalDone; bool mBloodmoonDone; + QString mMorrowindPath; + QString mTribunalPath; + QString mBloodmoonPath; + QString mPath; QString mIniPath; @@ -60,12 +90,16 @@ namespace Wizard QTextCodec *mIniCodec; + QWaitCondition mWait; + QMutex mMutex; public slots: void extract(); signals: void finished(); + void requestFileDialog(const QString &component); + void textChanged(const QString &text); void logTextChanged(const QString &text); diff --git a/files/wizard/icons/tango/48x48/preferences-desktop-locale.png b/files/wizard/icons/tango/48x48/preferences-desktop-locale.png new file mode 100644 index 0000000000000000000000000000000000000000..f56497bd23989040f0a2d23080b8103ad086cfbf GIT binary patch literal 1761 zcmV<71|Io|P)f8XQYxp&4h_IP4DiIX;ILTG3tgaC<(1skfaDuhs3pe_p%ELpSg zo((HNVgnl%tyF=ET2vukf$$J%)uuF+lR9pi_-)U_Gk5Oed$8~jEaW&-M~1PKBVFB@ zJ6D?X`@VDj=bR57*&}->6Z`%6!sovINg>SBGSFX?LOm|v1W?9%oO9L-(FVU^jd|C@ zycGTeTlfzyymk7cIp*B|1>pIoo)6{gZIO`BjAP`8Q$chZ>4Axt`a)fa}RE?qJr6vv%WPvw2}>zDue!|r|n zdiNJ4)Xy({_Q|QK6H|0HIxMU&5(Xhb5D)@T^e?8{>BTh3zgA>6H<_{48s#u0mG7*mkfBA|CDDsB3&Jow(6;uv3F&4^mZvDGP*M|UO%%n%QA}707#ojbyo_-kZ+3z~58l;EvGC#T zt}Pz?U;2Ng7T$aEEXNg&G*M`&?>VcaMyZ3jB9FR^%DppN9BqIjzXL}b!22TwpzmAW z_TlitvK8N8y*5vK>?T>MNh&*dX~BbV8VZffG`?BGY>eZ3Rp?EidUZe@GQfSTueNx5 z;#EG3|H5qJERWAV!R*wtjK>YeN|Qu-44kB}1*XX9q+6_RFS6dc!|LV=&GsT&=^C<7 z1f5C3%^9NYX`=2lQ95-Xu%Q8N)?eqwKbkWdQXc86EXzPV7|=qDn-0EFRerNADFM)%qCYljDpxYIqN6FU8?U9>D&a zLIsK&?=6@+x9%2cKMTEZEB-})auj7~H-G_n^UrUmX+JGGY3H@u8w1qg+fp7qz9z3Is2*A~) zs|h=!i5mc@!8>9nKitSTe*G0+As=Yf%%8@xjT@?(XD_x@fwNTDdJIu$iZMNbf_F|-J<(vhh? znM~-*Z4%L;ZB{8L4qt`#v3v3}89qiMJWWM6=x1HJ?S$>TiM1O@uZ%zwm#VT7HstZ} zJeBe|VJ)JauhC5JvejRr*lpFaXY5`fbmZyMIhqR+fafwDW{p z`L4hvwPFt$LxtH?f7Um$md(V@%!t{&}?jg Date: Mon, 27 Jan 2014 15:12:02 +0100 Subject: [PATCH 21/69] Made the getter/setter functions thread-safe --- apps/wizard/installationpage.cpp | 12 +- apps/wizard/unshield/unshieldworker.cpp | 273 ++++++++++++------------ apps/wizard/unshield/unshieldworker.hpp | 18 +- 3 files changed, 155 insertions(+), 148 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 860cfbb56..9b14b5918 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -73,22 +73,22 @@ void Wizard::InstallationPage::startInstallation() thread, SLOT(deleteLater())); connect(mUnshield, SIGNAL(finished()), - this, SLOT(installationFinished())); + this, SLOT(installationFinished()), Qt::QueuedConnection); connect(mUnshield, SIGNAL(error(QString)), - this, SLOT(installationError(QString))); + this, SLOT(installationError(QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(textChanged(QString)), - installProgressLabel, SLOT(setText(QString))); + installProgressLabel, SLOT(setText(QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(textChanged(QString)), - logTextEdit, SLOT(append(QString))); + logTextEdit, SLOT(append(QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(progressChanged(int)), - installProgressBar, SLOT(setValue(int))); + installProgressBar, SLOT(setValue(int)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(requestFileDialog(QString)), - this, SLOT(showFileDialog(QString))); + this, SLOT(showFileDialog(QString)), Qt::QueuedConnection); if (field("installation.new").toBool() == true) { diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 7d536affa..5c22df446 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -2,10 +2,11 @@ #include +#include +#include #include #include #include -#include #include #include #include @@ -47,68 +48,50 @@ Wizard::UnshieldWorker::~UnshieldWorker() void Wizard::UnshieldWorker::setInstallMorrowind(bool install) { -// QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mInstallMorrowind = install; } void Wizard::UnshieldWorker::setInstallTribunal(bool install) { -// QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mInstallTribunal = install; } void Wizard::UnshieldWorker::setInstallBloodmoon(bool install) { -// QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mInstallBloodmoon = install; } bool Wizard::UnshieldWorker::getInstallMorrowind() { -// QMutexLocker locker(&mMutex); + QReadLocker readLock(&mLock); return mInstallMorrowind; } bool Wizard::UnshieldWorker::getInstallTribunal() { -// QMutexLocker locker(&mMutex); + QReadLocker readLock(&mLock); return mInstallTribunal; } bool Wizard::UnshieldWorker::getInstallBloodmoon() { -// QMutexLocker locker(&mMutex); + QReadLocker readLock(&mLock); return mInstallBloodmoon; } -//bool Wizard::UnshieldWorker::getMorrowindDone() -//{ -// QMutexLocker locker(&mMutex); -// return mMorrowindDone; -//} - -//bool Wizard::UnshieldWorker::tribunalDone() -//{ -// QMutexLocker locker(&mMutex); -// return mTribunalDone; -//} - -//bool Wizard::UnshieldWorker::bloodmoonDone() -//{ -// return mBloodmoonDone; -//} - void Wizard::UnshieldWorker::setMorrowindPath(const QString &path) { - qDebug() << "setmorrowindpath!"; - QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mMorrowindPath = path; mWait.wakeAll(); } void Wizard::UnshieldWorker::setTribunalPath(const QString &path) { - //QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mTribunalPath = path; mWait.wakeAll(); @@ -116,7 +99,7 @@ void Wizard::UnshieldWorker::setTribunalPath(const QString &path) void Wizard::UnshieldWorker::setBloodmoonPath(const QString &path) { - //QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mBloodmoonPath = path; mWait.wakeAll(); @@ -124,57 +107,99 @@ void Wizard::UnshieldWorker::setBloodmoonPath(const QString &path) QString Wizard::UnshieldWorker::getMorrowindPath() { - qDebug() << "getmorrowindpath!"; - //QMutexLocker locker(&mMutex); - //mWait.wakeAll(); + QReadLocker readLock(&mLock); return mMorrowindPath; } QString Wizard::UnshieldWorker::getTribunalPath() { - //QMutexLocker locker(&mMutex); + QReadLocker readLock(&mLock); return mTribunalPath; } QString Wizard::UnshieldWorker::getBloodmoonPath() { - //QMutexLocker locker(&mMutex); + QReadLocker readLock(&mLock); return mBloodmoonPath; } void Wizard::UnshieldWorker::setPath(const QString &path) { + QWriteLocker writeLock(&mLock); mPath = path; } void Wizard::UnshieldWorker::setIniPath(const QString &path) { + QWriteLocker writeLock(&mLock); mIniPath = path; } +QString Wizard::UnshieldWorker::getPath() +{ + QReadLocker readLock(&mLock); + return mPath; +} + +QString Wizard::UnshieldWorker::getIniPath() +{ + QReadLocker readLock(&mLock); + return mIniPath; +} + void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) { + QWriteLocker writeLock(&mLock); mIniCodec = codec; } +void Wizard::UnshieldWorker::setMorrowindDone(bool done) +{ + QWriteLocker writeLock(&mLock); + mMorrowindDone = done; +} + +void Wizard::UnshieldWorker::setTribunalDone(bool done) +{ + QWriteLocker writeLock(&mLock); + mTribunalDone = done; +} + +void Wizard::UnshieldWorker::setBloodmoonDone(bool done) +{ + QWriteLocker writeLock(&mLock); + mBloodmoonDone = done; +} + +bool Wizard::UnshieldWorker::getMorrowindDone() +{ + QReadLocker readLock(&mLock); + return mMorrowindDone; +} + +bool Wizard::UnshieldWorker::getTribunalDone() +{ + QReadLocker readLock(&mLock); + return mTribunalDone; +} + +bool Wizard::UnshieldWorker::getBloodmoonDone() +{ + QReadLocker readLock(&mLock); + return mBloodmoonDone; +} + void Wizard::UnshieldWorker::setupSettings() { // Create Morrowind.ini settings map - if (mIniPath.isEmpty()) + if (getIniPath().isEmpty()) return; - QFile file(mIniPath); + QFile file(getIniPath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { // TODO: Emit error signal - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error opening Morrowind configuration file")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); + qDebug() << "Error opening .ini file!"; return; } @@ -308,7 +333,7 @@ void Wizard::UnshieldWorker::installDirectories(const QString &source) if (directories.contains(info.fileName())) { qDebug() << "found " << info.fileName(); emit textChanged(tr("Extracting: %1 directory").arg(info.fileName())); - copyDirectory(info.absoluteFilePath(), mPath + QDir::separator() + info.fileName()); + copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); } } @@ -316,7 +341,7 @@ void Wizard::UnshieldWorker::installDirectories(const QString &source) QFileInfo info(dir.absoluteFilePath("Data Files")); if (info.exists()) { emit textChanged(tr("Extracting: Data Files directory")); - copyDirectory(info.absoluteFilePath(), mPath); + copyDirectory(info.absoluteFilePath(), getPath()); } } @@ -325,36 +350,32 @@ void Wizard::UnshieldWorker::installDirectories(const QString &source) void Wizard::UnshieldWorker::extract() { qDebug() << "extract!"; - - QMutexLocker locker(&mMutex); - QDir disk; - qDebug() << "hi!"; - if (getInstallMorrowind()) { - while (!mMorrowindDone) + while (!getMorrowindDone()) { if (getMorrowindPath().isEmpty()) { qDebug() << "request file dialog"; + QReadLocker readLock(&mLock); emit requestFileDialog(QLatin1String("Morrowind")); - mWait.wait(&mMutex); + mWait.wait(&mLock); } - if (!mMorrowindDone && !getMorrowindPath().isEmpty()) { + if (!getMorrowindPath().isEmpty()) { disk.setPath(getMorrowindPath()); - if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa"))) { - qDebug() << "found"; + if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa")) + | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa")) + | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) + { + QReadLocker readLock(&mLock); emit requestFileDialog(QLatin1String("Morrowind")); - mWait.wait(&mMutex); - + mWait.wait(&mLock); } else { - qDebug() << "install morrowind!"; - if (installMorrowind()) { - mMorrowindDone = true; + setMorrowindDone(true); } else { qDebug() << "Erorr installing Morrowind"; return; @@ -366,41 +387,31 @@ void Wizard::UnshieldWorker::extract() if (getInstallTribunal()) { - while (!mTribunalDone) + while (!getTribunalDone()) { - QDir tribunal(disk); - - if (!tribunal.cd(QLatin1String("Tribunal"))) { - qDebug() << "not found on cd!"; - emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mMutex); - - } else if (tribunal.exists(QLatin1String("data1.hdr"))) { - qDebug() << "Exists! " << tribunal.absolutePath(); - mTribunalPath = tribunal.absolutePath(); - } - if (getTribunalPath().isEmpty()) { qDebug() << "request file dialog"; + QReadLocker locker(&mLock); emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mMutex); + mWait.wait(&mLock); } - // Make sure the dir is up-to-date - tribunal.setPath(getTribunalPath()); - - if (!findFile(tribunal.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) { - qDebug() << "file not found!"; - emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mMutex); + if (!getTribunalPath().isEmpty()) { + disk.setPath(getTribunalPath()); - } else { - qDebug() << "install tribunal!"; - if (installTribunal()) { - mTribunalDone = true; + if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) + { + qDebug() << "found"; + QReadLocker locker(&mLock); + emit requestFileDialog(QLatin1String("Tribunal")); + mWait.wait(&mLock); } else { - qDebug() << "Erorr installing Tribunal"; - return; + if (installTribunal()) { + setTribunalDone(true); + } else { + qDebug() << "Erorr installing Tribunal"; + return; + } } } } @@ -408,50 +419,35 @@ void Wizard::UnshieldWorker::extract() if (getInstallBloodmoon()) { - while (!mBloodmoonDone) + while (!getBloodmoonDone()) { - QDir bloodmoon(disk); - - qDebug() << "bloodmoon!: " << bloodmoon.absolutePath(); - - if (!bloodmoon.cd(QLatin1String("Bloodmoon"))) { - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mMutex); - - } else if (bloodmoon.exists(QLatin1String("data1.hdr"))) { - mBloodmoonPath = bloodmoon.absolutePath(); - } - if (getBloodmoonPath().isEmpty()) { qDebug() << "request file dialog"; + QReadLocker locker(&mLock); emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mMutex); + mWait.wait(&mLock); } - // Make sure the dir is up-to-date - bloodmoon.setPath(getBloodmoonPath()); - - if (!findFile(bloodmoon.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) { - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mMutex); + if (!getBloodmoonPath().isEmpty()) { + disk.setPath(getBloodmoonPath()); - } else { - qDebug() << "install bloodmoon!"; - if (installBloodmoon()) { - mBloodmoonDone = true; + if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) + { + QReadLocker locker(&mLock); + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mLock); } else { - qDebug() << "Erorr installing Bloodmoon"; - return; + if (installBloodmoon()) { + setBloodmoonDone(true); + } else { + qDebug() << "Erorr installing Bloodmoon"; + return; + } } } } } - // Remove the temporary directory - removeDirectory(mPath + QDir::separator() + QLatin1String("extract-temp")); - - locker.unlock(); - int total = 0; if (mInstallMorrowind) @@ -472,16 +468,19 @@ void Wizard::UnshieldWorker::extract() bool Wizard::UnshieldWorker::installMorrowind() { - emit textChanged(QLatin1String("Installing Morrowind\n")); + qDebug() << "install morrowind!"; + emit textChanged(QLatin1String("Installing Morrowind")); QDir disk(getMorrowindPath()); - if (!disk.exists()) + if (!disk.exists()) { + qDebug() << "getMorrowindPath: " << getMorrowindPath(); return false; + } // Create temporary extract directory // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QString tempPath(getPath() + QDir::separator() + QLatin1String("extract-temp")); QDir temp; // Make sure the temporary folder is empty @@ -494,8 +493,6 @@ bool Wizard::UnshieldWorker::installMorrowind() temp.setPath(tempPath); - QString cabFile(disk.absoluteFilePath(QLatin1String("data1.hdr"))); - if (!temp.mkdir(QLatin1String("morrowind"))) { qDebug() << "Can't make dir"; return false; @@ -511,7 +508,8 @@ bool Wizard::UnshieldWorker::installMorrowind() // TODO: Throw error; // Move the files from the temporary path to the destination folder - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + emit textChanged(tr("Moving installation files")); + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { qDebug() << "failed to move files!"; return false; } @@ -525,16 +523,17 @@ bool Wizard::UnshieldWorker::installMorrowind() QFileInfo info(iniPath); - qDebug() << info.absoluteFilePath() << mPath; + qDebug() << info.absoluteFilePath() << getPath(); if (info.exists()) { emit textChanged(tr("Extracting: Morrowind.ini")); - moveFile(info.absoluteFilePath(), mPath + QDir::separator() + QLatin1String("Morrowind.ini")); + moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); } else { qDebug() << "Could not find ini file!"; return false; } + emit textChanged(tr("Morrowind installation finished!")); return true; } @@ -551,7 +550,7 @@ bool Wizard::UnshieldWorker::installTribunal() // Create temporary extract directory // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QString tempPath(getPath() + QDir::separator() + QLatin1String("extract-temp")); QDir temp; // Make sure the temporary folder is empty @@ -579,7 +578,8 @@ bool Wizard::UnshieldWorker::installTribunal() // TODO: Throw error; // Move the files from the temporary path to the destination folder - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + emit textChanged(tr("Moving installation files")); + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { qDebug() << "failed to move files!"; return false; } @@ -587,6 +587,7 @@ bool Wizard::UnshieldWorker::installTribunal() // Install files outside of cab archives installDirectories(disk.absolutePath()); + emit textChanged(tr("Tribunal installation finished!")); return true; } @@ -597,13 +598,12 @@ bool Wizard::UnshieldWorker::installBloodmoon() QDir disk(getBloodmoonPath()); if (!disk.exists()) { - qDebug() << "disk does not exist! " << disk.absolutePath() << getTribunalPath(); return false; } // Create temporary extract directory // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QString tempPath(getPath() + QDir::separator() + QLatin1String("extract-temp")); QDir temp; // Make sure the temporary folder is empty @@ -631,7 +631,8 @@ bool Wizard::UnshieldWorker::installBloodmoon() // TODO: Throw error; // Move the files from the temporary path to the destination folder - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + emit textChanged(tr("Moving installation files")); + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { qDebug() << "failed to move files!"; return false; } @@ -640,13 +641,14 @@ bool Wizard::UnshieldWorker::installBloodmoon() installDirectories(disk.absolutePath()); QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); - QFileInfo original(mPath + QDir::separator() + QLatin1String("Tribunal.esm")); + QFileInfo original(getPath() + QDir::separator() + QLatin1String("Tribunal.esm")); if (original.exists() && patch.exists()) { emit textChanged(tr("Extracting: Tribunal patch")); copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); } + emit textChanged(tr("Bloodmoon installation finished!")); return true; } @@ -669,7 +671,6 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp path.replace(QLatin1Char('\\'), QDir::separator()); path = QDir::toNativeSeparators(path); - qDebug() << "path is: " << path << QString::fromLatin1(unshield_directory_name(unshield, directory)) + QLatin1Char('/'); // Ensure the target path exists QDir dir; dir.mkpath(path); @@ -678,12 +679,12 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp fileName.append(QString::fromLatin1(unshield_file_name(unshield, index))); // Calculate the percentage done - int progress = qFloor(((float) counter / (float) unshield_file_count(unshield)) * 100); + int progress = (((float) counter / (float) unshield_file_count(unshield)) * 100); - if (mMorrowindDone) + if (getMorrowindDone()) progress = progress + 100; - if (mTribunalDone) + if (getTribunalDone()) progress = progress + 100; qDebug() << progress << counter << unshield_file_count(unshield); diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 17c0973b0..bdaf44198 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -39,17 +40,20 @@ namespace Wizard void setPath(const QString &path); void setIniPath(const QString &path); + QString getPath(); + QString getIniPath(); + void setIniCodec(QTextCodec *codec); private: -// void setMorrowindDone(bool done); -// void setTribunalDone(bool done); -// void setBloodmoonDone(bool done); + void setMorrowindDone(bool done); + void setTribunalDone(bool done); + void setBloodmoonDone(bool done); -// bool getMorrowindDone(); -// bool getTribunalDone(); -// bool getBloodmoonDone(); + bool getMorrowindDone(); + bool getTribunalDone(); + bool getBloodmoonDone(); bool removeDirectory(const QString &dirName); @@ -93,6 +97,8 @@ namespace Wizard QWaitCondition mWait; QMutex mMutex; + QReadWriteLock mLock; + public slots: void extract(); From 3fd88aca594040a756ba811d6ac06fc4da8de523 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 27 Jan 2014 16:51:22 +0100 Subject: [PATCH 22/69] Re-added support for GoTY disks and added a messagebox, displayed when done --- apps/wizard/installationpage.cpp | 53 ++++------------- apps/wizard/unshield/unshieldworker.cpp | 78 +++++++++++++++++-------- 2 files changed, 64 insertions(+), 67 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 9b14b5918..8690cd2bc 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "mainwizard.hpp" #include "inisettings.hpp" @@ -135,49 +136,6 @@ void Wizard::InstallationPage::startInstallation() thread->start(); } - - -//void Wizard::InstallationPage::installAddons() -//{ -// qDebug() << "component finished"; - -// QStringList components(field("installation.components").toStringList()); - -// if (components.contains(QLatin1String("Tribunal")) && !mUnshield->tribunalDone()) -// { -// QString fileName = QFileDialog::getOpenFileName( -// this, -// tr("Select Tribunal installation file"), -// QDir::rootPath(), -// tr("InstallShield header files (*.hdr)")); - -// if (fileName.isEmpty()) { -// qDebug() << "Cancel was clicked!"; -// return; -// } - -// QFileInfo info(fileName); -// mUnshield->installTribunal(info.absolutePath()); -// } - -// if (components.contains(QLatin1String("Bloodmoon")) && !mUnshield->bloodmoonDone()) -// { -// QString fileName = QFileDialog::getOpenFileName( -// this, -// tr("Select Bloodmoon installation file"), -// QDir::rootPath(), -// tr("InstallShield header files (*.hdr)")); - -// if (fileName.isEmpty()) { -// qDebug() << "Cancel was clicked!"; -// return; -// } - -// QFileInfo info(fileName); -// mUnshield->installBloodmoon(info.absolutePath()); -// } -//} - void Wizard::InstallationPage::showFileDialog(const QString &component) { QString fileName; @@ -215,6 +173,15 @@ void Wizard::InstallationPage::showFileDialog(const QString &component) void Wizard::InstallationPage::installationFinished() { qDebug() << "finished!"; + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Installation finished")); + msgBox.setIcon(QMessageBox::Information); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("Installation completed sucessfully!")); + + msgBox.exec(); + mFinished = true; emit completeChanged(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 5c22df446..2ab68dde0 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -287,7 +287,7 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString if (info.isDir()) { result = moveDirectory(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } else { - qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; + qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } @@ -389,6 +389,19 @@ void Wizard::UnshieldWorker::extract() { while (!getTribunalDone()) { + QDir tribunal(disk); + + if (!tribunal.cd(QLatin1String("Tribunal"))) { + qDebug() << "not found on cd!"; + QReadLocker locker(&mLock); + emit requestFileDialog(QLatin1String("Tribunal")); + mWait.wait(&mLock); + + } else if (tribunal.exists(QLatin1String("data1.hdr"))) { + qDebug() << "Exists! " << tribunal.absolutePath(); + setTribunalPath(tribunal.absolutePath()); + } + if (getTribunalPath().isEmpty()) { qDebug() << "request file dialog"; QReadLocker locker(&mLock); @@ -396,10 +409,12 @@ void Wizard::UnshieldWorker::extract() mWait.wait(&mLock); } + // Make sure the dir is up-to-date + tribunal.setPath(getTribunalPath()); + if (!getTribunalPath().isEmpty()) { - disk.setPath(getTribunalPath()); - if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) + if (!findFile(tribunal.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) { qDebug() << "found"; QReadLocker locker(&mLock); @@ -421,6 +436,21 @@ void Wizard::UnshieldWorker::extract() { while (!getBloodmoonDone()) { + QDir bloodmoon(disk); + + qDebug() << "Test!: " << bloodmoon.absolutePath(); + + if (!bloodmoon.cd(QLatin1String("Bloodmoon"))) { + qDebug() << "not found on cd!"; + QReadLocker locker(&mLock); + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mLock); + + } else if (bloodmoon.exists(QLatin1String("data1.hdr"))) { + qDebug() << "Exists! " << bloodmoon.absolutePath(); + setBloodmoonPath(bloodmoon.absolutePath()); + } + if (getBloodmoonPath().isEmpty()) { qDebug() << "request file dialog"; QReadLocker locker(&mLock); @@ -428,35 +458,38 @@ void Wizard::UnshieldWorker::extract() mWait.wait(&mLock); } - if (!getBloodmoonPath().isEmpty()) { - disk.setPath(getBloodmoonPath()); + // Make sure the dir is up-to-date + bloodmoon.setPath(getBloodmoonPath()); - if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) - { - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mLock); + if (!findFile(bloodmoon.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) + { + QReadLocker locker(&mLock); + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mLock); + } else { + if (installBloodmoon()) { + setBloodmoonDone(true); } else { - if (installBloodmoon()) { - setBloodmoonDone(true); - } else { - qDebug() << "Erorr installing Bloodmoon"; - return; - } + qDebug() << "Erorr installing Bloodmoon"; + return; } } } } + // Remove the temporary directory + removeDirectory(mPath + QDir::separator() + QLatin1String("extract-temp")); + + // Fill the progress bar int total = 0; - if (mInstallMorrowind) + if (getInstallMorrowind()) total = 100; - if (mInstallTribunal) + if (getInstallTribunal()) total = total + 100; - if (mInstallBloodmoon) + if (getInstallBloodmoon()) total = total + 100; emit textChanged(tr("Installation finished!")); @@ -523,8 +556,6 @@ bool Wizard::UnshieldWorker::installMorrowind() QFileInfo info(iniPath); - qDebug() << info.absoluteFilePath() << getPath(); - if (info.exists()) { emit textChanged(tr("Extracting: Morrowind.ini")); moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); @@ -579,7 +610,8 @@ bool Wizard::UnshieldWorker::installTribunal() // TODO: Throw error; // Move the files from the temporary path to the destination folder emit textChanged(tr("Moving installation files")); - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) + { qDebug() << "failed to move files!"; return false; } @@ -687,8 +719,6 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp if (getTribunalDone()) progress = progress + 100; - qDebug() << progress << counter << unshield_file_count(unshield); - emit textChanged(tr("Extracting: %1").arg(QString::fromLatin1(unshield_file_name(unshield, index)))); emit progressChanged(progress); From 802448b217477578a20d5543194623455b45c1b2 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 27 Jan 2014 20:14:02 +0100 Subject: [PATCH 23/69] Added a settings page to the launcher --- apps/launcher/CMakeLists.txt | 4 + apps/launcher/maindialog.cpp | 28 +-- apps/launcher/maindialog.hpp | 7 + apps/launcher/settingspage.cpp | 18 ++ apps/launcher/settingspage.hpp | 20 +++ .../icons/tango/{ => 16x16}/document-new.png | Bin .../icons/tango/{ => 16x16}/edit-copy.png | Bin .../icons/tango/{ => 16x16}/edit-delete.png | Bin .../icons/tango/{ => 16x16}/go-bottom.png | Bin .../icons/tango/{ => 16x16}/go-down.png | Bin .../icons/tango/{ => 16x16}/go-top.png | Bin .../icons/tango/{ => 16x16}/go-up.png | Bin .../icons/tango/48x48/preferences-system.png | Bin 0 -> 3718 bytes .../icons/tango/{ => 48x48}/video-display.png | Bin files/launcher/icons/tango/index.theme | 7 +- files/launcher/launcher.qrc | 17 +- files/ui/mainwindow.ui | 10 +- files/ui/settingspage.ui | 159 ++++++++++++++++++ 18 files changed, 248 insertions(+), 22 deletions(-) create mode 100644 apps/launcher/settingspage.cpp create mode 100644 apps/launcher/settingspage.hpp rename files/launcher/icons/tango/{ => 16x16}/document-new.png (100%) rename files/launcher/icons/tango/{ => 16x16}/edit-copy.png (100%) rename files/launcher/icons/tango/{ => 16x16}/edit-delete.png (100%) rename files/launcher/icons/tango/{ => 16x16}/go-bottom.png (100%) rename files/launcher/icons/tango/{ => 16x16}/go-down.png (100%) rename files/launcher/icons/tango/{ => 16x16}/go-top.png (100%) rename files/launcher/icons/tango/{ => 16x16}/go-up.png (100%) create mode 100644 files/launcher/icons/tango/48x48/preferences-system.png rename files/launcher/icons/tango/{ => 48x48}/video-display.png (100%) create mode 100644 files/ui/settingspage.ui diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 279474406..d24c8e377 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -5,6 +5,7 @@ set(LAUNCHER maindialog.cpp playpage.cpp textslotmsgbox.cpp + settingspage.cpp settings/graphicssettings.cpp @@ -25,6 +26,7 @@ set(LAUNCHER_HEADER maindialog.hpp playpage.hpp textslotmsgbox.hpp + settingspage.hpp settings/graphicssettings.hpp @@ -45,6 +47,7 @@ set(LAUNCHER_HEADER_MOC maindialog.hpp playpage.hpp textslotmsgbox.hpp + settingspage.hpp utils/textinputdialog.hpp utils/checkablemessagebox.hpp @@ -64,6 +67,7 @@ set(LAUNCHER_UI ${CMAKE_SOURCE_DIR}/files/ui/mainwindow.ui ${CMAKE_SOURCE_DIR}/files/ui/playpage.ui ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui + ${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui ) source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER}) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 4012a1fbd..f81d15e9c 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -23,6 +23,7 @@ #include "playpage.hpp" #include "graphicspage.hpp" #include "datafilespage.hpp" +#include "settingspage.hpp" Launcher::MainDialog::MainDialog(QWidget *parent) : mGameSettings(mCfgMgr), QMainWindow (parent) @@ -75,27 +76,30 @@ void Launcher::MainDialog::createIcons() if (!QIcon::hasThemeIcon("document-new")) QIcon::setThemeName("tango"); - // We create a fallback icon because the default fallback doesn't work - QIcon graphicsIcon = QIcon(":/icons/tango/video-display.png"); - QListWidgetItem *playButton = new QListWidgetItem(iconWidget); playButton->setIcon(QIcon(":/images/openmw.png")); playButton->setText(tr("Play")); playButton->setTextAlignment(Qt::AlignCenter); playButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *graphicsButton = new QListWidgetItem(iconWidget); - graphicsButton->setIcon(QIcon::fromTheme("video-display", graphicsIcon)); - graphicsButton->setText(tr("Graphics")); - graphicsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom | Qt::AlignAbsolute); - graphicsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *dataFilesButton = new QListWidgetItem(iconWidget); dataFilesButton->setIcon(QIcon(":/images/openmw-plugin.png")); dataFilesButton->setText(tr("Data Files")); dataFilesButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); dataFilesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + QListWidgetItem *graphicsButton = new QListWidgetItem(iconWidget); + graphicsButton->setIcon(QIcon::fromTheme("video-display")); + graphicsButton->setText(tr("Graphics")); + graphicsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom | Qt::AlignAbsolute); + graphicsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + + QListWidgetItem *settingsButton = new QListWidgetItem(iconWidget); + settingsButton->setIcon(QIcon::fromTheme("preferences-system")); + settingsButton->setText(tr("Settings")); + settingsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); + settingsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + connect(iconWidget, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), this, SLOT(changePage(QListWidgetItem*,QListWidgetItem*))); @@ -105,8 +109,9 @@ void Launcher::MainDialog::createIcons() void Launcher::MainDialog::createPages() { mPlayPage = new PlayPage(this); - mGraphicsPage = new GraphicsPage(mCfgMgr, mGraphicsSettings, this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); + mGraphicsPage = new GraphicsPage(mCfgMgr, mGraphicsSettings, this); + mSettingsPage = new SettingsPage(this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); @@ -114,8 +119,9 @@ void Launcher::MainDialog::createPages() // Add the pages to the stacked widget pagesWidget->addWidget(mPlayPage); - pagesWidget->addWidget(mGraphicsPage); pagesWidget->addWidget(mDataFilesPage); + pagesWidget->addWidget(mGraphicsPage); + pagesWidget->addWidget(mSettingsPage); // Select the first page iconWidget->setCurrentItem(iconWidget->item(0), QItemSelectionModel::Select); diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index a4fb4cd9a..718acb5a9 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -13,6 +13,10 @@ #include "ui_mainwindow.h" class QListWidgetItem; +class QStackedWidget; +class QStringList; +class QStringListModel; +class QString; namespace Launcher { @@ -20,6 +24,7 @@ namespace Launcher class GraphicsPage; class DataFilesPage; class UnshieldThread; + class SettingsPage; #ifndef WIN32 bool expansions(Launcher::UnshieldThread& cd); @@ -58,6 +63,8 @@ namespace Launcher PlayPage *mPlayPage; GraphicsPage *mGraphicsPage; DataFilesPage *mDataFilesPage; + SettingsPage *mSettingsPage; + Files::ConfigurationManager mCfgMgr; diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp new file mode 100644 index 000000000..96dd6e529 --- /dev/null +++ b/apps/launcher/settingspage.cpp @@ -0,0 +1,18 @@ +#include "settingspage.hpp" + +Launcher::SettingsPage::SettingsPage(QWidget *parent) : QWidget(parent) +{ + setupUi(this); + + QStringList languages; + languages << "English" + << "French" + << "German" + << "Italian" + << "Polish" + << "Russian" + << "Spanish"; + + languageComboBox->addItems(languages); +} + diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp new file mode 100644 index 000000000..c87f6e46f --- /dev/null +++ b/apps/launcher/settingspage.hpp @@ -0,0 +1,20 @@ +#ifndef SETTINGSPAGE_HPP +#define SETTINGSPAGE_HPP + +#include + +#include "ui_settingspage.h" + +namespace Launcher +{ + + class SettingsPage : public QWidget, private Ui::SettingsPage + { + Q_OBJECT + public: + SettingsPage(QWidget *parent = 0); + + }; +} + +#endif // SETTINGSPAGE_HPP diff --git a/files/launcher/icons/tango/document-new.png b/files/launcher/icons/tango/16x16/document-new.png similarity index 100% rename from files/launcher/icons/tango/document-new.png rename to files/launcher/icons/tango/16x16/document-new.png diff --git a/files/launcher/icons/tango/edit-copy.png b/files/launcher/icons/tango/16x16/edit-copy.png similarity index 100% rename from files/launcher/icons/tango/edit-copy.png rename to files/launcher/icons/tango/16x16/edit-copy.png diff --git a/files/launcher/icons/tango/edit-delete.png b/files/launcher/icons/tango/16x16/edit-delete.png similarity index 100% rename from files/launcher/icons/tango/edit-delete.png rename to files/launcher/icons/tango/16x16/edit-delete.png diff --git a/files/launcher/icons/tango/go-bottom.png b/files/launcher/icons/tango/16x16/go-bottom.png similarity index 100% rename from files/launcher/icons/tango/go-bottom.png rename to files/launcher/icons/tango/16x16/go-bottom.png diff --git a/files/launcher/icons/tango/go-down.png b/files/launcher/icons/tango/16x16/go-down.png similarity index 100% rename from files/launcher/icons/tango/go-down.png rename to files/launcher/icons/tango/16x16/go-down.png diff --git a/files/launcher/icons/tango/go-top.png b/files/launcher/icons/tango/16x16/go-top.png similarity index 100% rename from files/launcher/icons/tango/go-top.png rename to files/launcher/icons/tango/16x16/go-top.png diff --git a/files/launcher/icons/tango/go-up.png b/files/launcher/icons/tango/16x16/go-up.png similarity index 100% rename from files/launcher/icons/tango/go-up.png rename to files/launcher/icons/tango/16x16/go-up.png diff --git a/files/launcher/icons/tango/48x48/preferences-system.png b/files/launcher/icons/tango/48x48/preferences-system.png new file mode 100644 index 0000000000000000000000000000000000000000..a1620e991663d37d6e95ed11be7abb9927d73dfc GIT binary patch literal 3718 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 literal 0 HcmV?d00001 diff --git a/files/launcher/icons/tango/video-display.png b/files/launcher/icons/tango/48x48/video-display.png similarity index 100% rename from files/launcher/icons/tango/video-display.png rename to files/launcher/icons/tango/48x48/video-display.png diff --git a/files/launcher/icons/tango/index.theme b/files/launcher/icons/tango/index.theme index 1f54489eb..8ec560f85 100644 --- a/files/launcher/icons/tango/index.theme +++ b/files/launcher/icons/tango/index.theme @@ -2,7 +2,10 @@ Name=Tango Comment=Tango Theme Inherits=default -Directories=16x16 +Directories=16x16,48x48 [16x16] -Size=16 \ No newline at end of file +Size=16 + +[48x48] +Size=48 \ No newline at end of file diff --git a/files/launcher/launcher.qrc b/files/launcher/launcher.qrc index 19b1c5a6f..4f55fccd5 100644 --- a/files/launcher/launcher.qrc +++ b/files/launcher/launcher.qrc @@ -9,13 +9,14 @@ icons/tango/index.theme - icons/tango/video-display.png - icons/tango/document-new.png - icons/tango/edit-copy.png - icons/tango/edit-delete.png - icons/tango/go-bottom.png - icons/tango/go-down.png - icons/tango/go-top.png - icons/tango/go-up.png + icons/tango/48x48/video-display.png + icons/tango/48x48/preferences-system.png + icons/tango/16x16/document-new.png + icons/tango/16x16/edit-copy.png + icons/tango/16x16/edit-delete.png + icons/tango/16x16/go-bottom.png + icons/tango/16x16/go-down.png + icons/tango/16x16/go-top.png + icons/tango/16x16/go-up.png diff --git a/files/ui/mainwindow.ui b/files/ui/mainwindow.ui index a1dfb172b..368e5cc5f 100644 --- a/files/ui/mainwindow.ui +++ b/files/ui/mainwindow.ui @@ -2,9 +2,17 @@ MainWindow + + + 0 + 0 + 635 + 575 + + - 575 + 635 535 diff --git a/files/ui/settingspage.ui b/files/ui/settingspage.ui new file mode 100644 index 000000000..95b62761e --- /dev/null +++ b/files/ui/settingspage.ui @@ -0,0 +1,159 @@ + + + SettingsPage + + + + 0 + 0 + 518 + 401 + + + + Form + + + + + + General + + + + + + Morrowind installation language: + + + + + + + + 250 + 0 + + + + + + + + + + + Morrowind Installation Wizard + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Run &Installation Wizard + + + + + + + + + + Morrowind Settings Importer + + + + + + + + File to import settings from: + + + + + + + + /home/user/.local/share/openmw/data/Morrowind.ini + + + + + + + + Browse... + + + + + + + + + Import previously selected add-ons (creates a new Content List) + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Run &Settings Importer + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + From 2736527e5a37ecf15b38474dbfd5624b61898394 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 27 Jan 2014 22:54:14 +0100 Subject: [PATCH 24/69] Did some cleanup work on the unshield thread: more code re-use --- apps/wizard/installationpage.cpp | 59 +-- apps/wizard/installationpage.hpp | 3 +- apps/wizard/unshield/unshieldworker.cpp | 496 +++++++++--------------- apps/wizard/unshield/unshieldworker.hpp | 42 +- 4 files changed, 229 insertions(+), 371 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 8690cd2bc..47d10c45f 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -8,7 +8,6 @@ #include "mainwizard.hpp" #include "inisettings.hpp" -#include "unshield/unshieldworker.hpp" Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : QWizardPage(wizard), @@ -58,9 +57,10 @@ void Wizard::InstallationPage::startInstallation() QThread *thread = new QThread(); mUnshield = new UnshieldWorker(); - mUnshield->moveToThread(thread); + qRegisterMetaType("Wizard::Component"); + connect(thread, SIGNAL(started()), mUnshield, SLOT(extract())); @@ -88,30 +88,30 @@ void Wizard::InstallationPage::startInstallation() connect(mUnshield, SIGNAL(progressChanged(int)), installProgressBar, SLOT(setValue(int)), Qt::QueuedConnection); - connect(mUnshield, SIGNAL(requestFileDialog(QString)), - this, SLOT(showFileDialog(QString)), Qt::QueuedConnection); + connect(mUnshield, SIGNAL(requestFileDialog(Wizard::Component)), + this, SLOT(showFileDialog(Wizard::Component)), Qt::QueuedConnection); if (field("installation.new").toBool() == true) { // Always install Morrowind - mUnshield->setInstallMorrowind(true); + mUnshield->setInstallComponent(Wizard::Component_Morrowind, true); if (components.contains(QLatin1String("Tribunal"))) - mUnshield->setInstallTribunal(true); + mUnshield->setInstallComponent(Wizard::Component_Tribunal, true); if (components.contains(QLatin1String("Bloodmoon"))) - mUnshield->setInstallBloodmoon(true); + mUnshield->setInstallComponent(Wizard::Component_Bloodmoon, true); } else { // Morrowind should already be installed - mUnshield->setInstallMorrowind(false); + mUnshield->setInstallComponent(Wizard::Component_Morrowind, false); if (components.contains(QLatin1String("Tribunal")) - && mWizard->mInstallations[path]->hasTribunal == false) - mUnshield->setInstallTribunal(true); + && !mWizard->mInstallations[path]->hasTribunal) + mUnshield->setInstallComponent(Wizard::Component_Tribunal, true); if (components.contains(QLatin1String("Bloodmoon")) - && mWizard->mInstallations[path]->hasBloodmoon == false) - mUnshield->setInstallBloodmoon(true); + && !mWizard->mInstallations[path]->hasBloodmoon) + mUnshield->setInstallComponent(Wizard::Component_Bloodmoon, true); // Set the location of the Morrowind.ini to update mUnshield->setIniPath(mWizard->mInstallations[path]->iniPath); @@ -136,38 +136,21 @@ void Wizard::InstallationPage::startInstallation() thread->start(); } -void Wizard::InstallationPage::showFileDialog(const QString &component) +void Wizard::InstallationPage::showFileDialog(Wizard::Component component) { - QString fileName; - - if (field("installation.new").toBool() == true) - { - fileName = QFileDialog::getOpenFileName( + QString fileName = QFileDialog::getOpenFileName( this, - tr("Select %0 installation file").arg(component), + tr("Select installation file"), QDir::rootPath(), tr("InstallShield header files (*.hdr)")); - if (fileName.isEmpty()) { - qDebug() << "Cancel was clicked!"; - return; - } - - QFileInfo info(fileName); - - if (component == QLatin1String("Morrowind")) - { - mUnshield->setMorrowindPath(info.absolutePath()); - } - else if (component == QLatin1String("Tribunal")) - { - mUnshield->setTribunalPath(info.absolutePath()); - } - else if (component == QLatin1String("Bloodmoon")) - { - mUnshield->setBloodmoonPath(info.absolutePath()); - } + if (fileName.isEmpty()) { + qDebug() << "Cancel was clicked!"; + return; } + + QFileInfo info(fileName); + mUnshield->setComponentPath(component, info.absolutePath()); } void Wizard::InstallationPage::installationFinished() diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index e125eadc4..3989177ab 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -3,6 +3,7 @@ #include +#include "unshield/unshieldworker.hpp" #include "ui_installationpage.h" #include "inisettings.hpp" @@ -33,7 +34,7 @@ namespace Wizard void startInstallation(); private slots: - void showFileDialog(const QString &component); + void showFileDialog(Wizard::Component component); void installationFinished(); void installationError(const QString &text); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 2ab68dde0..9c635818b 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -14,8 +14,6 @@ #include #include -#include - Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : QObject(parent), mIniSettings() @@ -46,81 +44,106 @@ Wizard::UnshieldWorker::~UnshieldWorker() } -void Wizard::UnshieldWorker::setInstallMorrowind(bool install) +void Wizard::UnshieldWorker::setInstallComponent(Wizard::Component component, bool install) { QWriteLocker writeLock(&mLock); - mInstallMorrowind = install; -} + switch (component) { -void Wizard::UnshieldWorker::setInstallTribunal(bool install) -{ - QWriteLocker writeLock(&mLock); - mInstallTribunal = install; -} - -void Wizard::UnshieldWorker::setInstallBloodmoon(bool install) -{ - QWriteLocker writeLock(&mLock); - mInstallBloodmoon = install; + case Wizard::Component_Morrowind: + mInstallMorrowind = install; + break; + case Wizard::Component_Tribunal: + mInstallTribunal = install; + break; + case Wizard::Component_Bloodmoon: + mInstallBloodmoon = install; + break; + } } -bool Wizard::UnshieldWorker::getInstallMorrowind() +bool Wizard::UnshieldWorker::getInstallComponent(Component component) { QReadLocker readLock(&mLock); - return mInstallMorrowind; -} + switch (component) { -bool Wizard::UnshieldWorker::getInstallTribunal() -{ - QReadLocker readLock(&mLock); - return mInstallTribunal; -} + case Wizard::Component_Morrowind: + return mInstallMorrowind; + case Wizard::Component_Tribunal: + return mInstallTribunal; + case Wizard::Component_Bloodmoon: + return mInstallBloodmoon; + } -bool Wizard::UnshieldWorker::getInstallBloodmoon() -{ - QReadLocker readLock(&mLock); - return mInstallBloodmoon; + return false; } -void Wizard::UnshieldWorker::setMorrowindPath(const QString &path) +void Wizard::UnshieldWorker::setComponentPath(Wizard::Component component, const QString &path) { - QWriteLocker writeLock(&mLock); - mMorrowindPath = path; - mWait.wakeAll(); + QWriteLocker writeLock(&mLock); + switch (component) { + + case Wizard::Component_Morrowind: + mMorrowindPath = path; + break; + case Wizard::Component_Tribunal: + mTribunalPath = path; + break; + case Wizard::Component_Bloodmoon: + mBloodmoonPath = path; + break; + } + + mWait.wakeAll(); } -void Wizard::UnshieldWorker::setTribunalPath(const QString &path) +QString Wizard::UnshieldWorker::getComponentPath(Component component) { - QWriteLocker writeLock(&mLock); - mTribunalPath = path; - mWait.wakeAll(); + QReadLocker readLock(&mLock); + switch (component) { + + case Wizard::Component_Morrowind: + return mMorrowindPath; + case Wizard::Component_Tribunal: + return mTribunalPath; + case Wizard::Component_Bloodmoon: + return mBloodmoonPath; + } + return QString(); } -void Wizard::UnshieldWorker::setBloodmoonPath(const QString &path) +void Wizard::UnshieldWorker::setComponentDone(Component component, bool done) { QWriteLocker writeLock(&mLock); - mBloodmoonPath = path; - mWait.wakeAll(); + switch (component) { + case Wizard::Component_Morrowind: + mMorrowindDone = done; + break; + case Wizard::Component_Tribunal: + mTribunalDone = done; + break; + case Wizard::Component_Bloodmoon: + mBloodmoonDone = done; + break; + } } -QString Wizard::UnshieldWorker::getMorrowindPath() +bool Wizard::UnshieldWorker::getComponentDone(Component component) { QReadLocker readLock(&mLock); - return mMorrowindPath; -} + switch (component) + { -QString Wizard::UnshieldWorker::getTribunalPath() -{ - QReadLocker readLock(&mLock); - return mTribunalPath; -} + case Wizard::Component_Morrowind: + return mMorrowindDone; + case Wizard::Component_Tribunal: + return mTribunalDone; + case Wizard::Component_Bloodmoon: + return mBloodmoonDone; + } -QString Wizard::UnshieldWorker::getBloodmoonPath() -{ - QReadLocker readLock(&mLock); - return mBloodmoonPath; + return false; } void Wizard::UnshieldWorker::setPath(const QString &path) @@ -153,42 +176,6 @@ void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) mIniCodec = codec; } -void Wizard::UnshieldWorker::setMorrowindDone(bool done) -{ - QWriteLocker writeLock(&mLock); - mMorrowindDone = done; -} - -void Wizard::UnshieldWorker::setTribunalDone(bool done) -{ - QWriteLocker writeLock(&mLock); - mTribunalDone = done; -} - -void Wizard::UnshieldWorker::setBloodmoonDone(bool done) -{ - QWriteLocker writeLock(&mLock); - mBloodmoonDone = done; -} - -bool Wizard::UnshieldWorker::getMorrowindDone() -{ - QReadLocker readLock(&mLock); - return mMorrowindDone; -} - -bool Wizard::UnshieldWorker::getTribunalDone() -{ - QReadLocker readLock(&mLock); - return mTribunalDone; -} - -bool Wizard::UnshieldWorker::getBloodmoonDone() -{ - QReadLocker readLock(&mLock); - return mBloodmoonDone; -} - void Wizard::UnshieldWorker::setupSettings() { // Create Morrowind.ini settings map @@ -287,8 +274,7 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString if (info.isDir()) { result = moveDirectory(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } else { - qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; - +// qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } } @@ -346,36 +332,35 @@ void Wizard::UnshieldWorker::installDirectories(const QString &source) } - void Wizard::UnshieldWorker::extract() { qDebug() << "extract!"; QDir disk; - if (getInstallMorrowind()) + if (getInstallComponent(Wizard::Component_Morrowind)) { - while (!getMorrowindDone()) + while (!getComponentDone(Wizard::Component_Morrowind)) { - if (getMorrowindPath().isEmpty()) { + if (getComponentPath(Wizard::Component_Morrowind).isEmpty()) { qDebug() << "request file dialog"; QReadLocker readLock(&mLock); - emit requestFileDialog(QLatin1String("Morrowind")); + emit requestFileDialog(Wizard::Component_Morrowind); mWait.wait(&mLock); } - if (!getMorrowindPath().isEmpty()) { - disk.setPath(getMorrowindPath()); + if (!getComponentPath(Wizard::Component_Morrowind).isEmpty()) { + disk.setPath(getComponentPath(Wizard::Component_Morrowind)); if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa")) | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa")) | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) { QReadLocker readLock(&mLock); - emit requestFileDialog(QLatin1String("Morrowind")); + emit requestFileDialog(Wizard::Component_Morrowind); mWait.wait(&mLock); } else { - if (installMorrowind()) { - setMorrowindDone(true); + if (installComponent(Wizard::Component_Morrowind)) { + setComponentDone(Wizard::Component_Morrowind, true); } else { qDebug() << "Erorr installing Morrowind"; return; @@ -385,111 +370,29 @@ void Wizard::UnshieldWorker::extract() } } - if (getInstallTribunal()) + if (getInstallComponent(Wizard::Component_Tribunal)) { - while (!getTribunalDone()) - { - QDir tribunal(disk); - - if (!tribunal.cd(QLatin1String("Tribunal"))) { - qDebug() << "not found on cd!"; - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mLock); - - } else if (tribunal.exists(QLatin1String("data1.hdr"))) { - qDebug() << "Exists! " << tribunal.absolutePath(); - setTribunalPath(tribunal.absolutePath()); - } - - if (getTribunalPath().isEmpty()) { - qDebug() << "request file dialog"; - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mLock); - } - - // Make sure the dir is up-to-date - tribunal.setPath(getTribunalPath()); - - if (!getTribunalPath().isEmpty()) { - - if (!findFile(tribunal.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) - { - qDebug() << "found"; - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mLock); - } else { - if (installTribunal()) { - setTribunalDone(true); - } else { - qDebug() << "Erorr installing Tribunal"; - return; - } - } - } - } + setupAddon(Wizard::Component_Tribunal); } - if (getInstallBloodmoon()) + if (getInstallComponent(Wizard::Component_Bloodmoon)) { - while (!getBloodmoonDone()) - { - QDir bloodmoon(disk); - - qDebug() << "Test!: " << bloodmoon.absolutePath(); - - if (!bloodmoon.cd(QLatin1String("Bloodmoon"))) { - qDebug() << "not found on cd!"; - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mLock); - - } else if (bloodmoon.exists(QLatin1String("data1.hdr"))) { - qDebug() << "Exists! " << bloodmoon.absolutePath(); - setBloodmoonPath(bloodmoon.absolutePath()); - } - - if (getBloodmoonPath().isEmpty()) { - qDebug() << "request file dialog"; - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mLock); - } - - // Make sure the dir is up-to-date - bloodmoon.setPath(getBloodmoonPath()); - - if (!findFile(bloodmoon.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) - { - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mLock); - } else { - if (installBloodmoon()) { - setBloodmoonDone(true); - } else { - qDebug() << "Erorr installing Bloodmoon"; - return; - } - } - } + setupAddon(Wizard::Component_Bloodmoon); } // Remove the temporary directory - removeDirectory(mPath + QDir::separator() + QLatin1String("extract-temp")); + removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); // Fill the progress bar int total = 0; - if (getInstallMorrowind()) + if (getInstallComponent(Wizard::Component_Morrowind)) total = 100; - if (getInstallTribunal()) + if (getInstallComponent(Wizard::Component_Tribunal)) total = total + 100; - if (getInstallBloodmoon()) + if (getInstallComponent(Wizard::Component_Bloodmoon)) total = total + 100; emit textChanged(tr("Installation finished!")); @@ -499,83 +402,87 @@ void Wizard::UnshieldWorker::extract() qDebug() << "installation finished!"; } -bool Wizard::UnshieldWorker::installMorrowind() +void Wizard::UnshieldWorker::setupAddon(Component component) { - qDebug() << "install morrowind!"; - emit textChanged(QLatin1String("Installing Morrowind")); - - QDir disk(getMorrowindPath()); + while (!getComponentDone(component)) + { + QDir disk(getComponentPath(Wizard::Component_Morrowind)); + QString name; + if (component == Wizard::Component_Tribunal) + name = QLatin1String("Tribunal"); + + if (component == Wizard::Component_Bloodmoon) + name = QLatin1String("Bloodmoon"); + + if (name.isEmpty()) + return; // Not a valid addon + + if (!disk.cd(name)) { + qDebug() << "not found on cd!"; + QReadLocker locker(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + + } else if (disk.exists(QLatin1String("data1.hdr"))) { + qDebug() << "Exists! " << disk.absolutePath(); + setComponentPath(component, disk.absolutePath()); + } - if (!disk.exists()) { - qDebug() << "getMorrowindPath: " << getMorrowindPath(); - return false; - } + if (getComponentPath(component).isEmpty()) { + qDebug() << "request file dialog"; + QReadLocker locker(&mLock); + emit requestFileDialog(Wizard::Component_Tribunal); + mWait.wait(&mLock); + } - // Create temporary extract directory - // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(getPath() + QDir::separator() + QLatin1String("extract-temp")); - QDir temp; + // Make sure the dir is up-to-date + disk.setPath(getComponentPath(component)); - // Make sure the temporary folder is empty - removeDirectory(tempPath); + if (!getComponentPath(component).isEmpty()) { - if (!temp.mkpath(tempPath)) { - qDebug() << "Can't make path"; - return false; + if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) + { + QReadLocker locker(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + } else { + // Now do the actual installing + if (installComponent(component)) { + setComponentDone(component, true); + } else { + qDebug() << "Error installing " << name; + return; + } + } + } } +} - temp.setPath(tempPath); - - if (!temp.mkdir(QLatin1String("morrowind"))) { - qDebug() << "Can't make dir"; - return false; - } +bool Wizard::UnshieldWorker::installComponent(Component component) +{ + QString name; + switch (component) { - if (!temp.cd(QLatin1String("morrowind"))) { - qDebug() << "Can't cd to dir"; - return false; + case Wizard::Component_Morrowind: + name = QLatin1String("Morrowind"); + break; + case Wizard::Component_Tribunal: + name = QLatin1String("Tribunal"); + break; + case Wizard::Component_Bloodmoon: + name = QLatin1String("Bloodmoon"); + break; } - // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); - - // TODO: Throw error; - // Move the files from the temporary path to the destination folder - emit textChanged(tr("Moving installation files")); - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { - qDebug() << "failed to move files!"; + if (name.isEmpty()) return false; - } - - // Install files outside of cab archives - installDirectories(disk.absolutePath()); - // Copy Morrowind configuration file - QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); - iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); + emit textChanged(tr("Installing %0").arg(name)); - QFileInfo info(iniPath); - - if (info.exists()) { - emit textChanged(tr("Extracting: Morrowind.ini")); - moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); - } else { - qDebug() << "Could not find ini file!"; - return false; - } - - emit textChanged(tr("Morrowind installation finished!")); - return true; -} - -bool Wizard::UnshieldWorker::installTribunal() -{ - emit textChanged(QLatin1String("Installing Tribunal")); - - QDir disk(getTribunalPath()); + QDir disk(getComponentPath(component)); if (!disk.exists()) { - qDebug() << "disk does not exist! " << disk.absolutePath() << getTribunalPath(); + qDebug() << "Component path not set: " << getComponentPath(Wizard::Component_Morrowind); return false; } @@ -594,12 +501,12 @@ bool Wizard::UnshieldWorker::installTribunal() temp.setPath(tempPath); - if (!temp.mkdir(QLatin1String("tribunal"))) { + if (!temp.mkdir(name)) { qDebug() << "Can't make dir"; return false; } - if (!temp.cd(QLatin1String("tribunal"))) { + if (!temp.cd(name)) { qDebug() << "Can't cd to dir"; return false; } @@ -610,8 +517,7 @@ bool Wizard::UnshieldWorker::installTribunal() // TODO: Throw error; // Move the files from the temporary path to the destination folder emit textChanged(tr("Moving installation files")); - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) - { + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { qDebug() << "failed to move files!"; return false; } @@ -619,71 +525,39 @@ bool Wizard::UnshieldWorker::installTribunal() // Install files outside of cab archives installDirectories(disk.absolutePath()); - emit textChanged(tr("Tribunal installation finished!")); - return true; -} - -bool Wizard::UnshieldWorker::installBloodmoon() -{ - emit textChanged(QLatin1String("Installing Bloodmoon")); - - QDir disk(getBloodmoonPath()); - - if (!disk.exists()) { - return false; - } - - // Create temporary extract directory - // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(getPath() + QDir::separator() + QLatin1String("extract-temp")); - QDir temp; - - // Make sure the temporary folder is empty - removeDirectory(tempPath); - - if (!temp.mkpath(tempPath)) { - qDebug() << "Can't make path"; - return false; - } - - temp.setPath(tempPath); - - if (!temp.mkdir(QLatin1String("bloodmoon"))) { - qDebug() << "Can't make dir"; - return false; - } - - if (!temp.cd(QLatin1String("bloodmoon"))) { - qDebug() << "Can't cd to dir"; - return false; - } + if (component == Wizard::Component_Morrowind) + { + // Copy Morrowind configuration file + QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); + iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); - // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + QFileInfo info(iniPath); - // TODO: Throw error; - // Move the files from the temporary path to the destination folder - emit textChanged(tr("Moving installation files")); - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { - qDebug() << "failed to move files!"; - return false; + if (info.exists()) { + emit textChanged(tr("Extracting: Morrowind.ini")); + moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); + } else { + qDebug() << "Could not find ini file!"; + return false; + } } - // Install files outside of cab archives - installDirectories(disk.absolutePath()); + if (component == Wizard::Component_Bloodmoon) + { + QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); + QFileInfo original(getPath() + QDir::separator() + QLatin1String("Tribunal.esm")); - QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); - QFileInfo original(getPath() + QDir::separator() + QLatin1String("Tribunal.esm")); + if (original.exists() && patch.exists()) { + emit textChanged(tr("Extracting: Tribunal patch")); + copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); + } - if (original.exists() && patch.exists()) { - emit textChanged(tr("Extracting: Tribunal patch")); - copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); } - emit textChanged(tr("Bloodmoon installation finished!")); + emit textChanged(tr("%0 installation finished!").arg(name)); return true; -} +} bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) { @@ -713,10 +587,10 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp // Calculate the percentage done int progress = (((float) counter / (float) unshield_file_count(unshield)) * 100); - if (getMorrowindDone()) + if (getComponentDone(Wizard::Component_Morrowind)) progress = progress + 100; - if (getTribunalDone()) + if (getComponentDone(Wizard::Component_Tribunal)) progress = progress + 100; emit textChanged(tr("Extracting: %1").arg(QString::fromLatin1(unshield_file_name(unshield, index)))); diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index bdaf44198..fb55bc88f 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -11,31 +11,29 @@ #include "../inisettings.hpp" + namespace Wizard { + enum Component { + Component_Morrowind, + Component_Tribunal, + Component_Bloodmoon + }; + + //Q_DECLARE_METATYPE(Wizard::Component) + class UnshieldWorker : public QObject { Q_OBJECT + Q_ENUMS(Wizard::Component) public: UnshieldWorker(QObject *parent = 0); ~UnshieldWorker(); - void setInstallMorrowind(bool install); - void setInstallTribunal(bool install); - void setInstallBloodmoon(bool install); + void setInstallComponent(Wizard::Component component, bool install); - bool getInstallMorrowind(); - bool getInstallTribunal(); - bool getInstallBloodmoon(); - - void setMorrowindPath(const QString &path); - void setTribunalPath(const QString &path); - void setBloodmoonPath(const QString &path); - - QString getMorrowindPath(); - QString getTribunalPath(); - QString getBloodmoonPath(); + void setComponentPath(Wizard::Component component, const QString &path); void setPath(const QString &path); void setIniPath(const QString &path); @@ -47,13 +45,12 @@ namespace Wizard private: - void setMorrowindDone(bool done); - void setTribunalDone(bool done); - void setBloodmoonDone(bool done); + bool getInstallComponent(Component component); - bool getMorrowindDone(); - bool getTribunalDone(); - bool getBloodmoonDone(); + QString getComponentPath(Component component); + + void setComponentDone(Component component, bool done = true); + bool getComponentDone(Component component); bool removeDirectory(const QString &dirName); @@ -75,6 +72,9 @@ namespace Wizard bool installTribunal(); bool installBloodmoon(); + bool installComponent(Component component); + void setupAddon(Component component); + bool mInstallMorrowind; bool mInstallTribunal; bool mInstallBloodmoon; @@ -104,7 +104,7 @@ namespace Wizard signals: void finished(); - void requestFileDialog(const QString &component); + void requestFileDialog(Wizard::Component component); void textChanged(const QString &text); void logTextChanged(const QString &text); From 1262eab03ab51c7629a20f8c225c5c490d6ad7e3 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 28 Jan 2014 01:03:47 +0100 Subject: [PATCH 25/69] Added some nice error messages to the installation process --- apps/wizard/conclusionpage.cpp | 17 ++++++--- apps/wizard/installationpage.cpp | 48 +++++++++++++++++++------ apps/wizard/installationpage.hpp | 2 +- apps/wizard/mainwizard.hpp | 2 ++ apps/wizard/unshield/unshieldworker.cpp | 45 ++++++++++++++--------- apps/wizard/unshield/unshieldworker.hpp | 4 +-- files/ui/wizard/conclusionpage.ui | 6 ++++ files/ui/wizard/installationpage.ui | 4 +-- 8 files changed, 92 insertions(+), 36 deletions(-) diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 55b03b912..61d58eaba 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -13,13 +13,20 @@ Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : void Wizard::ConclusionPage::initializePage() { - if (field("installation.new").toBool() == true) + if (!mWizard->mError) { - textLabel->setText(tr("The OpenMW Wizard successfully installed Morrowind on your computer.\n\n") + - tr("Click Finish to close the Wizard.")); + if (field("installation.new").toBool() == true) + { + textLabel->setText(tr("

The OpenMW Wizard successfully installed Morrowind on your computer.

\ +

Click Finish to close the Wizard.

")); + } else { + textLabel->setText(tr("

The OpenMW Wizard successfully modified your existing Morrowind installation.

\ +

Click Finish to close the Wizard.

")); + } } else { - textLabel->setText(tr("The OpenMW Wizard successfully modified your existing Morrowind installation.\n\n") + - tr("Click Finish to close the Wizard.")); + textLabel->setText(tr("

The OpenMW Wizard failed to install Morrowind on your computer.

\ +

Please report any bugs you might have encountered to our \ + bug tracker.
Make sure to include the installation log.


")); } } diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 47d10c45f..fc527ed3f 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -23,8 +23,8 @@ void Wizard::InstallationPage::initializePage() QString path(field("installation.path").toString()); QStringList components(field("installation.components").toStringList()); - logTextEdit->append(QString("Installing to %1").arg(path)); - logTextEdit->append(QString("Installing %1.").arg(components.join(", "))); + logTextEdit->appendPlainText(QString("Installing to %1").arg(path)); + logTextEdit->appendPlainText(QString("Installing %1.").arg(components.join(", "))); installProgressBar->setMinimum(0); @@ -39,11 +39,11 @@ void Wizard::InstallationPage::initializePage() else { if (components.contains(QLatin1String("Tribunal")) - && mWizard->mInstallations[path]->hasTribunal == false) + && !mWizard->mInstallations[path]->hasTribunal) installProgressBar->setMaximum(100); if (components.contains(QLatin1String("Bloodmoon")) - && mWizard->mInstallations[path]->hasBloodmoon == false) + && !mWizard->mInstallations[path]->hasBloodmoon) installProgressBar->setMaximum(installProgressBar->maximum() + 100); } @@ -76,14 +76,14 @@ void Wizard::InstallationPage::startInstallation() connect(mUnshield, SIGNAL(finished()), this, SLOT(installationFinished()), Qt::QueuedConnection); - connect(mUnshield, SIGNAL(error(QString)), - this, SLOT(installationError(QString)), Qt::QueuedConnection); + connect(mUnshield, SIGNAL(error(QString, QString)), + this, SLOT(installationError(QString, QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(textChanged(QString)), installProgressLabel, SLOT(setText(QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(textChanged(QString)), - logTextEdit, SLOT(append(QString)), Qt::QueuedConnection); + logTextEdit, SLOT(appendPlainText(QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(progressChanged(int)), installProgressBar, SLOT(setValue(int)), Qt::QueuedConnection); @@ -170,17 +170,45 @@ void Wizard::InstallationPage::installationFinished() } -void Wizard::InstallationPage::installationError(const QString &text) +void Wizard::InstallationPage::installationError(const QString &text, const QString &details) { qDebug() << "error: " << text; + + installProgressLabel->setText(tr("Installation failed!")); + + logTextEdit->appendHtml(tr("Error: %1").arg(text)); + logTextEdit->appendHtml(tr("%1").arg(details)); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("An error occurred")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

The Wizard has encountered an error

\ +

The error reported was:

%1

\ +

Press "Show Details..." for more information.

").arg(text)); + + msgBox.setDetailedText(details); + msgBox.exec(); + + mWizard->mError = true; + emit completeChanged(); + } bool Wizard::InstallationPage::isComplete() const { - return mFinished; + if (!mWizard->mError) { + return mFinished; + } else { + return true; + } } int Wizard::InstallationPage::nextId() const { - return MainWizard::Page_Import; + if (!mWizard->mError) { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_Conclusion; + } } diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 3989177ab..7001b2630 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -37,7 +37,7 @@ namespace Wizard void showFileDialog(Wizard::Component component); void installationFinished(); - void installationError(const QString &text); + void installationError(const QString &text, const QString &details); protected: void initializePage(); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 0d4c05fce..31a93ffbb 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -44,6 +44,8 @@ namespace Wizard Files::ConfigurationManager mCfgMgr; + bool mError; + private: void setupInstallations(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 9c635818b..7becec101 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -185,8 +185,8 @@ void Wizard::UnshieldWorker::setupSettings() QFile file(getIniPath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - // TODO: Emit error signal qDebug() << "Error opening .ini file!"; + emit error(tr("Failed to open Morrowind configuration file!"), tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); return; } @@ -240,6 +240,7 @@ bool Wizard::UnshieldWorker::copyFile(const QString &source, const QString &dest } } else { qDebug() << "copy failed! " << file.errorString(); + emit error(tr("Failed to copy file!"), tr("Copying %1 to %2 failed: %3.").arg(source, destination, file.errorString())); } return false; @@ -274,7 +275,6 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString if (info.isDir()) { result = moveDirectory(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } else { -// qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } } @@ -363,6 +363,7 @@ void Wizard::UnshieldWorker::extract() setComponentDone(Wizard::Component_Morrowind, true); } else { qDebug() << "Erorr installing Morrowind"; + return; } } @@ -414,8 +415,10 @@ void Wizard::UnshieldWorker::setupAddon(Component component) if (component == Wizard::Component_Bloodmoon) name = QLatin1String("Bloodmoon"); - if (name.isEmpty()) - return; // Not a valid addon + if (name.isEmpty()) { + emit error(tr("Component parameter is invalid!"), tr("An invalid component parameter was supplied.")); + return; + } if (!disk.cd(name)) { qDebug() << "not found on cd!"; @@ -474,8 +477,10 @@ bool Wizard::UnshieldWorker::installComponent(Component component) break; } - if (name.isEmpty()) + if (name.isEmpty()) { + emit error(tr("Component parameter is invalid!"), tr("An invalid component parameter was supplied.")); return false; + } emit textChanged(tr("Installing %0").arg(name)); @@ -483,6 +488,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component) if (!disk.exists()) { qDebug() << "Component path not set: " << getComponentPath(Wizard::Component_Morrowind); + emit error(tr("Component path not set!"), tr("The source path for %0 was not set.").arg(name)); return false; } @@ -496,29 +502,34 @@ bool Wizard::UnshieldWorker::installComponent(Component component) if (!temp.mkpath(tempPath)) { qDebug() << "Can't make path"; + emit error(tr("Cannot create temporary directory!"), tr("Failed to create %0.").arg(tempPath)); return false; } temp.setPath(tempPath); if (!temp.mkdir(name)) { - qDebug() << "Can't make dir"; + emit error(tr("Cannot create temporary directory!"), tr("Failed to create %0.").arg(temp.absoluteFilePath(name))); return false; } if (!temp.cd(name)) { qDebug() << "Can't cd to dir"; + emit error(tr("Cannot move into temporary directory!"), tr("Failed to move into %0.").arg(temp.absoluteFilePath(name))); return false; } // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + if (!extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath())) + return false; - // TODO: Throw error; // Move the files from the temporary path to the destination folder emit textChanged(tr("Moving installation files")); if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { qDebug() << "failed to move files!"; + emit error(tr("Moving extracted files failed!"), + tr("Failed to move files from %0 to %1.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), + getPath())); return false; } @@ -538,6 +549,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component) moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); } else { qDebug() << "Could not find ini file!"; + emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %0.").arg(iniPath)); return false; } } @@ -599,7 +611,7 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp success = unshield_file_save(unshield, index, fileName.toLatin1().constData()); if (!success) { - emit error(tr("Failed to extract %1").arg(fileName)); + emit error(tr("Failed to extract %1.").arg(QString::fromLatin1(unshield_file_name(unshield, index))), tr("Complete path: %1.").arg(fileName)); dir.remove(fileName); } @@ -611,9 +623,8 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil Unshield *unshield; unshield = unshield_open(cabFile.toLatin1().constData()); - // TODO: Proper error if (!unshield) { - emit error(tr("Failed to open %1").arg(cabFile)); + emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); return false; } @@ -635,15 +646,16 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil return false; } -void Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &outputDir) +bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &outputDir) { + bool success; + Unshield *unshield; unshield = unshield_open(cabFile.toLatin1().constData()); - // TODO: Proper error if (!unshield) { - emit error(tr("Failed to open %1").arg(cabFile)); - return; + emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); + return false; } int counter = 0; @@ -655,11 +667,12 @@ void Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &o for (size_t j=group->first_file; j<=group->last_file; ++j) { if (unshield_file_is_valid(unshield, j)) { - extractFile(unshield, outputDir, group->name, j, counter); + success = extractFile(unshield, outputDir, group->name, j, counter); ++counter; } } } unshield_close(unshield); + return success; } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index fb55bc88f..501daaf30 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -62,7 +62,7 @@ namespace Wizard void setupSettings(); - void extractCab(const QString &cabFile, const QString &outputDir); + bool extractCab(const QString &cabFile, const QString &outputDir); bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); bool findFile(const QString &cabFile, const QString &fileName); @@ -109,7 +109,7 @@ namespace Wizard void textChanged(const QString &text); void logTextChanged(const QString &text); - void error(const QString &text); + void error(const QString &text, const QString &details); void progressChanged(int progress); diff --git a/files/ui/wizard/conclusionpage.ui b/files/ui/wizard/conclusionpage.ui index a27f66789..eb6828b38 100644 --- a/files/ui/wizard/conclusionpage.ui +++ b/files/ui/wizard/conclusionpage.ui @@ -22,9 +22,15 @@ Placeholder + + Qt::RichText + true + + true +
diff --git a/files/ui/wizard/installationpage.ui b/files/ui/wizard/installationpage.ui index 664e10556..6877f1e58 100644 --- a/files/ui/wizard/installationpage.ui +++ b/files/ui/wizard/installationpage.ui @@ -23,7 +23,7 @@ - Extracting: %1 + @@ -35,7 +35,7 @@
- + false From 2ccf4d61124d0e62826e62252648fa3195792b80 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 28 Jan 2014 11:51:13 +0100 Subject: [PATCH 26/69] Post-merge cleanups, using confignmanager for XDG paths now, thanks scrawl! --- apps/wizard/installationtargetpage.cpp | 28 +++++--------------------- apps/wizard/installationtargetpage.hpp | 8 +++++++- apps/wizard/mainwizard.cpp | 6 +++--- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 60acfd5f7..459e7145d 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -2,13 +2,13 @@ #include #include -#include #include "mainwizard.hpp" -Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard) : +Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard, const Files::ConfigurationManager &cfg) : QWizardPage(wizard), - mWizard(wizard) + mWizard(wizard), + mCfgMgr(cfg) { setupUi(this); @@ -17,24 +17,8 @@ Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard) : void Wizard::InstallationTargetPage::initializePage() { - qDebug() << mWizard->field("installation.language"); - -#ifdef Q_OS_WIN - QString path = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation); -#endif - -#ifdef Q_OS_MAC - QString path = QDesktopServices::storageLocation(QDesktopServices::DataLocation); -#endif - -#if defined(Q_OS_LINUX) || defined(Q_OS_UNIX) - QString path = QFile::decodeName(qgetenv("XDG_DATA_HOME")); - - if (path.isEmpty()) - path = QDir::homePath() + QLatin1String("/.local/share"); -#endif - - path.append(QLatin1String("/openmw/data")); + QString path(QFile::decodeName(mCfgMgr.getUserDataPath().string().c_str())); + path.append(QDir::separator() + QLatin1String("data")); if (!QFile::exists(path)) { QDir dir; @@ -43,8 +27,6 @@ void Wizard::InstallationTargetPage::initializePage() QDir dir(path); targetLineEdit->setText(QDir::toNativeSeparators(dir.absolutePath())); - - qDebug() << path; } void Wizard::InstallationTargetPage::on_browseButton_clicked() diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp index ccaac5969..f6d28b013 100644 --- a/apps/wizard/installationtargetpage.hpp +++ b/apps/wizard/installationtargetpage.hpp @@ -5,6 +5,11 @@ #include "ui_installationtargetpage.h" +namespace Files +{ + struct ConfigurationManager; +} + namespace Wizard { class MainWizard; @@ -13,7 +18,7 @@ namespace Wizard { Q_OBJECT public: - InstallationTargetPage(MainWizard *wizard); + InstallationTargetPage(MainWizard *wizard, const Files::ConfigurationManager &cfg); int nextId() const; @@ -22,6 +27,7 @@ namespace Wizard private: MainWizard *mWizard; + const Files::ConfigurationManager &mCfgMgr; protected: void initializePage(); diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index cbc9253ed..aef59fa4e 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -41,7 +41,7 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : void Wizard::MainWizard::setupInstallations() { - QString userPath(QFile::decodeName(mCfgMgr.getUserPath().string().c_str())); + QString userPath(QFile::decodeName(mCfgMgr.getUserConfigPath().string().c_str())); QString globalPath(QFile::decodeName(mCfgMgr.getGlobalPath().string().c_str())); QStringList paths; @@ -122,7 +122,7 @@ void Wizard::MainWizard::setupPages() setPage(Page_MethodSelection, new MethodSelectionPage(this)); setPage(Page_LanguageSelection, new LanguageSelectionPage(this)); setPage(Page_ExistingInstallation, new ExistingInstallationPage(this)); - setPage(Page_InstallationTarget, new InstallationTargetPage(this)); + setPage(Page_InstallationTarget, new InstallationTargetPage(this, mCfgMgr)); setPage(Page_ComponentSelection, new ComponentSelectionPage(this)); setPage(Page_Installation, new InstallationPage(this)); setPage(Page_Import, new ImportPage(this)); @@ -137,7 +137,7 @@ void Wizard::MainWizard::accept() void Wizard::MainWizard::writeSettings() { - QString userPath(QFile::decodeName(mCfgMgr.getUserPath().string().c_str())); + QString userPath(QFile::decodeName(mCfgMgr.getUserConfigPath().string().c_str())); QFile file(userPath + QLatin1String("openmw.cfg")); if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { From 5024f1bf7763ccaf569c483b8e1a6a3c36029e65 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 28 Jan 2014 13:27:09 +0100 Subject: [PATCH 27/69] Added a warning when trying to install tribunal after bloodmoon --- apps/wizard/componentselectionpage.cpp | 61 ++++++++++++++++++++--- apps/wizard/componentselectionpage.hpp | 1 + apps/wizard/existinginstallationpage.cpp | 6 +-- apps/wizard/installationpage.cpp | 16 +++++- apps/wizard/installationtargetpage.cpp | 4 +- apps/wizard/languageselectionpage.cpp | 5 +- apps/wizard/mainwizard.cpp | 17 +++---- apps/wizard/methodselectionpage.cpp | 6 ++- apps/wizard/utils/componentlistwidget.cpp | 18 ++++--- apps/wizard/utils/componentlistwidget.hpp | 3 +- 10 files changed, 99 insertions(+), 38 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 7d934fe84..87779e93d 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -1,7 +1,9 @@ #include "componentselectionpage.hpp" #include +#include #include +#include #include "mainwizard.hpp" @@ -51,7 +53,7 @@ void Wizard::ComponentSelectionPage::initializePage() { componentsList->clear(); - QString path = field("installation.path").toString(); + QString path(field("installation.path").toString()); QListWidgetItem *morrowindItem = new QListWidgetItem(QLatin1String("Morrowind")); QListWidgetItem *tribunalItem = new QListWidgetItem(QLatin1String("Tribunal")); @@ -72,7 +74,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(bloodmoonItem); } else { - if (mWizard->mInstallations[path]->hasMorrowind == true) { + if (mWizard->mInstallations[path]->hasMorrowind) { morrowindItem->setText(tr("Morrowind\t\t(installed)")); morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); morrowindItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -83,7 +85,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(morrowindItem); - if (mWizard->mInstallations[path]->hasTribunal == true) { + if (mWizard->mInstallations[path]->hasTribunal) { tribunalItem->setText(tr("Tribunal\t\t(installed)")); tribunalItem->setFlags(tribunalItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); tribunalItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -94,7 +96,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(tribunalItem); - if (mWizard->mInstallations[path]->hasBloodmoon == true) { + if (mWizard->mInstallations[path]->hasBloodmoon) { bloodmoonItem->setText(tr("Bloodmoon\t\t(installed)")); bloodmoonItem->setFlags(bloodmoonItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); bloodmoonItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -107,10 +109,55 @@ void Wizard::ComponentSelectionPage::initializePage() } } +bool Wizard::ComponentSelectionPage::validatePage() +{ + QStringList components(field("installation.components").toStringList()); + QString path(field("installation.path").toString()); + + qDebug() << components << path << mWizard->mInstallations[path]; + + if (field("installation.new").toBool() == false) { + if (components.contains(QLatin1String("Tribunal")) && !components.contains(QLatin1String("Bloodmoon"))) + { + if (mWizard->mInstallations[path]->hasBloodmoon) + { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("About to install Tribunal after Bloodmoon")); + msgBox.setIcon(QMessageBox::Information); + msgBox.setStandardButtons(QMessageBox::Cancel); + msgBox.setText(tr("

You are about to install Tribunal

\ +

Bloodmoon is already installed on your computer.

\ +

However, it is recommended that you install Tribunal before Bloodmoon.

\ +

Would you like to re-install Bloodmoon?

")); + + QAbstractButton *reinstallButton = msgBox.addButton(tr("Re-install &Bloodmoon"), QMessageBox::ActionRole); + msgBox.exec(); + + + if (msgBox.clickedButton() == reinstallButton) { + // Force reinstallation + mWizard->mInstallations[path]->hasBloodmoon = false; + QList items = componentsList->findItems(QLatin1String("Bloodmoon"), Qt::MatchStartsWith); + + foreach (QListWidgetItem *item, items) { + item->setText(QLatin1String("Bloodmoon")); + item->setCheckState(Qt::Checked); + } + + return true; + } + } + } + } + + return true; +} + int Wizard::ComponentSelectionPage::nextId() const { - if (isCommitPage()) + if (isCommitPage()) { return MainWizard::Page_Installation; - - return MainWizard::Page_Import; + } else { + return MainWizard::Page_Import; + } } diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index 8b4c186d0..ed007fd08 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -16,6 +16,7 @@ namespace Wizard ComponentSelectionPage(MainWizard *wizard); int nextId() const; + virtual bool validatePage(); private slots: void updateButton(QListWidgetItem *item); diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 5c66ceb4f..c1dbf8e9b 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -57,7 +57,7 @@ void Wizard::ExistingInstallationPage::textChanged(const QString &text) // Set the installation path manually, as registerField doesn't work // Because it doesn't accept two widgets operating on a single field if (!text.isEmpty()) - mWizard->setField("installation.path", text); + mWizard->setField(QLatin1String("installation.path"), text); } void Wizard::ExistingInstallationPage::initializePage() @@ -135,7 +135,7 @@ int Wizard::ExistingInstallationPage::nextId() const QString path(field("installation.path").toString()); if (path.isEmpty()) - return MainWizard::Page_ComponentSelection; + return MainWizard::Page_LanguageSelection; if (mWizard->mInstallations[path]->hasMorrowind == true && mWizard->mInstallations[path]->hasTribunal == true && @@ -143,6 +143,6 @@ int Wizard::ExistingInstallationPage::nextId() const { return MainWizard::Page_Import; } else { - return MainWizard::Page_ComponentSelection; + return MainWizard::Page_LanguageSelection; } } diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index fc527ed3f..e838a19c1 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -138,9 +138,23 @@ void Wizard::InstallationPage::startInstallation() void Wizard::InstallationPage::showFileDialog(Wizard::Component component) { + QString name; + switch (component) { + + case Wizard::Component_Morrowind: + name = QLatin1String("Morrowind"); + break; + case Wizard::Component_Tribunal: + name = QLatin1String("Tribunal"); + break; + case Wizard::Component_Bloodmoon: + name = QLatin1String("Bloodmoon"); + break; + } + QString fileName = QFileDialog::getOpenFileName( this, - tr("Select installation file"), + tr("Select %1 installation file").arg(name), QDir::rootPath(), tr("InstallShield header files (*.hdr)")); diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 459e7145d..900a978cf 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -34,7 +34,7 @@ void Wizard::InstallationTargetPage::on_browseButton_clicked() QString selectedPath = QFileDialog::getExistingDirectory( this, tr("Select where to install Morrowind"), - QDir::currentPath(), + QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); qDebug() << selectedPath; @@ -49,5 +49,5 @@ void Wizard::InstallationTargetPage::on_browseButton_clicked() int Wizard::InstallationTargetPage::nextId() const { - return MainWizard::Page_ComponentSelection; + return MainWizard::Page_LanguageSelection; } diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index 102a4c39a..b921d5ddb 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -27,8 +27,5 @@ void Wizard::LanguageSelectionPage::initializePage() int Wizard::LanguageSelectionPage::nextId() const { - if (field("installation.new").toBool() == true) - return MainWizard::Page_InstallationTarget; - - return MainWizard::Page_ExistingInstallation; + return MainWizard::Page_ComponentSelection; } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index aef59fa4e..0913cf9e4 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -73,14 +73,10 @@ void Wizard::MainWizard::setupInstallations() file.close(); } - // Check if the paths actually contain data files + // Check if the paths actually contains a Morrowind installation foreach (const QString path, mGameSettings.getDataDirs()) { - QDir dir(path); - QStringList filters; - filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; - // Add to Wizard installations - if (!dir.entryList(filters).isEmpty()) + if (findFiles(QLatin1String("Morrowind"), path)) addInstallation(path); } @@ -112,8 +108,10 @@ void Wizard::MainWizard::addInstallation(const QString &path) mInstallations.insert(QDir::toNativeSeparators(path), install); // Add it to the openmw.cfg too - mGameSettings.setMultiValue(QString("data"), path); - mGameSettings.addDataDir(path); + if (!mGameSettings.getDataDirs().contains(path)) { + mGameSettings.setMultiValue(QString("data"), path); + mGameSettings.addDataDir(path); + } } void Wizard::MainWizard::setupPages() @@ -168,8 +166,7 @@ bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) return false; // TODO: add MIME handling to make sure the files are real - if (dir.exists(name + QLatin1String(".esm")) && dir.exists(name + QLatin1String(".bsa"))) - { + if (dir.exists(name + QLatin1String(".esm")) && dir.exists(name + QLatin1String(".bsa"))) { return true; } else { return false; diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index 4185fa2bc..6db0779a6 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -13,5 +13,9 @@ Wizard::MethodSelectionPage::MethodSelectionPage(MainWizard *wizard) : int Wizard::MethodSelectionPage::nextId() const { - return MainWizard::Page_LanguageSelection; + if (field("installation.new").toBool() == true) { + return MainWizard::Page_InstallationTarget; + } else { + return MainWizard::Page_ExistingInstallation; + } } diff --git a/apps/wizard/utils/componentlistwidget.cpp b/apps/wizard/utils/componentlistwidget.cpp index c9a1c1940..6a5d019b5 100644 --- a/apps/wizard/utils/componentlistwidget.cpp +++ b/apps/wizard/utils/componentlistwidget.cpp @@ -10,15 +10,9 @@ ComponentListWidget::ComponentListWidget(QWidget *parent) : connect(this, SIGNAL(itemChanged(QListWidgetItem *)), this, SLOT(updateCheckedItems(QListWidgetItem *))); -} - -void ComponentListWidget::addItem(QListWidgetItem *item) -{ - // The model does not emit a dataChanged signal when items are added - // So we need to update manually - QListWidget::insertItem(count(), item); - updateCheckedItems(item); + connect(model(), SIGNAL(rowsInserted(QModelIndex, int, int)), + this, SLOT(updateCheckedItems(QModelIndex, int, int))); } QStringList ComponentListWidget::checkedItems() @@ -27,8 +21,16 @@ QStringList ComponentListWidget::checkedItems() return mCheckedItems; } +void ComponentListWidget::updateCheckedItems(const QModelIndex &index, int start, int end) +{ + updateCheckedItems(item(start)); +} + void ComponentListWidget::updateCheckedItems(QListWidgetItem *item) { + if (!item) + return; + QString text = item->text(); if (item->checkState() == Qt::Checked) { diff --git a/apps/wizard/utils/componentlistwidget.hpp b/apps/wizard/utils/componentlistwidget.hpp index 6869629d3..23965f8a6 100644 --- a/apps/wizard/utils/componentlistwidget.hpp +++ b/apps/wizard/utils/componentlistwidget.hpp @@ -15,13 +15,12 @@ public: QStringList mCheckedItems; QStringList checkedItems(); - void addItem(QListWidgetItem *item); - signals: void checkedItemsChanged(const QStringList &items); private slots: void updateCheckedItems(QListWidgetItem *item); + void updateCheckedItems(const QModelIndex &index, int start, int end); }; #endif // COMPONENTLISTWIDGET_HPP From ceb66d0790ed8aa1c62b6ca2564db311a2921d38 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 28 Jan 2014 13:36:14 +0100 Subject: [PATCH 28/69] Also install the separate Sounds directory for Tribunal --- apps/wizard/unshield/unshieldworker.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 7becec101..d7cda05a0 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -554,6 +554,17 @@ bool Wizard::UnshieldWorker::installComponent(Component component) } } + if (component == Wizard::Component_Tribunal) + { + QFileInfo sounds(temp.absoluteFilePath(QLatin1String("Sounds"))); + + if (sounds.exists()) { + emit textChanged(tr("Extracting: Sound directory")); + copyDirectory(sounds.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Sound")); + } + + } + if (component == Wizard::Component_Bloodmoon) { QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); From e8170adde5b02fe1a66a471831a8d117f6f4b277 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sat, 8 Feb 2014 00:09:25 +0100 Subject: [PATCH 29/69] Some minor changes, implemented suggestions --- apps/wizard/installationpage.cpp | 15 ++++----------- apps/wizard/installationtargetpage.cpp | 22 ++++++++++++++++++++++ apps/wizard/installationtargetpage.hpp | 1 + apps/wizard/unshield/unshieldworker.cpp | 2 ++ apps/wizard/unshield/unshieldworker.hpp | 2 -- files/ui/wizard/importpage.ui | 6 +++--- files/ui/wizard/methodselectionpage.ui | 12 +++++------- 7 files changed, 37 insertions(+), 23 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index e838a19c1..efa6880be 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -32,12 +32,9 @@ void Wizard::InstallationPage::initializePage() // That way installing all three components would yield 300% // When one component is done the bar will be filled by 33% - if (field("installation.new").toBool() == true) - { + if (field("installation.new").toBool() == true) { installProgressBar->setMaximum((components.count() * 100)); - } - else - { + } else { if (components.contains(QLatin1String("Tribunal")) && !mWizard->mInstallations[path]->hasTribunal) installProgressBar->setMaximum(100); @@ -59,8 +56,6 @@ void Wizard::InstallationPage::startInstallation() mUnshield = new UnshieldWorker(); mUnshield->moveToThread(thread); - qRegisterMetaType("Wizard::Component"); - connect(thread, SIGNAL(started()), mUnshield, SLOT(extract())); @@ -125,11 +120,9 @@ void Wizard::InstallationPage::startInstallation() if (language == QLatin1String("Polish")) { mUnshield->setIniCodec(QTextCodec::codecForName("windows-1250")); - } - else if (language == QLatin1String("Russian")) { + } else if (language == QLatin1String("Russian")) { mUnshield->setIniCodec(QTextCodec::codecForName("windows-1251")); - } - else { + } else { mUnshield->setIniCodec(QTextCodec::codecForName("windows-1252")); } diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 900a978cf..306e7221c 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "mainwizard.hpp" @@ -29,6 +30,27 @@ void Wizard::InstallationTargetPage::initializePage() targetLineEdit->setText(QDir::toNativeSeparators(dir.absolutePath())); } +bool Wizard::InstallationTargetPage::validatePage() +{ + QString path(field("installation.path").toString()); + + qDebug() << "Validating path: " << path; + + if (mWizard->findFiles(QLatin1String("Morrowind"), path)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Destination not empty")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

The destination directory is not empty

\ +

An existing Morrowind installation is present in the specified location.

\ +

Please specify a different location, or go back and select the location as an existing installation.

")); + msgBox.exec(); + return false; + } + + return true; +} + void Wizard::InstallationTargetPage::on_browseButton_clicked() { QString selectedPath = QFileDialog::getExistingDirectory( diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp index f6d28b013..0711c1c05 100644 --- a/apps/wizard/installationtargetpage.hpp +++ b/apps/wizard/installationtargetpage.hpp @@ -21,6 +21,7 @@ namespace Wizard InstallationTargetPage(MainWizard *wizard, const Files::ConfigurationManager &cfg); int nextId() const; + virtual bool validatePage(); private slots: void on_browseButton_clicked(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index d7cda05a0..02e50a7e3 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -37,6 +37,8 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : mMorrowindDone = false; mTribunalDone = false; mBloodmoonDone = false; + + qRegisterMetaType("Wizard::Component"); } Wizard::UnshieldWorker::~UnshieldWorker() diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 501daaf30..d110851f4 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -20,8 +20,6 @@ namespace Wizard Component_Bloodmoon }; - //Q_DECLARE_METATYPE(Wizard::Component) - class UnshieldWorker : public QObject { Q_OBJECT diff --git a/files/ui/wizard/importpage.ui b/files/ui/wizard/importpage.ui index 21037f3e8..5b078efca 100644 --- a/files/ui/wizard/importpage.ui +++ b/files/ui/wizard/importpage.ui @@ -6,8 +6,8 @@ 0 0 - 513 - 328 + 511 + 326 @@ -43,7 +43,7 @@ - Import previously selected add-ons (creates a new Content List in the launcher) + Import add-on and plugin selection (creates a new Content List in the launcher) diff --git a/files/ui/wizard/methodselectionpage.ui b/files/ui/wizard/methodselectionpage.ui index e77349f5c..122ef754e 100644 --- a/files/ui/wizard/methodselectionpage.ui +++ b/files/ui/wizard/methodselectionpage.ui @@ -6,8 +6,8 @@ 0 0 - 398 - 298 + 396 + 296 @@ -26,7 +26,7 @@ font-weight:bold; - Install to a new location + Install Morrowind to a new location true @@ -94,7 +94,7 @@ font-weight:bold - Select an existing installation + Select an existing Morrowind installation
@@ -149,8 +149,6 @@
- - - + From 6916c0bc9479a65cadbe384be6ff3bde72c2ce70 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 18 Feb 2014 11:55:26 +0100 Subject: [PATCH 30/69] Wizard now writes valid Morrowind.ini files --- apps/wizard/existinginstallationpage.cpp | 3 +- apps/wizard/inisettings.cpp | 180 ++++++++++++++++++++++- apps/wizard/inisettings.hpp | 13 +- apps/wizard/installationpage.cpp | 1 + apps/wizard/installationtargetpage.cpp | 12 +- apps/wizard/mainwizard.cpp | 1 + apps/wizard/unshield/unshieldworker.cpp | 66 ++++++++- apps/wizard/unshield/unshieldworker.hpp | 6 +- 8 files changed, 262 insertions(+), 20 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index c1dbf8e9b..34e771e3d 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -38,8 +38,7 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() QString path(QDir::toNativeSeparators(info.absolutePath())); QList items = installationsList->findItems(path, Qt::MatchExactly); - if (items.isEmpty()) - { + if (items.isEmpty()) { // Path is not yet in the list, add it mWizard->addInstallation(path); diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index 2c099acec..f34699742 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -1,8 +1,10 @@ #include "inisettings.hpp" +#include #include #include +#include #include #include #include @@ -15,25 +17,40 @@ Wizard::IniSettings::~IniSettings() { } +QStringList Wizard::IniSettings::findKeys(const QString &text) +{ + QStringList result; + + foreach (const QString &key, mSettings.keys()) { + + if (key.startsWith(text)) + result << key; + + } + + return result; +} + bool Wizard::IniSettings::readFile(QTextStream &stream) { + qDebug() << "readFile called!"; // Look for a square bracket, "'\\[" // that has one or more "not nothing" in it, "([^]]+)" // and is closed with a square bracket, "\\]" - QRegExp sectionRe("^\\[([^]]+)\\]"); + QRegExp sectionRe(QLatin1String("^\\[([^]]+)\\]")); // Find any character(s) that is/are not equal sign(s), "[^=]+" // followed by an optional whitespace, an equal sign, and another optional whitespace, "\\s*=\\s*" // and one or more periods, "(.+)" - QRegExp keyRe("^([^=]+)\\s*=\\s*(.+)$"); + QRegExp keyRe(QLatin1String("^([^=]+)\\s*=\\s*(.+)$")); QString currentSection; while (!stream.atEnd()) { - QString line(stream.readLine()); + const QString line(stream.readLine()); - if (line.isEmpty() || line.startsWith(";")) + if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) continue; if (sectionRe.exactMatch(line)) @@ -49,6 +66,7 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) if (!currentSection.isEmpty()) key = currentSection + QLatin1Char('/') + key; + qDebug() << "adding: " << key << value; mSettings[key] = QVariant(value); } } @@ -56,13 +74,161 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) return true; } -bool Wizard::IniSettings::writeFile(QTextStream &stream) +bool Wizard::IniSettings::writeFile(const QString &path, QTextStream &stream) { - qDebug() << "test! " << stream.readAll(); + // Look for a square bracket, "'\\[" + // that has one or more "not nothing" in it, "([^]]+)" + // and is closed with a square bracket, "\\]" + QRegExp sectionRe(QLatin1String("^\\[([^]]+)\\]")); + + // Find any character(s) that is/are not equal sign(s), "[^=]+" + // followed by an optional whitespace, an equal sign, and another optional whitespace, "\\s*=\\s*" + // and one or more periods, "(.+)" + QRegExp keyRe(QLatin1String("^([^=]+)\\s*=\\s*(.+)$")); + + const QStringList keys(mSettings.keys()); + + QString currentSection; + QString buffer; + + qDebug() << "Keys! " << keys; while (!stream.atEnd()) { - qDebug() << "test! " << stream.readLine(); + + const QString line(stream.readLine()); + + if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) { + buffer.append(line + QLatin1String("\n")); + continue; + } + + if (sectionRe.exactMatch(line)) { + buffer.append(line + QLatin1String("\n")); + currentSection = sectionRe.cap(1); + } else if (keyRe.indexIn(line) != -1) { + QString key(keyRe.cap(1).trimmed()); + QString lookupKey(key); + + // Append the section, but only if there is one + if (!currentSection.isEmpty()) + lookupKey = currentSection + QLatin1Char('/') + key; + + buffer.append(key + QLatin1Char('=') + mSettings[lookupKey].toString() + QLatin1String("\n")); + mSettings.remove(lookupKey); + } + } + + // Add the new settings to the buffer + QHashIterator i(mSettings); + while (i.hasNext()) { + i.next(); + + QStringList fullKey(i.key().split(QLatin1Char('/'))); + QString section(fullKey.at(0)); + section.prepend(QLatin1Char('[')); + section.append(QLatin1Char(']')); + QString key(fullKey.at(1)); + + int index = buffer.lastIndexOf(section); + if (index != -1) { + // Append the new keys to the bottom of the section + index = buffer.indexOf(QLatin1Char('['), index + 1); + + if (index == -1 ) + { + // Beginning of next section not found, we are at the last section + if (buffer.lastIndexOf(QLatin1String("\n")) > (buffer.lastIndexOf(section) + section.length())) { + // There is a newline after the section + index = buffer.lastIndexOf(QLatin1String("\n")) - 1; + buffer.insert(index - 2, QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + continue; + } else { + // No newline found, or the last newline is before the last section + // Append the key to the bottom of the file + buffer.append(QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + continue; + } + } + + // Add the key at the index + buffer.insert(index - 1, QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + } else { + // Add the section to the end of the file, because it's not found + buffer.append(QString("\n%1\n").arg(section)); + i.previous(); + } } + // Now we reopen the file, this time we write + QFile file(path); + + if (file.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text)) { + QTextStream in(&file); + in.setCodec(stream.codec()); + + // Write the updated buffer to an empty file + in << buffer; + file.flush(); + file.close(); + } else { + return false; + } + + return true; +} + +bool Wizard::IniSettings::parseInx(const QString &path) +{ + QFile file(path); + + if (file.open(QIODevice::ReadOnly)) + { + + const QByteArray data(file.readAll()); + const QByteArray pattern("\x21\x00\x1A\x01\x04\x00\x04\x97\xFF\x06", 10); + + int i = 0; + while ((i = data.indexOf(pattern, i)) != -1) { + + int next = data.indexOf(pattern, i + 1); + if (next == -1) + break; + + QByteArray array(data.mid(i, (next - i))); + + // Skip some invalid entries + if (array.contains("\x04\x96\xFF")) { + ++i; + continue; + } + + // Remove the pattern from the beginning + array.remove(0, 12); + + int index = array.indexOf("\x06"); + const QString section(array.left(index)); + + // Figure how many characters to read for the key + int lenght = array.indexOf("\x06", section.length() + 3) - (section.length() + 3); + const QString key(array.mid(section.length() + 3, lenght)); + + QString value(array.mid(section.length() + key.length() + 6)); + //qDebug() << section << key << value; + + // Add the value + setValue(section + QLatin1Char('/') + key, QVariant(value)); + + ++i; + } + + file.close(); + } else { + qDebug() << "Failed to open INX file: " << path; + return false; + } + return true; } diff --git a/apps/wizard/inisettings.hpp b/apps/wizard/inisettings.hpp index 66fe96ec1..d425a9b1b 100644 --- a/apps/wizard/inisettings.hpp +++ b/apps/wizard/inisettings.hpp @@ -22,6 +22,11 @@ namespace Wizard return mSettings.value(key, defaultValue); } + inline QList values() const + { + return mSettings.values(); + } + inline void setValue(const QString &key, const QVariant &value) { mSettings.insert(key, value); @@ -32,11 +37,17 @@ namespace Wizard mSettings.remove(key); } + QStringList findKeys(const QString &text); + bool readFile(QTextStream &stream); - bool writeFile(QTextStream &stream); + bool writeFile(const QString &path, QTextStream &stream); + + bool parseInx(const QString &path); private: + int getLastNewline(const QString &buffer, int from); + SettingsMap mSettings; }; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index efa6880be..05d1b4af8 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -110,6 +110,7 @@ void Wizard::InstallationPage::startInstallation() // Set the location of the Morrowind.ini to update mUnshield->setIniPath(mWizard->mInstallations[path]->iniPath); + mUnshield->setupSettings(); } // Set the installation target path diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 306e7221c..daf2c0846 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -21,11 +21,6 @@ void Wizard::InstallationTargetPage::initializePage() QString path(QFile::decodeName(mCfgMgr.getUserDataPath().string().c_str())); path.append(QDir::separator() + QLatin1String("data")); - if (!QFile::exists(path)) { - QDir dir; - dir.mkpath(path); - } - QDir dir(path); targetLineEdit->setText(QDir::toNativeSeparators(dir.absolutePath())); } @@ -36,6 +31,13 @@ bool Wizard::InstallationTargetPage::validatePage() qDebug() << "Validating path: " << path; + // TODO: Check writeability + if (!QFile::exists(path)) { + QDir dir; + dir.mkpath(path); + return true; + } + if (mWizard->findFiles(QLatin1String("Morrowind"), path)) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Destination not empty")); diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 0913cf9e4..d9b2c3ba8 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -127,6 +127,7 @@ void Wizard::MainWizard::setupPages() setPage(Page_Conclusion, new ConclusionPage(this)); setStartId(Page_Intro); } + void Wizard::MainWizard::accept() { writeSettings(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 02e50a7e3..47d3c1be7 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -43,7 +43,6 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : Wizard::UnshieldWorker::~UnshieldWorker() { - } void Wizard::UnshieldWorker::setInstallComponent(Wizard::Component component, bool install) @@ -198,6 +197,29 @@ void Wizard::UnshieldWorker::setupSettings() mIniSettings.readFile(stream); } +void Wizard::UnshieldWorker::writeSettings() +{ + if (getIniPath().isEmpty()) + return; + + QFile file(getIniPath()); + + if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { + qDebug() << "Error opening .ini file!"; + emit error(tr("Failed to open Morrowind configuration file!"), + tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); + return; + } + + QTextStream stream(&file); + stream.setCodec(mIniCodec); + + if (!mIniSettings.writeFile(getIniPath(), stream)) { + emit error(tr("Failed to write Morrowind configuration file!"), + tr("Writing to %1 failed: %2.").arg(getIniPath(), file.errorString())); + } +} + bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) { bool result = true; @@ -383,6 +405,32 @@ void Wizard::UnshieldWorker::extract() setupAddon(Wizard::Component_Bloodmoon); } + // Update Morrowind configuration + if (getInstallComponent(Wizard::Component_Tribunal)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); + } + + if (getInstallComponent(Wizard::Component_Bloodmoon)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); + } + + if (getInstallComponent(Wizard::Component_Tribunal) && + getInstallComponent(Wizard::Component_Bloodmoon)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); + } + + + // Write the settings to the Morrowind config file + writeSettings(); + // Remove the temporary directory removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); @@ -554,6 +602,10 @@ bool Wizard::UnshieldWorker::installComponent(Component component) emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %0.").arg(iniPath)); return false; } + + // Setup Morrowind configuration + setIniPath(getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); + setupSettings(); } if (component == Wizard::Component_Tribunal) @@ -564,7 +616,6 @@ bool Wizard::UnshieldWorker::installComponent(Component component) emit textChanged(tr("Extracting: Sound directory")); copyDirectory(sounds.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Sound")); } - } if (component == Wizard::Component_Bloodmoon) @@ -577,6 +628,15 @@ bool Wizard::UnshieldWorker::installComponent(Component component) copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); } + // Load Morrowind configuration settings from the setup script + QFileInfo inx(disk.absoluteFilePath(QLatin1String("setup.inx"))); + + if (inx.exists()) { + emit textChanged(tr("Updating Morrowind configuration file")); + mIniSettings.parseInx(inx.absoluteFilePath()); + } else { + qDebug() << "setup.inx not found!"; + } } emit textChanged(tr("%0 installation finished!").arg(name)); @@ -649,7 +709,7 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil { QString current(QString::fromLatin1(unshield_file_name(unshield, j))); - qDebug() << "File is: " << unshield_file_name(unshield, j); + qDebug() << "File is: " << current; if (current == fileName) return true; // File is found! } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index d110851f4..8aa54c9c0 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -41,8 +41,12 @@ namespace Wizard void setIniCodec(QTextCodec *codec); + void setupSettings(); + private: + void writeSettings(); + bool getInstallComponent(Component component); QString getComponentPath(Component component); @@ -58,8 +62,6 @@ namespace Wizard bool moveFile(const QString &source, const QString &destination); bool moveDirectory(const QString &source, const QString &destination); - void setupSettings(); - bool extractCab(const QString &cabFile, const QString &outputDir); bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); bool findFile(const QString &cabFile, const QString &fileName); From 755c99df3ccd14db12a550f5e38e429f48909935 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 18 Feb 2014 12:44:27 +0100 Subject: [PATCH 31/69] Some minor fixes --- apps/wizard/componentselectionpage.cpp | 14 +++++------ apps/wizard/conclusionpage.cpp | 8 ++++++- apps/wizard/existinginstallationpage.cpp | 4 ++-- apps/wizard/inisettings.cpp | 30 ++++++++---------------- apps/wizard/installationpage.cpp | 14 +++++------ apps/wizard/installationtargetpage.cpp | 2 +- apps/wizard/methodselectionpage.cpp | 4 ++-- 7 files changed, 36 insertions(+), 40 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 87779e93d..19a07438d 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -16,7 +16,7 @@ Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : setCommitPage(true); setButtonText(QWizard::CommitButton, tr("&Install")); - registerField("installation.components", componentsList); + registerField(QLatin1String("installation.components"), componentsList); connect(componentsList, SIGNAL(itemChanged(QListWidgetItem *)), this, SLOT(updateButton(QListWidgetItem *))); @@ -25,7 +25,7 @@ Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : void Wizard::ComponentSelectionPage::updateButton(QListWidgetItem *item) { - if (field("installation.new").toBool() == true) + if (field(QLatin1String("installation.new")).toBool() == true) return; // Morrowind is always checked here bool unchecked = true; @@ -53,13 +53,13 @@ void Wizard::ComponentSelectionPage::initializePage() { componentsList->clear(); - QString path(field("installation.path").toString()); + QString path(field(QLatin1String("installation.path")).toString()); QListWidgetItem *morrowindItem = new QListWidgetItem(QLatin1String("Morrowind")); QListWidgetItem *tribunalItem = new QListWidgetItem(QLatin1String("Tribunal")); QListWidgetItem *bloodmoonItem = new QListWidgetItem(QLatin1String("Bloodmoon")); - if (field("installation.new").toBool() == true) + if (field(QLatin1String("installation.new")).toBool() == true) { morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); morrowindItem->setData(Qt::CheckStateRole, Qt::Checked); @@ -111,12 +111,12 @@ void Wizard::ComponentSelectionPage::initializePage() bool Wizard::ComponentSelectionPage::validatePage() { - QStringList components(field("installation.components").toStringList()); - QString path(field("installation.path").toString()); + QStringList components(field(QLatin1String("installation.components")).toStringList()); + QString path(field(QLatin1String("installation.path")).toString()); qDebug() << components << path << mWizard->mInstallations[path]; - if (field("installation.new").toBool() == false) { + if (field(QLatin1String("installation.new")).toBool() == false) { if (components.contains(QLatin1String("Tribunal")) && !components.contains(QLatin1String("Bloodmoon"))) { if (mWizard->mInstallations[path]->hasBloodmoon) diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 61d58eaba..d734d39ab 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -13,9 +13,15 @@ Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : void Wizard::ConclusionPage::initializePage() { + // Write the path to openmw.cfg + if (field(QLatin1String("installation.new")).toBool() == true) { + QString path(field(QLatin1String("installation.path")).toString()); + mWizard->addInstallation(path); + } + if (!mWizard->mError) { - if (field("installation.new").toBool() == true) + if (field(QLatin1String("installation.new")).toBool() == true) { textLabel->setText(tr("

The OpenMW Wizard successfully installed Morrowind on your computer.

\

Click Finish to close the Wizard.

")); diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 34e771e3d..f0f5c8573 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -82,7 +82,7 @@ bool Wizard::ExistingInstallationPage::validatePage() // It can be missing entirely // Or failed to be detected due to the target being a symlink - QString path(field("installation.path").toString()); + QString path(field(QLatin1String("installation.path")).toString()); QFile file(mWizard->mInstallations[path]->iniPath); if (!file.exists()) { @@ -131,7 +131,7 @@ bool Wizard::ExistingInstallationPage::isComplete() const int Wizard::ExistingInstallationPage::nextId() const { - QString path(field("installation.path").toString()); + QString path(field(QLatin1String("installation.path")).toString()); if (path.isEmpty()) return MainWizard::Page_LanguageSelection; diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index f34699742..a2c255684 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -131,30 +131,20 @@ bool Wizard::IniSettings::writeFile(const QString &path, QTextStream &stream) int index = buffer.lastIndexOf(section); if (index != -1) { - // Append the new keys to the bottom of the section + // Look for the next section index = buffer.indexOf(QLatin1Char('['), index + 1); - if (index == -1 ) - { - // Beginning of next section not found, we are at the last section - if (buffer.lastIndexOf(QLatin1String("\n")) > (buffer.lastIndexOf(section) + section.length())) { - // There is a newline after the section - index = buffer.lastIndexOf(QLatin1String("\n")) - 1; - buffer.insert(index - 2, QString("\n%1=%2").arg(key, i.value().toString())); - mSettings.remove(i.key()); - continue; - } else { - // No newline found, or the last newline is before the last section - // Append the key to the bottom of the file - buffer.append(QString("\n%1=%2").arg(key, i.value().toString())); - mSettings.remove(i.key()); - continue; - } + if (index == -1 ) { + // We are at the last section, append it to the bottom of the file + buffer.append(QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + continue; + } else { + // Not at last section, add the key at the index + buffer.insert(index - 1, QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); } - // Add the key at the index - buffer.insert(index - 1, QString("\n%1=%2").arg(key, i.value().toString())); - mSettings.remove(i.key()); } else { // Add the section to the end of the file, because it's not found buffer.append(QString("\n%1\n").arg(section)); diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 05d1b4af8..774e02f3c 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -20,8 +20,8 @@ Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : void Wizard::InstallationPage::initializePage() { - QString path(field("installation.path").toString()); - QStringList components(field("installation.components").toStringList()); + QString path(field(QLatin1String("installation.path")).toString()); + QStringList components(field(QLatin1String("installation.components")).toStringList()); logTextEdit->appendPlainText(QString("Installing to %1").arg(path)); logTextEdit->appendPlainText(QString("Installing %1.").arg(components.join(", "))); @@ -32,7 +32,7 @@ void Wizard::InstallationPage::initializePage() // That way installing all three components would yield 300% // When one component is done the bar will be filled by 33% - if (field("installation.new").toBool() == true) { + if (field(QLatin1String("installation.new")).toBool() == true) { installProgressBar->setMaximum((components.count() * 100)); } else { if (components.contains(QLatin1String("Tribunal")) @@ -49,8 +49,8 @@ void Wizard::InstallationPage::initializePage() void Wizard::InstallationPage::startInstallation() { - QStringList components(field("installation.components").toStringList()); - QString path(field("installation.path").toString()); + QStringList components(field(QLatin1String("installation.components")).toStringList()); + QString path(field(QLatin1String("installation.path")).toString()); QThread *thread = new QThread(); mUnshield = new UnshieldWorker(); @@ -86,7 +86,7 @@ void Wizard::InstallationPage::startInstallation() connect(mUnshield, SIGNAL(requestFileDialog(Wizard::Component)), this, SLOT(showFileDialog(Wizard::Component)), Qt::QueuedConnection); - if (field("installation.new").toBool() == true) + if (field(QLatin1String("installation.new")).toBool() == true) { // Always install Morrowind mUnshield->setInstallComponent(Wizard::Component_Morrowind, true); @@ -117,7 +117,7 @@ void Wizard::InstallationPage::startInstallation() mUnshield->setPath(path); // Set the right codec to use for Morrowind.ini - QString language(field("installation.language").toString()); + QString language(field(QLatin1String("installation.language")).toString()); if (language == QLatin1String("Polish")) { mUnshield->setIniCodec(QTextCodec::codecForName("windows-1250")); diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index daf2c0846..521dc2cea 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -27,7 +27,7 @@ void Wizard::InstallationTargetPage::initializePage() bool Wizard::InstallationTargetPage::validatePage() { - QString path(field("installation.path").toString()); + QString path(field(QLatin1String("installation.path")).toString()); qDebug() << "Validating path: " << path; diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index 6db0779a6..b1483ac3a 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -8,12 +8,12 @@ Wizard::MethodSelectionPage::MethodSelectionPage(MainWizard *wizard) : { setupUi(this); - registerField("installation.new", newLocationRadioButton); + registerField(QLatin1String("installation.new"), newLocationRadioButton); } int Wizard::MethodSelectionPage::nextId() const { - if (field("installation.new").toBool() == true) { + if (field(QLatin1String("installation.new")).toBool() == true) { return MainWizard::Page_InstallationTarget; } else { return MainWizard::Page_ExistingInstallation; From 36d4287da423cac163f3cc817639deaff6a1a281 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 24 Feb 2014 15:59:44 +0100 Subject: [PATCH 32/69] Fixed openmw.cfg handling: file should not be cleared anymore --- apps/wizard/mainwizard.cpp | 64 ++++++++++++++++++++----- apps/wizard/unshield/unshieldworker.cpp | 21 ++++---- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index d9b2c3ba8..eb1e3b304 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -41,9 +41,36 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : void Wizard::MainWizard::setupInstallations() { - QString userPath(QFile::decodeName(mCfgMgr.getUserConfigPath().string().c_str())); - QString globalPath(QFile::decodeName(mCfgMgr.getGlobalPath().string().c_str())); + QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + QString globalPath(QString::fromUtf8(mCfgMgr.getGlobalPath().string().c_str())); + QString message(tr("

Could not open %1 for reading

\ +

Please make sure you have the right permissions \ + and try again.

")); + + // Load the user config file first, separately + // So we can write it properly, uncontaminated + QString path(userPath + QLatin1String("openmw.cfg")); + QFile file(path); + + qDebug() << "Loading config file:" << qPrintable(path); + + if (file.exists()) { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(message.arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mGameSettings.readUserFile(stream); + } + // Now the rest QStringList paths; paths.append(userPath + QLatin1String("openmw.cfg")); paths.append(QLatin1String("openmw.cfg")); @@ -59,10 +86,8 @@ void Wizard::MainWizard::setupInstallations() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); + msgBox.setText(message.arg(file.fileName())); + return qApp->quit(); } QTextStream stream(&file); @@ -73,7 +98,7 @@ void Wizard::MainWizard::setupInstallations() file.close(); } - // Check if the paths actually contains a Morrowind installation + // Check if the paths actually contain a Morrowind installation foreach (const QString path, mGameSettings.getDataDirs()) { if (findFiles(QLatin1String("Morrowind"), path)) @@ -136,7 +161,24 @@ void Wizard::MainWizard::accept() void Wizard::MainWizard::writeSettings() { - QString userPath(QFile::decodeName(mCfgMgr.getUserConfigPath().string().c_str())); + QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + QDir dir(userPath); + + if (!dir.exists()) { + if (!dir.mkpath(userPath)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error creating OpenMW configuration directory")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not create %1

\ +

Please make sure you have the right permissions \ + and try again.

").arg(userPath)); + msgBox.exec(); + return qApp->quit(); + } + } + + // Game settings QFile file(userPath + QLatin1String("openmw.cfg")); if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { @@ -145,9 +187,9 @@ void Wizard::MainWizard::writeSettings() msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not open or create %0 for writing

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); + msgBox.setText(tr("

Could not open %1 for writing

\ +

Please make sure you have the right permissions \ + and try again.

").arg(file.fileName())); msgBox.exec(); return qApp->quit(); } diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 47d3c1be7..6a19673f3 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -656,7 +656,7 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp path.append(prefix + QDir::separator()); if (directory >= 0) - path.append(QString::fromLatin1(unshield_directory_name(unshield, directory)) + QDir::separator()); + path.append(QString::fromUtf8(unshield_directory_name(unshield, directory)) + QDir::separator()); // Ensure the path has the right separators path.replace(QLatin1Char('\\'), QDir::separator()); @@ -667,7 +667,7 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp dir.mkpath(path); QString fileName(path); - fileName.append(QString::fromLatin1(unshield_file_name(unshield, index))); + fileName.append(QString::fromUtf8(unshield_file_name(unshield, index))); // Calculate the percentage done int progress = (((float) counter / (float) unshield_file_count(unshield)) * 100); @@ -678,13 +678,14 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp if (getComponentDone(Wizard::Component_Tribunal)) progress = progress + 100; - emit textChanged(tr("Extracting: %1").arg(QString::fromLatin1(unshield_file_name(unshield, index)))); + emit textChanged(tr("Extracting: %1").arg(QString::fromUtf8(unshield_file_name(unshield, index)))); emit progressChanged(progress); - success = unshield_file_save(unshield, index, fileName.toLatin1().constData()); + QByteArray array(fileName.toUtf8()); + success = unshield_file_save(unshield, index, array.constData()); if (!success) { - emit error(tr("Failed to extract %1.").arg(QString::fromLatin1(unshield_file_name(unshield, index))), tr("Complete path: %1.").arg(fileName)); + emit error(tr("Failed to extract %1.").arg(QString::fromUtf8(unshield_file_name(unshield, index))), tr("Complete path: %1.").arg(fileName)); dir.remove(fileName); } @@ -693,8 +694,10 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fileName) { + QByteArray array(cabFile.toUtf8()); + Unshield *unshield; - unshield = unshield_open(cabFile.toLatin1().constData()); + unshield = unshield_open(array.constData()); if (!unshield) { emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); @@ -707,7 +710,7 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil for (size_t j=group->first_file; j<=group->last_file; ++j) { - QString current(QString::fromLatin1(unshield_file_name(unshield, j))); + QString current(QString::fromUtf8(unshield_file_name(unshield, j))); qDebug() << "File is: " << current; if (current == fileName) @@ -723,8 +726,10 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &o { bool success; + QByteArray array(cabFile.toUtf8()); + Unshield *unshield; - unshield = unshield_open(cabFile.toLatin1().constData()); + unshield = unshield_open(array.constData()); if (!unshield) { emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); From 3792b301e9378127d5fae3ddf9b2f4eefa602e73 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 25 Feb 2014 15:33:30 +0100 Subject: [PATCH 33/69] Wizard now runs the ini-importer to import settings from Morrowind.ini --- apps/launcher/maindialog.cpp | 106 +--------------- apps/wizard/conclusionpage.cpp | 18 ++- apps/wizard/importpage.cpp | 3 + apps/wizard/inisettings.cpp | 154 ++++++++++++------------ apps/wizard/installationpage.cpp | 21 +++- apps/wizard/installationtargetpage.cpp | 29 ++++- apps/wizard/mainwizard.cpp | 83 +++++++++++-- apps/wizard/mainwizard.hpp | 2 + apps/wizard/unshield/unshieldworker.cpp | 27 +++-- components/CMakeLists.txt | 4 + components/config/gamesettings.hpp | 2 + components/process/processinvoker.cpp | 109 +++++++++++++++++ components/process/processinvoker.hpp | 23 ++++ files/ui/wizard/importpage.ui | 6 +- 14 files changed, 373 insertions(+), 214 deletions(-) create mode 100644 components/process/processinvoker.cpp create mode 100644 components/process/processinvoker.hpp diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 56b3186ff..3b7a3273a 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -1,6 +1,7 @@ #include "maindialog.hpp" #include +#include #include #include @@ -29,6 +30,8 @@ #include "graphicspage.hpp" #include "datafilespage.hpp" +using namespace Process; + Launcher::MainDialog::MainDialog(QWidget *parent) : mGameSettings(mCfgMgr), QMainWindow (parent) { @@ -273,7 +276,7 @@ bool Launcher::MainDialog::showFirstRunDialog() arguments.append(QString("--cfg")); arguments.append(path); - if (!startProgram(QString("mwiniimport"), arguments, false)) + if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) return false; // Re-read the game settings @@ -810,103 +813,6 @@ void Launcher::MainDialog::play() } // Launch the game detached - startProgram(QString("openmw"), true); - qApp->quit(); -} - -bool Launcher::MainDialog::startProgram(const QString &name, const QStringList &arguments, bool detached) -{ - QString path = name; -#ifdef Q_OS_WIN - path.append(QString(".exe")); -#elif defined(Q_OS_MAC) - QDir dir(QCoreApplication::applicationDirPath()); - path = dir.absoluteFilePath(name); -#else - path.prepend(QString("./")); -#endif - - QFile file(path); - - QProcess process; - QFileInfo info(file); - - if (!file.exists()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not find %1

\ - The application is not found.
\ - Please make sure OpenMW is installed correctly and try again.
").arg(info.fileName())); - msgBox.exec(); - - return false; - } - - if (!info.isExecutable()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not start %1

\ - The application is not executable.
\ - Please make sure you have the right permissions and try again.
").arg(info.fileName())); - msgBox.exec(); - - return false; - } - - // Start the executable - if (detached) { - if (!process.startDetached(path, arguments)) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not start %1

\ - An error occurred while starting %1.

\ - Press \"Show Details...\" for more information.
").arg(info.fileName())); - msgBox.setDetailedText(process.errorString()); - msgBox.exec(); - - return false; - } - } else { - process.start(path, arguments); - if (!process.waitForFinished()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not start %1

\ - An error occurred while starting %1.

\ - Press \"Show Details...\" for more information.
").arg(info.fileName())); - msgBox.setDetailedText(process.errorString()); - msgBox.exec(); - - return false; - } - - if (process.exitCode() != 0 || process.exitStatus() == QProcess::CrashExit) { - QString error(process.readAllStandardError()); - error.append(tr("\nArguments:\n")); - error.append(arguments.join(" ")); - - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error running executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Executable %1 returned an error

\ - An error occurred while running %1.

\ - Press \"Show Details...\" for more information.
").arg(info.fileName())); - msgBox.setDetailedText(error); - msgBox.exec(); - - return false; - } - } - - return true; - + if (ProcessInvoker::startProcess(QLatin1String("openmw"), true)) + qApp->quit(); } diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index d734d39ab..3681459b7 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -13,10 +13,20 @@ Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : void Wizard::ConclusionPage::initializePage() { - // Write the path to openmw.cfg - if (field(QLatin1String("installation.new")).toBool() == true) { - QString path(field(QLatin1String("installation.path")).toString()); - mWizard->addInstallation(path); +// // Write the path to openmw.cfg +// if (field(QLatin1String("installation.new")).toBool() == true) { +// QString path(field(QLatin1String("installation.path")).toString()); +// mWizard->addInstallation(path); +// } + + if (!mWizard->mError) + { + if ((field(QLatin1String("installation.new")).toBool() == true) + | (field(QLatin1String("installation.import-settings")).toBool() == true)) + { + qDebug() << "IMPORT SETTINGS"; + mWizard->runSettingsImporter(); + } } if (!mWizard->mError) diff --git a/apps/wizard/importpage.cpp b/apps/wizard/importpage.cpp index 059683cf0..b49105faa 100644 --- a/apps/wizard/importpage.cpp +++ b/apps/wizard/importpage.cpp @@ -7,6 +7,9 @@ Wizard::ImportPage::ImportPage(MainWizard *wizard) : mWizard(wizard) { setupUi(this); + + registerField(QLatin1String("installation.import-settings"), importCheckBox); + registerField(QLatin1String("installation.import-addons"), addonsCheckBox); } int Wizard::ImportPage::nextId() const diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index a2c255684..8c733ce3b 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -51,7 +51,7 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) const QString line(stream.readLine()); if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) - continue; + continue; if (sectionRe.exactMatch(line)) { @@ -91,83 +91,81 @@ bool Wizard::IniSettings::writeFile(const QString &path, QTextStream &stream) QString currentSection; QString buffer; - qDebug() << "Keys! " << keys; - - while (!stream.atEnd()) { - - const QString line(stream.readLine()); - - if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) { - buffer.append(line + QLatin1String("\n")); - continue; - } - - if (sectionRe.exactMatch(line)) { - buffer.append(line + QLatin1String("\n")); - currentSection = sectionRe.cap(1); - } else if (keyRe.indexIn(line) != -1) { - QString key(keyRe.cap(1).trimmed()); - QString lookupKey(key); - - // Append the section, but only if there is one - if (!currentSection.isEmpty()) - lookupKey = currentSection + QLatin1Char('/') + key; - - buffer.append(key + QLatin1Char('=') + mSettings[lookupKey].toString() + QLatin1String("\n")); - mSettings.remove(lookupKey); - } - } - - // Add the new settings to the buffer - QHashIterator i(mSettings); - while (i.hasNext()) { - i.next(); - - QStringList fullKey(i.key().split(QLatin1Char('/'))); - QString section(fullKey.at(0)); - section.prepend(QLatin1Char('[')); - section.append(QLatin1Char(']')); - QString key(fullKey.at(1)); - - int index = buffer.lastIndexOf(section); - if (index != -1) { - // Look for the next section - index = buffer.indexOf(QLatin1Char('['), index + 1); - - if (index == -1 ) { - // We are at the last section, append it to the bottom of the file - buffer.append(QString("\n%1=%2").arg(key, i.value().toString())); - mSettings.remove(i.key()); - continue; - } else { - // Not at last section, add the key at the index - buffer.insert(index - 1, QString("\n%1=%2").arg(key, i.value().toString())); - mSettings.remove(i.key()); - } - - } else { - // Add the section to the end of the file, because it's not found - buffer.append(QString("\n%1\n").arg(section)); - i.previous(); - } - } - - // Now we reopen the file, this time we write - QFile file(path); - - if (file.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text)) { - QTextStream in(&file); - in.setCodec(stream.codec()); - - // Write the updated buffer to an empty file - in << buffer; - file.flush(); - file.close(); - } else { - return false; - } - - return true; + while (!stream.atEnd()) { + + const QString line(stream.readLine()); + + if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) { + buffer.append(line + QLatin1String("\n")); + continue; + } + + if (sectionRe.exactMatch(line)) { + buffer.append(line + QLatin1String("\n")); + currentSection = sectionRe.cap(1); + } else if (keyRe.indexIn(line) != -1) { + QString key(keyRe.cap(1).trimmed()); + QString lookupKey(key); + + // Append the section, but only if there is one + if (!currentSection.isEmpty()) + lookupKey = currentSection + QLatin1Char('/') + key; + + buffer.append(key + QLatin1Char('=') + mSettings[lookupKey].toString() + QLatin1String("\n")); + mSettings.remove(lookupKey); + } + } + + // Add the new settings to the buffer + QHashIterator i(mSettings); + while (i.hasNext()) { + i.next(); + + QStringList fullKey(i.key().split(QLatin1Char('/'))); + QString section(fullKey.at(0)); + section.prepend(QLatin1Char('[')); + section.append(QLatin1Char(']')); + QString key(fullKey.at(1)); + + int index = buffer.lastIndexOf(section); + if (index != -1) { + // Look for the next section + index = buffer.indexOf(QLatin1Char('['), index + 1); + + if (index == -1 ) { + // We are at the last section, append it to the bottom of the file + buffer.append(QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + continue; + } else { + // Not at last section, add the key at the index + buffer.insert(index - 1, QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + } + + } else { + // Add the section to the end of the file, because it's not found + buffer.append(QString("\n%1\n").arg(section)); + i.previous(); + } + } + + // Now we reopen the file, this time we write + QFile file(path); + + if (file.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text)) { + QTextStream in(&file); + in.setCodec(stream.codec()); + + // Write the updated buffer to an empty file + in << buffer; + file.flush(); + file.close(); + } else { + return false; + } + + return true; } bool Wizard::IniSettings::parseInx(const QString &path) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 774e02f3c..f104c8195 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -154,6 +154,11 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) if (fileName.isEmpty()) { qDebug() << "Cancel was clicked!"; + + logTextEdit->appendHtml(tr("


\ + Error: The installation was aborted by the user

")); + mWizard->mError = true; + emit completeChanged(); return; } @@ -184,8 +189,10 @@ void Wizard::InstallationPage::installationError(const QString &text, const QStr installProgressLabel->setText(tr("Installation failed!")); - logTextEdit->appendHtml(tr("Error: %1").arg(text)); - logTextEdit->appendHtml(tr("%1").arg(details)); + logTextEdit->appendHtml(tr("


\ + Error: %1

").arg(text)); + logTextEdit->appendHtml(tr("

\ + %1

").arg(details)); QMessageBox msgBox; msgBox.setWindowTitle(tr("An error occurred")); @@ -214,9 +221,13 @@ bool Wizard::InstallationPage::isComplete() const int Wizard::InstallationPage::nextId() const { - if (!mWizard->mError) { - return MainWizard::Page_Import; - } else { + if (field(QLatin1String("installation.new")).toBool() == true) { return MainWizard::Page_Conclusion; + } else { + if (!mWizard->mError) { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_Conclusion; + } } } diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 521dc2cea..d89d54a00 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -31,11 +31,34 @@ bool Wizard::InstallationTargetPage::validatePage() qDebug() << "Validating path: " << path; - // TODO: Check writeability if (!QFile::exists(path)) { QDir dir; - dir.mkpath(path); - return true; + + if (!dir.mkpath(path)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error creating destination")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not create the destination directory

\ +

Please make sure you have the right permissions \ + and try again, or specify a different location.

")); + msgBox.exec(); + return false; + } + } + + QFileInfo info(path); + + if (!info.isWritable()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Insufficient permissions")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not write to the destination directory

\ +

Please make sure you have the right permissions \ + and try again, or specify a different location.

")); + msgBox.exec(); + return false; } if (mWizard->findFiles(QLatin1String("Morrowind"), path)) { diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index eb1e3b304..fced2d39e 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -1,5 +1,7 @@ #include "mainwizard.hpp" +#include + #include #include @@ -17,6 +19,8 @@ #include "importpage.hpp" #include "conclusionpage.hpp" +using namespace Process; + Wizard::MainWizard::MainWizard(QWidget *parent) : mGameSettings(mCfgMgr), QWizard(parent) @@ -35,11 +39,12 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); + setupGameSettings(); setupInstallations(); setupPages(); } -void Wizard::MainWizard::setupInstallations() +void Wizard::MainWizard::setupGameSettings() { QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); QString globalPath(QString::fromUtf8(mCfgMgr.getGlobalPath().string().c_str())); @@ -97,14 +102,74 @@ void Wizard::MainWizard::setupInstallations() } file.close(); } +} +void Wizard::MainWizard::setupInstallations() +{ // Check if the paths actually contain a Morrowind installation foreach (const QString path, mGameSettings.getDataDirs()) { if (findFiles(QLatin1String("Morrowind"), path)) addInstallation(path); } +} +void Wizard::MainWizard::runSettingsImporter() +{ + QString path(field(QLatin1String("installation.path")).toString()); + + // Create the file if it doesn't already exist, else the importer will fail + QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + QFile file(userPath + QLatin1String("openmw.cfg")); + + if (!file.exists()) { + if (!file.open(QIODevice::ReadWrite)) { + // File cannot be created + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not open or create %1 for writing

\ +

Please make sure you have the right permissions \ + and try again.

").arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + + file.close(); + } + + // Construct the arguments to run the importer + QStringList arguments; + + // Import plugin selection? + if (field(QLatin1String("installation.import-addons")).toBool() == true) + arguments.append(QLatin1String("--game-files")); + + arguments.append(QLatin1String("--encoding")); + + // Set encoding + QString language(field(QLatin1String("installation.language")).toString()); + + if (language == QLatin1String("Polish")) { + arguments.append(QLatin1String("windows-1250")); + } else if (language == QLatin1String("Russian")) { + arguments.append(QLatin1String("windows-1251")); + } else { + arguments.append(QLatin1String("windows-1252")); + } + + // Now the paths + arguments.append(QLatin1String("--ini")); + arguments.append(path + QDir::separator() + QLatin1String("Morrowind.ini")); + arguments.append(QLatin1String("--cfg")); + arguments.append(userPath + QLatin1String("openmw.cfg")); + + if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) + return qApp->quit();; + + // Re-read the game settings + setupGameSettings(); } void Wizard::MainWizard::addInstallation(const QString &path) @@ -134,7 +199,7 @@ void Wizard::MainWizard::addInstallation(const QString &path) // Add it to the openmw.cfg too if (!mGameSettings.getDataDirs().contains(path)) { - mGameSettings.setMultiValue(QString("data"), path); + mGameSettings.setMultiValue(QLatin1String("data"), path); mGameSettings.addDataDir(path); } } @@ -161,6 +226,12 @@ void Wizard::MainWizard::accept() void Wizard::MainWizard::writeSettings() { + QString path(field(QLatin1String("installation.path")).toString()); + + // Make sure the installation path is the last data= entry + mGameSettings.removeDataDir(path); + mGameSettings.addDataDir(path); + QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); QDir dir(userPath); @@ -209,10 +280,6 @@ bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) return false; // TODO: add MIME handling to make sure the files are real - if (dir.exists(name + QLatin1String(".esm")) && dir.exists(name + QLatin1String(".bsa"))) { - return true; - } else { - return false; - } - + return (dir.entryList().contains(name + QLatin1String(".esm"), Qt::CaseInsensitive) + && dir.entryList().contains(name + QLatin1String(".bsa"), Qt::CaseInsensitive)); } diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 31a93ffbb..7520ba637 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -39,6 +39,7 @@ namespace Wizard bool findFiles(const QString &name, const QString &path); void addInstallation(const QString &path); + void runSettingsImporter(); QMap mInstallations; @@ -48,6 +49,7 @@ namespace Wizard private: + void setupGameSettings(); void setupInstallations(); void setupPages(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 6a19673f3..64d15e92a 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -477,7 +477,6 @@ void Wizard::UnshieldWorker::setupAddon(Component component) mWait.wait(&mLock); } else if (disk.exists(QLatin1String("data1.hdr"))) { - qDebug() << "Exists! " << disk.absolutePath(); setComponentPath(component, disk.absolutePath()); } @@ -532,13 +531,13 @@ bool Wizard::UnshieldWorker::installComponent(Component component) return false; } - emit textChanged(tr("Installing %0").arg(name)); + emit textChanged(tr("Installing %1").arg(name)); QDir disk(getComponentPath(component)); if (!disk.exists()) { qDebug() << "Component path not set: " << getComponentPath(Wizard::Component_Morrowind); - emit error(tr("Component path not set!"), tr("The source path for %0 was not set.").arg(name)); + emit error(tr("Component path not set!"), tr("The source path for %1 was not set.").arg(name)); return false; } @@ -552,20 +551,20 @@ bool Wizard::UnshieldWorker::installComponent(Component component) if (!temp.mkpath(tempPath)) { qDebug() << "Can't make path"; - emit error(tr("Cannot create temporary directory!"), tr("Failed to create %0.").arg(tempPath)); + emit error(tr("Cannot create temporary directory!"), tr("Failed to create %1.").arg(tempPath)); return false; } temp.setPath(tempPath); if (!temp.mkdir(name)) { - emit error(tr("Cannot create temporary directory!"), tr("Failed to create %0.").arg(temp.absoluteFilePath(name))); + emit error(tr("Cannot create temporary directory!"), tr("Failed to create %1.").arg(temp.absoluteFilePath(name))); return false; } if (!temp.cd(name)) { qDebug() << "Can't cd to dir"; - emit error(tr("Cannot move into temporary directory!"), tr("Failed to move into %0.").arg(temp.absoluteFilePath(name))); + emit error(tr("Cannot move into temporary directory!"), tr("Failed to move into %1.").arg(temp.absoluteFilePath(name))); return false; } @@ -578,7 +577,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component) if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { qDebug() << "failed to move files!"; emit error(tr("Moving extracted files failed!"), - tr("Failed to move files from %0 to %1.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), + tr("Failed to move files from %1 to %2.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())); return false; } @@ -599,7 +598,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component) moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); } else { qDebug() << "Could not find ini file!"; - emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %0.").arg(iniPath)); + emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %1.").arg(iniPath)); return false; } @@ -639,7 +638,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component) } } - emit textChanged(tr("%0 installation finished!").arg(name)); + emit textChanged(tr("%1 installation finished!").arg(name)); return true; } @@ -710,11 +709,13 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil for (size_t j=group->first_file; j<=group->last_file; ++j) { - QString current(QString::fromUtf8(unshield_file_name(unshield, j))); - qDebug() << "File is: " << current; - if (current == fileName) - return true; // File is found! + if (unshield_file_is_valid(unshield, j)) { + QString current(QString::fromUtf8(unshield_file_name(unshield, j))); + + if (current.toLower() == fileName.toLower()) + return true; // File is found! + } } } diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 711599787..9a1557963 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -105,6 +105,10 @@ if(QT_QTGUI_LIBRARY AND QT_QTCORE_LIBRARY) launchersettings settingsbase ) + + add_component_qt_dir (process + processinvoker + ) include(${QT_USE_FILE}) QT4_WRAP_UI(ESM_UI_HDR ${ESM_UI}) diff --git a/components/config/gamesettings.hpp b/components/config/gamesettings.hpp index 41335707d..c4a6ead79 100644 --- a/components/config/gamesettings.hpp +++ b/components/config/gamesettings.hpp @@ -52,6 +52,8 @@ namespace Config } inline QStringList getDataDirs() { return mDataDirs; } + + inline void removeDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.removeAll(dir); } inline void addDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.append(dir); } inline QString getDataLocal() {return mDataLocal; } diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp new file mode 100644 index 000000000..e75daae9b --- /dev/null +++ b/components/process/processinvoker.cpp @@ -0,0 +1,109 @@ +#include "processinvoker.hpp" + +#include +#include +#include +#include +#include +#include +#include + +Process::ProcessInvoker::ProcessInvoker() +{ +} + +Process::ProcessInvoker::~ProcessInvoker() +{ +} + +bool Process::ProcessInvoker::startProcess(const QString &name, const QStringList &arguments, bool detached) +{ + QString path(name); +#ifdef Q_OS_WIN + path.append(QLatin1String(".exe")); +#elif defined(Q_OS_MAC) + QDir dir(QCoreApplication::applicationDirPath()); + path = dir.absoluteFilePath(name); +#else + path.prepend(QLatin1String("./")); +#endif + + QProcess process; + QFileInfo info(path); + + if (!info.exists()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not find %1

\ +

The application is not found.

\ +

Please make sure OpenMW is installed correctly and try again.

").arg(info.fileName())); + msgBox.exec(); + return false; + } + + if (!info.isExecutable()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not start %1

\ +

The application is not executable.

\ +

Please make sure you have the right permissions and try again.

").arg(info.fileName())); + msgBox.exec(); + return false; + } + + // Start the executable + if (detached) { + if (!process.startDetached(path, arguments)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not start %1

\ +

An error occurred while starting %1.

\ +

Press \"Show Details...\" for more information.

").arg(info.fileName())); + msgBox.setDetailedText(process.errorString()); + msgBox.exec(); + return false; + } + } else { + process.start(path, arguments); + if (!process.waitForFinished()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not start %1

\ +

An error occurred while starting %1.

\ +

Press \"Show Details...\" for more information.

").arg(info.fileName())); + msgBox.setDetailedText(process.errorString()); + msgBox.exec(); + + return false; + } + + if (process.exitCode() != 0 || process.exitStatus() == QProcess::CrashExit) { + QString error(process.readAllStandardError()); + error.append(tr("\nArguments:\n")); + error.append(arguments.join(" ")); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error running executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Executable %1 returned an error

\ +

An error occurred while running %1.

\ +

Press \"Show Details...\" for more information.

").arg(info.fileName())); + msgBox.setDetailedText(error); + msgBox.exec(); + + return false; + } + } + + return true; + +} diff --git a/components/process/processinvoker.hpp b/components/process/processinvoker.hpp new file mode 100644 index 000000000..d59d2f012 --- /dev/null +++ b/components/process/processinvoker.hpp @@ -0,0 +1,23 @@ +#ifndef PROCESSINVOKER_HPP +#define PROCESSINVOKER_HPP + +#include +#include + +namespace Process +{ + class ProcessInvoker : public QObject + { + Q_OBJECT + + ProcessInvoker(); + ~ProcessInvoker(); + + public: + + inline static bool startProcess(const QString &name, bool detached = false) { return startProcess(name, QStringList(), detached); } + bool static startProcess(const QString &name, const QStringList &arguments, bool detached = false); + }; +} + +#endif // PROCESSINVOKER_HPP diff --git a/files/ui/wizard/importpage.ui b/files/ui/wizard/importpage.ui index 5b078efca..52f9ac4f2 100644 --- a/files/ui/wizard/importpage.ui +++ b/files/ui/wizard/importpage.ui @@ -6,8 +6,8 @@ 0 0 - 511 - 326 + 510 + 324 @@ -43,7 +43,7 @@ - Import add-on and plugin selection (creates a new Content List in the launcher) + Import add-on and plugin selection From d30c4edb14f789b9a5c3c69d95612a3bbe49d8ef Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 25 Feb 2014 17:23:26 +0100 Subject: [PATCH 34/69] Support separate Splash directory, found in some installation media cabinets --- apps/wizard/unshield/unshieldworker.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 64d15e92a..ea09650f6 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -587,6 +587,14 @@ bool Wizard::UnshieldWorker::installComponent(Component component) if (component == Wizard::Component_Morrowind) { + // Some installations have a separate Splash directory + QFileInfo splash(temp.absoluteFilePath(QLatin1String("Splash"))); + + if (splash.exists()) { + emit textChanged(tr("Extracting: Splash directory")); + copyDirectory(splash.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Splash")); + } + // Copy Morrowind configuration file QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); From 6b9082c6ba1098f95c96c76875cb112982a7d15e Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 25 Feb 2014 17:36:55 +0100 Subject: [PATCH 35/69] Support different Tribunal patch .esm location, as seen on German GoTY disk --- apps/wizard/unshield/unshieldworker.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index ea09650f6..617049daf 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -630,6 +630,10 @@ bool Wizard::UnshieldWorker::installComponent(Component component) QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); QFileInfo original(getPath() + QDir::separator() + QLatin1String("Tribunal.esm")); + // Look for the patch in other places too, it's not always in "Tribunal Patch" + if (!patch.exists()) + patch = QFileInfo(temp.absoluteFilePath(QLatin1String("Tribunal") + QDir::separator() + QLatin1String("Tribunal.esm"))); + if (original.exists() && patch.exists()) { emit textChanged(tr("Extracting: Tribunal patch")); copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); From 05f8af07576ae565969379f2074c5d732cf8b596 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 25 Feb 2014 18:15:29 +0100 Subject: [PATCH 36/69] Use the correct encoding for the ini importer --- apps/wizard/mainwizard.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index fced2d39e..8b95ecfa1 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -152,11 +152,11 @@ void Wizard::MainWizard::runSettingsImporter() QString language(field(QLatin1String("installation.language")).toString()); if (language == QLatin1String("Polish")) { - arguments.append(QLatin1String("windows-1250")); + arguments.append(QLatin1String("win1250")); } else if (language == QLatin1String("Russian")) { - arguments.append(QLatin1String("windows-1251")); + arguments.append(QLatin1String("win1251")); } else { - arguments.append(QLatin1String("windows-1252")); + arguments.append(QLatin1String("win1252")); } // Now the paths From b2156351d8852fc32ac6f83404651befa333fb33 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 16 Mar 2014 20:07:54 +0100 Subject: [PATCH 37/69] Work in progress commit, working on data1.hdr autodetection --- apps/wizard/existinginstallationpage.cpp | 1 - apps/wizard/inisettings.cpp | 2 +- apps/wizard/installationpage.cpp | 2 + apps/wizard/installationpage.hpp | 1 - apps/wizard/mainwizard.cpp | 13 + apps/wizard/mainwizard.hpp | 1 + apps/wizard/unshield/unshieldworker.cpp | 475 +++++++++++++++-------- apps/wizard/unshield/unshieldworker.hpp | 12 +- 8 files changed, 328 insertions(+), 179 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index f0f5c8573..91d4f2df1 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -61,7 +61,6 @@ void Wizard::ExistingInstallationPage::textChanged(const QString &text) void Wizard::ExistingInstallationPage::initializePage() { - QStringList paths(mWizard->mInstallations.keys()); if (paths.isEmpty()) diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index 8c733ce3b..f67da0455 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -66,7 +66,7 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) if (!currentSection.isEmpty()) key = currentSection + QLatin1Char('/') + key; - qDebug() << "adding: " << key << value; + //qDebug() << "adding: " << key << value; mSettings[key] = QVariant(value); } } diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index f104c8195..33ceadf8c 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -44,6 +44,8 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setMaximum(installProgressBar->maximum() + 100); } + // Handle when cancel is clicked + startInstallation(); } diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 7001b2630..5e58477be 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -42,7 +42,6 @@ namespace Wizard protected: void initializePage(); - }; } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 8b95ecfa1..e6dfc67d1 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -224,6 +224,19 @@ void Wizard::MainWizard::accept() QWizard::accept(); } +void Wizard::MainWizard::reject() +{ + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Quit Wizard")); + msgBox.setIcon(QMessageBox::Question); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setText(tr("Are you sure you want to exit the Wizard?")); + + if (msgBox.exec() == QMessageBox::Yes) { + QWizard::reject(); + } +} + void Wizard::MainWizard::writeSettings() { QString path(field(QLatin1String("installation.path")).toString()); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 7520ba637..962571ac8 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -59,6 +59,7 @@ namespace Wizard private slots: void accept(); + void reject(); }; diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 617049daf..c23738943 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -319,144 +319,171 @@ bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString return copyDirectory(source, destination, false); } -void Wizard::UnshieldWorker::installDirectories(const QString &source) +bool Wizard::UnshieldWorker::installFile(const QString &fileName, const QString &path) { - QDir dir(source); + qDebug() << "Attempting to find file: " << fileName << " in: " << path; - if (!dir.exists()) - return; + bool result = true; + QDir dir(path); - QStringList directories; - directories << QLatin1String("Fonts") - << QLatin1String("Music") - << QLatin1String("Sound") - << QLatin1String("Splash") - << QLatin1String("Video"); + if (!dir.exists()) + return false; QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | - QDir::System | QDir::Hidden | - QDir::AllDirs)); + QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, QDir::DirsFirst)); foreach(QFileInfo info, list) { - if (info.isSymLink()) - continue; + if (info.isDir()) { + result = installFile(fileName, info.absoluteFilePath()); + } else { + if (info.fileName() == fileName) { + qDebug() << "File found at: " << info.absoluteFilePath(); - if (directories.contains(info.fileName())) { - qDebug() << "found " << info.fileName(); - emit textChanged(tr("Extracting: %1 directory").arg(info.fileName())); - copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); + emit textChanged(tr("Installing: %1").arg(info.fileName())); + return moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); + } } } - // Copy the Data Files dir too, but only the subdirectories - QFileInfo info(dir.absoluteFilePath("Data Files")); - if (info.exists()) { - emit textChanged(tr("Extracting: Data Files directory")); - copyDirectory(info.absoluteFilePath(), getPath()); - } - + return result; } -void Wizard::UnshieldWorker::extract() +bool Wizard::UnshieldWorker::installDirectory(const QString &dirName, const QString &path, bool recursive) { - qDebug() << "extract!"; - QDir disk; - - if (getInstallComponent(Wizard::Component_Morrowind)) - { - while (!getComponentDone(Wizard::Component_Morrowind)) - { - if (getComponentPath(Wizard::Component_Morrowind).isEmpty()) { - qDebug() << "request file dialog"; - QReadLocker readLock(&mLock); - emit requestFileDialog(Wizard::Component_Morrowind); - mWait.wait(&mLock); - } + qDebug() << "Attempting to find: " << dirName << " in: " << path; + bool result = true; + QDir dir(path); - if (!getComponentPath(Wizard::Component_Morrowind).isEmpty()) { - disk.setPath(getComponentPath(Wizard::Component_Morrowind)); + if (!dir.exists()) + return false; - if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa")) - | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa")) - | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) - { - QReadLocker readLock(&mLock); - emit requestFileDialog(Wizard::Component_Morrowind); - mWait.wait(&mLock); - } else { - if (installComponent(Wizard::Component_Morrowind)) { - setComponentDone(Wizard::Component_Morrowind, true); - } else { - qDebug() << "Erorr installing Morrowind"; + QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs)); + foreach(QFileInfo info, list) { + if (info.isSymLink()) + continue; - return; - } - } + if (info.isDir()) { + if (info.fileName() == dirName) { + qDebug() << "Directory found at: " << info.absoluteFilePath(); + emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); + return copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); + } else { + if (recursive) + result = installDirectory(dirName, info.absoluteFilePath()); } } } - if (getInstallComponent(Wizard::Component_Tribunal)) - { - setupAddon(Wizard::Component_Tribunal); - } - - if (getInstallComponent(Wizard::Component_Bloodmoon)) - { - setupAddon(Wizard::Component_Bloodmoon); - } - - // Update Morrowind configuration - if (getInstallComponent(Wizard::Component_Tribunal)) - { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); - } - - if (getInstallComponent(Wizard::Component_Bloodmoon)) - { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); - } - - if (getInstallComponent(Wizard::Component_Tribunal) && - getInstallComponent(Wizard::Component_Bloodmoon)) - { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); - mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); - } - - - // Write the settings to the Morrowind config file - writeSettings(); - - // Remove the temporary directory - removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); - - // Fill the progress bar - int total = 0; - - if (getInstallComponent(Wizard::Component_Morrowind)) - total = 100; - - if (getInstallComponent(Wizard::Component_Tribunal)) - total = total + 100; - - if (getInstallComponent(Wizard::Component_Bloodmoon)) - total = total + 100; + return result; +} - emit textChanged(tr("Installation finished!")); - emit progressChanged(total); - emit finished(); +void Wizard::UnshieldWorker::extract() +{ + qDebug() << "extract!"; - qDebug() << "installation finished!"; + qDebug() << findFiles(QLatin1String("data1.hdr"), QLatin1String("/mnt/cdrom")); +// QDir disk; + +// if (getInstallComponent(Wizard::Component_Morrowind)) +// { +// if (!getComponentDone(Wizard::Component_Morrowind)) +// { +// if (getComponentPath(Wizard::Component_Morrowind).isEmpty()) { +// qDebug() << "request file dialog"; +// QReadLocker readLock(&mLock); +// emit requestFileDialog(Wizard::Component_Morrowind); +// mWait.wait(&mLock); +// } + +// if (!getComponentPath(Wizard::Component_Morrowind).isEmpty()) { +// disk.setPath(getComponentPath(Wizard::Component_Morrowind)); + +// if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa"))) +// { +// QReadLocker readLock(&mLock); +// emit requestFileDialog(Wizard::Component_Morrowind); +// mWait.wait(&mLock); +// } else { +// if (installComponent(Wizard::Component_Morrowind)) { +// setComponentDone(Wizard::Component_Morrowind, true); +// } else { +// qDebug() << "Erorr installing Morrowind"; + +// return; +// } +// } +// } +// } +// } + +// if (getInstallComponent(Wizard::Component_Tribunal)) +// { +// setupAddon(Wizard::Component_Tribunal); +// } + +// if (getInstallComponent(Wizard::Component_Bloodmoon)) +// { +// setupAddon(Wizard::Component_Bloodmoon); +// } + +// // Update Morrowind configuration +// if (getInstallComponent(Wizard::Component_Tribunal)) +// { +// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); +// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); +// } + +// if (getInstallComponent(Wizard::Component_Bloodmoon)) +// { +// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); +// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); +// } + +// if (getInstallComponent(Wizard::Component_Tribunal) && +// getInstallComponent(Wizard::Component_Bloodmoon)) +// { +// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); +// mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); +// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); +// mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); +// } + + +// // Write the settings to the Morrowind config file +// writeSettings(); + +// // Remove the temporary directory +// //removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); + +// // Fill the progress bar +// int total = 0; + +// if (getInstallComponent(Wizard::Component_Morrowind)) +// total = 100; + +// if (getInstallComponent(Wizard::Component_Tribunal)) +// total = total + 100; + +// if (getInstallComponent(Wizard::Component_Bloodmoon)) +// total = total + 100; + +// emit textChanged(tr("Installation finished!")); +// emit progressChanged(total); +// emit finished(); + +// qDebug() << "installation finished!"; } void Wizard::UnshieldWorker::setupAddon(Component component) { - while (!getComponentDone(component)) + qDebug() << "SetupAddon!" << getComponentPath(component) << getComponentPath(Wizard::Component_Morrowind); + + if (!getComponentDone(component)) { + qDebug() << "Component not done!"; + QDir disk(getComponentPath(Wizard::Component_Morrowind)); QString name; if (component == Wizard::Component_Tribunal) @@ -470,43 +497,72 @@ void Wizard::UnshieldWorker::setupAddon(Component component) return; } - if (!disk.cd(name)) { - qDebug() << "not found on cd!"; - QReadLocker locker(&mLock); - emit requestFileDialog(component); - mWait.wait(&mLock); + qDebug() << "Determine if file is in current data1.hdr: " << name; - } else if (disk.exists(QLatin1String("data1.hdr"))) { - setComponentPath(component, disk.absolutePath()); - } + if (!disk.isEmpty()) { + if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) + { + if (!disk.cd(name)) { + qDebug() << "not found on cd!"; + QReadLocker locker(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + + } else if (disk.exists(QLatin1String("data1.hdr"))) { + if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) + { + QReadLocker locker(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + } else { + setComponentPath(component, disk.absolutePath()); + disk.setPath(getComponentPath(component)); + } + } + } - if (getComponentPath(component).isEmpty()) { - qDebug() << "request file dialog"; + } else { QReadLocker locker(&mLock); - emit requestFileDialog(Wizard::Component_Tribunal); + emit requestFileDialog(component); mWait.wait(&mLock); } - // Make sure the dir is up-to-date disk.setPath(getComponentPath(component)); - if (!getComponentPath(component).isEmpty()) { - - if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) - { + if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) + { + if (!disk.cd(name)) { + qDebug() << "not found on cd!"; QReadLocker locker(&mLock); emit requestFileDialog(component); mWait.wait(&mLock); - } else { - // Now do the actual installing - if (installComponent(component)) { - setComponentDone(component, true); + + } else if (disk.exists(QLatin1String("data1.hdr"))) { + if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) + { + QReadLocker locker(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); } else { - qDebug() << "Error installing " << name; - return; + setComponentPath(component, disk.absolutePath()); + disk.setPath(getComponentPath(component)); } } + + // Make sure the dir is up-to-date + //disk.setPath(getComponentPath(component)); + } + + // Now do the actual installing + + if (installComponent(component)) { + setComponentDone(component, true); + } else { + qDebug() << "Error installing " << name; + return; } + + } } @@ -573,40 +629,60 @@ bool Wizard::UnshieldWorker::installComponent(Component component) return false; // Move the files from the temporary path to the destination folder +// emit textChanged(tr("Moving installation files")); +// if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { +// qDebug() << "failed to move files!"; +// emit error(tr("Moving extracted files failed!"), +// tr("Failed to move files from %1 to %2.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), +// getPath())); +// return false; +// } emit textChanged(tr("Moving installation files")); - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { - qDebug() << "failed to move files!"; - emit error(tr("Moving extracted files failed!"), - tr("Failed to move files from %1 to %2.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), - getPath())); - return false; + + // Install extracted directories + QStringList directories; + directories << QLatin1String("BookArt") + << QLatin1String("Fonts") + << QLatin1String("Icons") + << QLatin1String("Meshes") + << QLatin1String("Music") + << QLatin1String("Sound") + << QLatin1String("Splash") + << QLatin1String("Textures") + << QLatin1String("Video"); + + foreach (const QString &dir, directories) { + installDirectory(dir, temp.absolutePath()); } - // Install files outside of cab archives - installDirectories(disk.absolutePath()); + // Install directories from disk + foreach (const QString &dir, directories) { + qDebug() << "\n\nDISK DIRS!"; + installDirectory(dir, disk.absolutePath(), false); + } + + QFileInfo info(disk.absoluteFilePath("Data Files")); + if (info.exists()) { + emit textChanged(tr("Installing: Data Files directory")); + copyDirectory(info.absoluteFilePath(), getPath()); + } if (component == Wizard::Component_Morrowind) { - // Some installations have a separate Splash directory - QFileInfo splash(temp.absoluteFilePath(QLatin1String("Splash"))); - - if (splash.exists()) { - emit textChanged(tr("Extracting: Splash directory")); - copyDirectory(splash.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Splash")); + QStringList files; + files << QLatin1String("Morrowind.esm") + << QLatin1String("Morrowin.bsa"); + + foreach (const QString &file, files) { + if (!installFile(file, temp.absolutePath())) { + emit error(tr("Could not find Morrowind data file!"), tr("Failed to find %1.").arg(file)); + return false; + } } // Copy Morrowind configuration file - QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); - iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); - - QFileInfo info(iniPath); - - if (info.exists()) { - emit textChanged(tr("Extracting: Morrowind.ini")); - moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); - } else { - qDebug() << "Could not find ini file!"; - emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %1.").arg(iniPath)); + if (!installFile(QLatin1String("Morrowind.ini"), temp.absolutePath())) { + emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %1.").arg(QLatin1String("Morrowind.ini"))); return false; } @@ -620,23 +696,43 @@ bool Wizard::UnshieldWorker::installComponent(Component component) QFileInfo sounds(temp.absoluteFilePath(QLatin1String("Sounds"))); if (sounds.exists()) { - emit textChanged(tr("Extracting: Sound directory")); + emit textChanged(tr("Installing: Sound directory")); copyDirectory(sounds.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Sound")); } + + QStringList files; + files << QLatin1String("Tribunal.esm") + << QLatin1String("Tribunal.bsa"); + + foreach (const QString &file, files) { + if (!installFile(file, temp.absolutePath())) { + emit error(tr("Could not find Tribunal data file!"), tr("Failed to find %1.").arg(file)); + return false; + } + } + } if (component == Wizard::Component_Bloodmoon) { - QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); QFileInfo original(getPath() + QDir::separator() + QLatin1String("Tribunal.esm")); - // Look for the patch in other places too, it's not always in "Tribunal Patch" - if (!patch.exists()) - patch = QFileInfo(temp.absoluteFilePath(QLatin1String("Tribunal") + QDir::separator() + QLatin1String("Tribunal.esm"))); + if (original.exists()) { + if (!installFile(QLatin1String("Tribunal.esm"), temp.absolutePath())) { + emit error(tr("Could not find Tribunal patch file!"), tr("Failed to find %1.").arg(QLatin1String("Tribunal.esm"))); + return false; + } + } - if (original.exists() && patch.exists()) { - emit textChanged(tr("Extracting: Tribunal patch")); - copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); + QStringList files; + files << QLatin1String("Bloodmoon.esm") + << QLatin1String("Bloodmoon.bsa"); + + foreach (const QString &file, files) { + if (!installFile(file, temp.absolutePath())) { + emit error(tr("Could not find Bloodmoon data file!"), tr("Failed to find %1.").arg(file)); + return false; + } } // Load Morrowind configuration settings from the setup script @@ -655,10 +751,10 @@ bool Wizard::UnshieldWorker::installComponent(Component component) } -bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) +bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter) { bool success; - QString path(outputDir); + QString path(destination); path.append(QDir::separator()); int directory = unshield_file_directory(unshield, index); @@ -703,7 +799,42 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp return success; } -bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fileName) +QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path) +{ + return findFiles(fileName, path).first(); +} + +QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QString &path) +{ + QStringList result; + QDir dir(source); + + if (!dir.exists()) + return QStringList(); + + QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, QDir::DirsFirst)); + foreach(QFileInfo info, list) { + + if (info.isSymLink()) + continue; + + if (info.isDir()) { + result = findFiles(file, info.absoluteFilePath()); + } else { + if (info.fileName() == fileName) { + qDebug() << "File found at: " << info.absoluteFilePath(); + result.append(info.absoluteFilePath()); + } + } + } + + return result; + +} + +bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fileName) { QByteArray array(cabFile.toUtf8()); @@ -724,7 +855,7 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil if (unshield_file_is_valid(unshield, j)) { QString current(QString::fromUtf8(unshield_file_name(unshield, j))); - + qDebug() << "Current is: " << current; if (current.toLower() == fileName.toLower()) return true; // File is found! } @@ -735,7 +866,7 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil return false; } -bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &outputDir) +bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &destination) { bool success; @@ -758,7 +889,7 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &o for (size_t j=group->first_file; j<=group->last_file; ++j) { if (unshield_file_is_valid(unshield, j)) { - success = extractFile(unshield, outputDir, group->name, j, counter); + success = extractFile(unshield, destination, group->name, j, counter); ++counter; } } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 8aa54c9c0..74ae6c6d8 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -62,11 +62,15 @@ namespace Wizard bool moveFile(const QString &source, const QString &destination); bool moveDirectory(const QString &source, const QString &destination); - bool extractCab(const QString &cabFile, const QString &outputDir); - bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); - bool findFile(const QString &cabFile, const QString &fileName); + bool extractCab(const QString &cabFile, const QString &destination); + bool extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter); + bool findInCab(const QString &cabFile, const QString &fileName); - void installDirectories(const QString &source); + QString findFile(const QString &fileName, const QString &path); + QStringList findFiles(const QString &fileName, const QString &path); + + bool installFile(const QString &fileName, const QString &path); + bool installDirectory(const QString &dirName, const QString &path, bool recursive = true); bool installMorrowind(); bool installTribunal(); From ef16b461483284cb5044cfc2348d2cd93f48339e Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 16 Mar 2014 22:09:20 +0100 Subject: [PATCH 38/69] Wizard now autodetects correct installation archive --- apps/wizard/installationpage.cpp | 20 +- apps/wizard/unshield/unshieldworker.cpp | 405 ++++++++++-------------- apps/wizard/unshield/unshieldworker.hpp | 23 +- 3 files changed, 186 insertions(+), 262 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 33ceadf8c..3fcbd1fe2 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -148,13 +148,18 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) break; } - QString fileName = QFileDialog::getOpenFileName( - this, - tr("Select %1 installation file").arg(name), - QDir::rootPath(), - tr("InstallShield header files (*.hdr)")); +// QString fileName = QFileDialog::getOpenFileName( +// this, +// tr("Select %1 installation file").arg(name), +// QDir::rootPath(), +// tr("InstallShield header files (*.hdr)")); - if (fileName.isEmpty()) { + + QString path = QFileDialog::getExistingDirectory(this, + tr("Select %1 installation media").arg(name), + QDir::rootPath()); + + if (path.isEmpty()) { qDebug() << "Cancel was clicked!"; logTextEdit->appendHtml(tr("


\ @@ -164,8 +169,7 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) return; } - QFileInfo info(fileName); - mUnshield->setComponentPath(component, info.absolutePath()); + mUnshield->setDiskPath(path); } void Wizard::InstallationPage::installationFinished() diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index c23738943..223de41ce 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -20,12 +20,9 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : { unshield_set_log_level(0); - mMorrowindPath = QString(); - mTribunalPath = QString(); - mBloodmoonPath = QString(); - mPath = QString(); mIniPath = QString(); + mDiskPath = QString(); // Default to Latin encoding mIniCodec = QTextCodec::codecForName("windows-1252"); @@ -78,41 +75,6 @@ bool Wizard::UnshieldWorker::getInstallComponent(Component component) return false; } -void Wizard::UnshieldWorker::setComponentPath(Wizard::Component component, const QString &path) -{ - QWriteLocker writeLock(&mLock); - switch (component) { - - case Wizard::Component_Morrowind: - mMorrowindPath = path; - break; - case Wizard::Component_Tribunal: - mTribunalPath = path; - break; - case Wizard::Component_Bloodmoon: - mBloodmoonPath = path; - break; - } - - mWait.wakeAll(); -} - -QString Wizard::UnshieldWorker::getComponentPath(Component component) -{ - QReadLocker readLock(&mLock); - switch (component) { - - case Wizard::Component_Morrowind: - return mMorrowindPath; - case Wizard::Component_Tribunal: - return mTribunalPath; - case Wizard::Component_Bloodmoon: - return mBloodmoonPath; - } - - return QString(); -} - void Wizard::UnshieldWorker::setComponentDone(Component component, bool done) { QWriteLocker writeLock(&mLock); @@ -159,6 +121,13 @@ void Wizard::UnshieldWorker::setIniPath(const QString &path) mIniPath = path; } +void Wizard::UnshieldWorker::setDiskPath(const QString &path) +{ + QWriteLocker writeLock(&mLock); + mDiskPath = path; + mWait.wakeAll(); +} + QString Wizard::UnshieldWorker::getPath() { QReadLocker readLock(&mLock); @@ -171,6 +140,13 @@ QString Wizard::UnshieldWorker::getIniPath() return mIniPath; } +QString Wizard::UnshieldWorker::getDiskPath() +{ + QReadLocker readLock(&mLock); + return mDiskPath; +} + + void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) { QWriteLocker writeLock(&mLock); @@ -332,7 +308,7 @@ bool Wizard::UnshieldWorker::installFile(const QString &fileName, const QString QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)); - foreach(QFileInfo info, list) { + foreach(const QFileInfo &info, list) { if (info.isDir()) { result = installFile(fileName, info.absoluteFilePath()); } else { @@ -360,7 +336,7 @@ bool Wizard::UnshieldWorker::installDirectory(const QString &dirName, const QStr QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs)); - foreach(QFileInfo info, list) { + foreach(const QFileInfo &info, list) { if (info.isSymLink()) continue; @@ -383,190 +359,139 @@ void Wizard::UnshieldWorker::extract() { qDebug() << "extract!"; - qDebug() << findFiles(QLatin1String("data1.hdr"), QLatin1String("/mnt/cdrom")); -// QDir disk; - -// if (getInstallComponent(Wizard::Component_Morrowind)) -// { -// if (!getComponentDone(Wizard::Component_Morrowind)) -// { -// if (getComponentPath(Wizard::Component_Morrowind).isEmpty()) { -// qDebug() << "request file dialog"; -// QReadLocker readLock(&mLock); -// emit requestFileDialog(Wizard::Component_Morrowind); -// mWait.wait(&mLock); -// } - -// if (!getComponentPath(Wizard::Component_Morrowind).isEmpty()) { -// disk.setPath(getComponentPath(Wizard::Component_Morrowind)); - -// if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa"))) -// { -// QReadLocker readLock(&mLock); -// emit requestFileDialog(Wizard::Component_Morrowind); -// mWait.wait(&mLock); -// } else { -// if (installComponent(Wizard::Component_Morrowind)) { -// setComponentDone(Wizard::Component_Morrowind, true); -// } else { -// qDebug() << "Erorr installing Morrowind"; - -// return; -// } -// } -// } -// } -// } - -// if (getInstallComponent(Wizard::Component_Tribunal)) -// { -// setupAddon(Wizard::Component_Tribunal); -// } - -// if (getInstallComponent(Wizard::Component_Bloodmoon)) -// { -// setupAddon(Wizard::Component_Bloodmoon); -// } - -// // Update Morrowind configuration -// if (getInstallComponent(Wizard::Component_Tribunal)) -// { -// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); -// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); -// } - -// if (getInstallComponent(Wizard::Component_Bloodmoon)) -// { -// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); -// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); -// } - -// if (getInstallComponent(Wizard::Component_Tribunal) && -// getInstallComponent(Wizard::Component_Bloodmoon)) -// { -// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); -// mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); -// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); -// mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); -// } - - -// // Write the settings to the Morrowind config file -// writeSettings(); - -// // Remove the temporary directory -// //removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); - -// // Fill the progress bar -// int total = 0; - -// if (getInstallComponent(Wizard::Component_Morrowind)) -// total = 100; - -// if (getInstallComponent(Wizard::Component_Tribunal)) -// total = total + 100; - -// if (getInstallComponent(Wizard::Component_Bloodmoon)) -// total = total + 100; - -// emit textChanged(tr("Installation finished!")); -// emit progressChanged(total); -// emit finished(); - -// qDebug() << "installation finished!"; -} + if (getInstallComponent(Wizard::Component_Morrowind)) + { + if (!getComponentDone(Wizard::Component_Morrowind)) + if (!setupComponent(Wizard::Component_Morrowind)) + return; + } -void Wizard::UnshieldWorker::setupAddon(Component component) -{ - qDebug() << "SetupAddon!" << getComponentPath(component) << getComponentPath(Wizard::Component_Morrowind); + if (getInstallComponent(Wizard::Component_Tribunal)) + { + if (!getComponentDone(Wizard::Component_Tribunal)) + if (!setupComponent(Wizard::Component_Tribunal)) + return; + } + + if (getInstallComponent(Wizard::Component_Bloodmoon)) + { + if (!getComponentDone(Wizard::Component_Bloodmoon)) + if (!setupComponent(Wizard::Component_Bloodmoon)) + return; + } + + // Update Morrowind configuration + if (getInstallComponent(Wizard::Component_Tribunal)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); + } - if (!getComponentDone(component)) + if (getInstallComponent(Wizard::Component_Bloodmoon)) { - qDebug() << "Component not done!"; + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); + } - QDir disk(getComponentPath(Wizard::Component_Morrowind)); - QString name; - if (component == Wizard::Component_Tribunal) - name = QLatin1String("Tribunal"); + if (getInstallComponent(Wizard::Component_Tribunal) && + getInstallComponent(Wizard::Component_Bloodmoon)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); + } - if (component == Wizard::Component_Bloodmoon) - name = QLatin1String("Bloodmoon"); + // Write the settings to the Morrowind config file + writeSettings(); - if (name.isEmpty()) { - emit error(tr("Component parameter is invalid!"), tr("An invalid component parameter was supplied.")); - return; - } + // Remove the temporary directory + removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); - qDebug() << "Determine if file is in current data1.hdr: " << name; - - if (!disk.isEmpty()) { - if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) - { - if (!disk.cd(name)) { - qDebug() << "not found on cd!"; - QReadLocker locker(&mLock); - emit requestFileDialog(component); - mWait.wait(&mLock); - - } else if (disk.exists(QLatin1String("data1.hdr"))) { - if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) - { - QReadLocker locker(&mLock); - emit requestFileDialog(component); - mWait.wait(&mLock); - } else { - setComponentPath(component, disk.absolutePath()); - disk.setPath(getComponentPath(component)); - } - } - } + // Fill the progress bar + int total = 0; - } else { - QReadLocker locker(&mLock); + if (getInstallComponent(Wizard::Component_Morrowind)) + total = 100; + + if (getInstallComponent(Wizard::Component_Tribunal)) + total = total + 100; + + if (getInstallComponent(Wizard::Component_Bloodmoon)) + total = total + 100; + + emit textChanged(tr("Installation finished!")); + emit progressChanged(total); + emit finished(); + + qDebug() << "installation finished!"; +} + +bool Wizard::UnshieldWorker::setupComponent(Component component) +{ + QString name; + switch (component) { + + case Wizard::Component_Morrowind: + name = QLatin1String("Morrowind"); + break; + case Wizard::Component_Tribunal: + name = QLatin1String("Tribunal"); + break; + case Wizard::Component_Bloodmoon: + name = QLatin1String("Bloodmoon"); + break; + } + + if (name.isEmpty()) { + emit error(tr("Component parameter is invalid!"), tr("An invalid component parameter was supplied.")); + return false; + } + + bool found = false; + QString cabFile; + QDir disk; + + // Keep showing the file dialog until we find the necessary install files + while (!found) { + if (getDiskPath().isEmpty()) { + QReadLocker readLock(&mLock); emit requestFileDialog(component); mWait.wait(&mLock); + disk.setPath(getDiskPath()); + } else { + disk.setPath(getDiskPath()); } - disk.setPath(getComponentPath(component)); + QStringList list(findFiles(QLatin1String("data1.hdr"), disk.absolutePath())); - if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) - { - if (!disk.cd(name)) { - qDebug() << "not found on cd!"; - QReadLocker locker(&mLock); - emit requestFileDialog(component); - mWait.wait(&mLock); - - } else if (disk.exists(QLatin1String("data1.hdr"))) { - if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) - { - QReadLocker locker(&mLock); - emit requestFileDialog(component); - mWait.wait(&mLock); - } else { - setComponentPath(component, disk.absolutePath()); - disk.setPath(getComponentPath(component)); - } + foreach (const QString &file, list) { + qDebug() << "current cab file is: " << file; + if (findInCab(file, name + QLatin1String(".bsa"))) { + cabFile = file; + found = true; } - - // Make sure the dir is up-to-date - //disk.setPath(getComponentPath(component)); } - // Now do the actual installing - - if (installComponent(component)) { - setComponentDone(component, true); - } else { - qDebug() << "Error installing " << name; - return; + if (!found) { + QReadLocker readLock(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); } + } - + if (installComponent(component, cabFile)) { + setComponentDone(component, true); + return true; + } else { + qDebug() << "Erorr installing " << name; + return false; } + + return true; } -bool Wizard::UnshieldWorker::installComponent(Component component) +bool Wizard::UnshieldWorker::installComponent(Component component, const QString &path) { QString name; switch (component) { @@ -587,13 +512,15 @@ bool Wizard::UnshieldWorker::installComponent(Component component) return false; } + qDebug() << "Install " << name << " from " << path; + + emit textChanged(tr("Installing %1").arg(name)); - QDir disk(getComponentPath(component)); + QFileInfo info(path); - if (!disk.exists()) { - qDebug() << "Component path not set: " << getComponentPath(Wizard::Component_Morrowind); - emit error(tr("Component path not set!"), tr("The source path for %1 was not set.").arg(name)); + if (!info.exists()) { + emit error(tr("Installation media path not set!"), tr("The source path for %1 was not set.").arg(name)); return false; } @@ -606,7 +533,6 @@ bool Wizard::UnshieldWorker::installComponent(Component component) removeDirectory(tempPath); if (!temp.mkpath(tempPath)) { - qDebug() << "Can't make path"; emit error(tr("Cannot create temporary directory!"), tr("Failed to create %1.").arg(tempPath)); return false; } @@ -619,24 +545,15 @@ bool Wizard::UnshieldWorker::installComponent(Component component) } if (!temp.cd(name)) { - qDebug() << "Can't cd to dir"; emit error(tr("Cannot move into temporary directory!"), tr("Failed to move into %1.").arg(temp.absoluteFilePath(name))); return false; } // Extract the installation files - if (!extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath())) + if (!extractCab(info.absoluteFilePath(), temp.absolutePath())) return false; // Move the files from the temporary path to the destination folder -// emit textChanged(tr("Moving installation files")); -// if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { -// qDebug() << "failed to move files!"; -// emit error(tr("Moving extracted files failed!"), -// tr("Failed to move files from %1 to %2.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), -// getPath())); -// return false; -// } emit textChanged(tr("Moving installation files")); // Install extracted directories @@ -658,20 +575,20 @@ bool Wizard::UnshieldWorker::installComponent(Component component) // Install directories from disk foreach (const QString &dir, directories) { qDebug() << "\n\nDISK DIRS!"; - installDirectory(dir, disk.absolutePath(), false); + installDirectory(dir, info.absolutePath(), false); } - QFileInfo info(disk.absoluteFilePath("Data Files")); - if (info.exists()) { + QFileInfo datafiles(info.absolutePath() + QDir::separator() + QLatin1String("Data Files")); + if (datafiles.exists()) { emit textChanged(tr("Installing: Data Files directory")); - copyDirectory(info.absoluteFilePath(), getPath()); + copyDirectory(datafiles.absoluteFilePath(), getPath()); } if (component == Wizard::Component_Morrowind) { QStringList files; files << QLatin1String("Morrowind.esm") - << QLatin1String("Morrowin.bsa"); + << QLatin1String("Morrowind.bsa"); foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { @@ -702,7 +619,9 @@ bool Wizard::UnshieldWorker::installComponent(Component component) QStringList files; files << QLatin1String("Tribunal.esm") - << QLatin1String("Tribunal.bsa"); + << QLatin1String("Tribunal.bsa") + << QLatin1String("Morrowind.esm") + << QLatin1String("Morrowind.bsa"); foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { @@ -710,7 +629,6 @@ bool Wizard::UnshieldWorker::installComponent(Component component) return false; } } - } if (component == Wizard::Component_Bloodmoon) @@ -726,7 +644,9 @@ bool Wizard::UnshieldWorker::installComponent(Component component) QStringList files; files << QLatin1String("Bloodmoon.esm") - << QLatin1String("Bloodmoon.bsa"); + << QLatin1String("Bloodmoon.bsa") + << QLatin1String("Morrowind.esm") + << QLatin1String("Morrowind.bsa"); foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { @@ -736,13 +656,12 @@ bool Wizard::UnshieldWorker::installComponent(Component component) } // Load Morrowind configuration settings from the setup script - QFileInfo inx(disk.absoluteFilePath(QLatin1String("setup.inx"))); + QStringList list(findFiles(QLatin1String("setup.inx"), getDiskPath())); - if (inx.exists()) { - emit textChanged(tr("Updating Morrowind configuration file")); - mIniSettings.parseInx(inx.absoluteFilePath()); - } else { - qDebug() << "setup.inx not found!"; + emit textChanged(tr("Updating Morrowind configuration file")); + + foreach (const QString &inx, list) { + mIniSettings.parseInx(inx); } } @@ -799,15 +718,23 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &dest return success; } -QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path) +QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path, int depth) { - return findFiles(fileName, path).first(); + return findFiles(fileName, path, depth).first(); } -QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QString &path) +QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QString &path, int depth) { + qDebug() << "Searching path: " << path << " for: " << fileName; + static const int MAXIMUM_DEPTH = 5; + + if (depth >= MAXIMUM_DEPTH) { + qWarning("Maximum directory depth limit reached."); + return QStringList(); + } + QStringList result; - QDir dir(source); + QDir dir(path); if (!dir.exists()) return QStringList(); @@ -821,7 +748,7 @@ QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QSt continue; if (info.isDir()) { - result = findFiles(file, info.absoluteFilePath()); + result.append(findFiles(fileName, info.absoluteFilePath(), depth + 1)); } else { if (info.fileName() == fileName) { qDebug() << "File found at: " << info.absoluteFilePath(); @@ -831,7 +758,6 @@ QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QSt } return result; - } bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fileName) @@ -855,7 +781,6 @@ bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fi if (unshield_file_is_valid(unshield, j)) { QString current(QString::fromUtf8(unshield_file_name(unshield, j))); - qDebug() << "Current is: " << current; if (current.toLower() == fileName.toLower()) return true; // File is found! } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 74ae6c6d8..bda951456 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -31,7 +31,8 @@ namespace Wizard void setInstallComponent(Wizard::Component component, bool install); - void setComponentPath(Wizard::Component component, const QString &path); +// void setComponentPath(Wizard::Component component, const QString &path); + void setDiskPath(const QString &path); void setPath(const QString &path); void setIniPath(const QString &path); @@ -49,7 +50,8 @@ namespace Wizard bool getInstallComponent(Component component); - QString getComponentPath(Component component); + //QString getComponentPath(Component component); + QString getDiskPath(); void setComponentDone(Component component, bool done = true); bool getComponentDone(Component component); @@ -66,18 +68,14 @@ namespace Wizard bool extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter); bool findInCab(const QString &cabFile, const QString &fileName); - QString findFile(const QString &fileName, const QString &path); - QStringList findFiles(const QString &fileName, const QString &path); + QString findFile(const QString &fileName, const QString &path, int depth = 0); + QStringList findFiles(const QString &fileName, const QString &path, int depth = 0); bool installFile(const QString &fileName, const QString &path); bool installDirectory(const QString &dirName, const QString &path, bool recursive = true); - bool installMorrowind(); - bool installTribunal(); - bool installBloodmoon(); - - bool installComponent(Component component); - void setupAddon(Component component); + bool installComponent(Component component, const QString &path); + bool setupComponent(Component component); bool mInstallMorrowind; bool mInstallTribunal; @@ -87,12 +85,9 @@ namespace Wizard bool mTribunalDone; bool mBloodmoonDone; - QString mMorrowindPath; - QString mTribunalPath; - QString mBloodmoonPath; - QString mPath; QString mIniPath; + QString mDiskPath; IniSettings mIniSettings; From 254fe0a424edee364030970af3c4f4c8a8e19ee5 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 17 Mar 2014 14:31:05 +0100 Subject: [PATCH 39/69] Made install functions use file finding methods and improved error handling --- apps/wizard/conclusionpage.cpp | 2 +- apps/wizard/installationpage.cpp | 30 +-- apps/wizard/mainwizard.cpp | 3 +- apps/wizard/unshield/unshieldworker.cpp | 266 ++++++++++++++---------- apps/wizard/unshield/unshieldworker.hpp | 25 ++- 5 files changed, 184 insertions(+), 142 deletions(-) diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 3681459b7..7b638813b 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -22,7 +22,7 @@ void Wizard::ConclusionPage::initializePage() if (!mWizard->mError) { if ((field(QLatin1String("installation.new")).toBool() == true) - | (field(QLatin1String("installation.import-settings")).toBool() == true)) + || (field(QLatin1String("installation.import-settings")).toBool() == true)) { qDebug() << "IMPORT SETTINGS"; mWizard->runSettingsImporter(); diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 3fcbd1fe2..74666c394 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -44,8 +44,6 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setMaximum(installProgressBar->maximum() + 100); } - // Handle when cancel is clicked - startInstallation(); } @@ -54,21 +52,21 @@ void Wizard::InstallationPage::startInstallation() QStringList components(field(QLatin1String("installation.components")).toStringList()); QString path(field(QLatin1String("installation.path")).toString()); - QThread *thread = new QThread(); + mThread = new QThread(); mUnshield = new UnshieldWorker(); - mUnshield->moveToThread(thread); + mUnshield->moveToThread(mThread); - connect(thread, SIGNAL(started()), + connect(mThread, SIGNAL(started()), mUnshield, SLOT(extract())); connect(mUnshield, SIGNAL(finished()), - thread, SLOT(quit())); + mThread, SLOT(quit())); connect(mUnshield, SIGNAL(finished()), mUnshield, SLOT(deleteLater())); connect(mUnshield, SIGNAL(finished()), - thread, SLOT(deleteLater())); + mThread, SLOT(deleteLater()));; connect(mUnshield, SIGNAL(finished()), this, SLOT(installationFinished()), Qt::QueuedConnection); @@ -129,7 +127,7 @@ void Wizard::InstallationPage::startInstallation() mUnshield->setIniCodec(QTextCodec::codecForName("windows-1252")); } - thread->start(); + mThread->start(); } void Wizard::InstallationPage::showFileDialog(Wizard::Component component) @@ -148,23 +146,15 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) break; } -// QString fileName = QFileDialog::getOpenFileName( -// this, -// tr("Select %1 installation file").arg(name), -// QDir::rootPath(), -// tr("InstallShield header files (*.hdr)")); - - QString path = QFileDialog::getExistingDirectory(this, tr("Select %1 installation media").arg(name), QDir::rootPath()); if (path.isEmpty()) { - qDebug() << "Cancel was clicked!"; - logTextEdit->appendHtml(tr("


\ Error: The installation was aborted by the user

")); mWizard->mError = true; + emit completeChanged(); return; } @@ -174,8 +164,6 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) void Wizard::InstallationPage::installationFinished() { - qDebug() << "finished!"; - QMessageBox msgBox; msgBox.setWindowTitle(tr("Installation finished")); msgBox.setIcon(QMessageBox::Information); @@ -186,13 +174,10 @@ void Wizard::InstallationPage::installationFinished() mFinished = true; emit completeChanged(); - } void Wizard::InstallationPage::installationError(const QString &text, const QString &details) { - qDebug() << "error: " << text; - installProgressLabel->setText(tr("Installation failed!")); logTextEdit->appendHtml(tr("


\ @@ -213,7 +198,6 @@ void Wizard::InstallationPage::installationError(const QString &text, const QStr mWizard->mError = true; emit completeChanged(); - } bool Wizard::InstallationPage::isComplete() const diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index e6dfc67d1..2fec3cec3 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -143,7 +143,8 @@ void Wizard::MainWizard::runSettingsImporter() QStringList arguments; // Import plugin selection? - if (field(QLatin1String("installation.import-addons")).toBool() == true) + if (field(QLatin1String("installation.new")).toBool() == true + || field(QLatin1String("installation.import-addons")).toBool() == true) arguments.append(QLatin1String("--game-files")); arguments.append(QLatin1String("--encoding")); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 223de41ce..2319ca0df 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -162,8 +162,8 @@ void Wizard::UnshieldWorker::setupSettings() QFile file(getIniPath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qDebug() << "Error opening .ini file!"; - emit error(tr("Failed to open Morrowind configuration file!"), tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); + emit error(tr("Failed to open Morrowind configuration file!"), + tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); return; } @@ -181,7 +181,6 @@ void Wizard::UnshieldWorker::writeSettings() QFile file(getIniPath()); if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { - qDebug() << "Error opening .ini file!"; emit error(tr("Failed to open Morrowind configuration file!"), tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); return; @@ -198,7 +197,7 @@ void Wizard::UnshieldWorker::writeSettings() bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) { - bool result = true; + bool result = false; QDir dir(dirName); if (dir.exists(dirName)) @@ -229,38 +228,42 @@ bool Wizard::UnshieldWorker::copyFile(const QString &source, const QString &dest QFileInfo info(destination); - if (info.exists()) - dir.remove(info.absoluteFilePath()); + if (info.exists()) { + if (!dir.remove(info.absoluteFilePath())) + return false; + } if (file.copy(source, destination)) { if (!keepSource) { - return file.remove(source); + if (!file.remove(source)) + return false; } else { return true; } } else { - qDebug() << "copy failed! " << file.errorString(); - emit error(tr("Failed to copy file!"), tr("Copying %1 to %2 failed: %3.").arg(source, destination, file.errorString())); + return false; } - return false; + return true; } bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString &destination, bool keepSource) { QDir sourceDir(source); QDir destDir(destination); + bool result = false; if (!destDir.exists()) { - sourceDir.mkpath(destination); + if (!sourceDir.mkpath(destination)) { + return false; + } + destDir.refresh(); } if (!destDir.exists()) return false; - bool result = true; - QFileInfoList list(sourceDir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)); @@ -269,13 +272,15 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString QString relativePath(info.absoluteFilePath()); relativePath.remove(source); + QString destinationPath(destDir.absolutePath() + relativePath); + if (info.isSymLink()) continue; if (info.isDir()) { - result = moveDirectory(info.absoluteFilePath(), destDir.absolutePath() + relativePath); + result = copyDirectory(info.absoluteFilePath(), destinationPath); } else { - result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); + result = copyFile(info.absoluteFilePath(), destinationPath); } } @@ -285,80 +290,56 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString return result; } -bool Wizard::UnshieldWorker::moveFile(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::installFile(const QString &fileName, const QString &path, Qt::MatchFlags flags, bool keepSource) { - return copyFile(source, destination, false); + return installFiles(fileName, path, flags, true, keepSource); } -bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::installFiles(const QString &fileName, const QString &path, Qt::MatchFlags flags, bool keepSource, bool single) { - return copyDirectory(source, destination, false); -} - -bool Wizard::UnshieldWorker::installFile(const QString &fileName, const QString &path) -{ - qDebug() << "Attempting to find file: " << fileName << " in: " << path; - - bool result = true; QDir dir(path); if (!dir.exists()) return false; - QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | - QDir::System | QDir::Hidden | - QDir::AllDirs | QDir::Files, QDir::DirsFirst)); - foreach(const QFileInfo &info, list) { - if (info.isDir()) { - result = installFile(fileName, info.absoluteFilePath()); - } else { - if (info.fileName() == fileName) { - qDebug() << "File found at: " << info.absoluteFilePath(); + QStringList files(findFiles(fileName, path, flags)); - emit textChanged(tr("Installing: %1").arg(info.fileName())); - return moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); - } + foreach (const QString &file, files) { + QFileInfo info(file); + emit textChanged(tr("Installing: %1").arg(info.fileName())); + + if (single) { + return copyFile(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName(), keepSource); + } else { + if (!copyFile(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName(), keepSource)) + return false; } } - return result; + return true; } -bool Wizard::UnshieldWorker::installDirectory(const QString &dirName, const QString &path, bool recursive) +bool Wizard::UnshieldWorker::installDirectories(const QString &dirName, const QString &path, bool recursive, bool keepSource) { - qDebug() << "Attempting to find: " << dirName << " in: " << path; - bool result = true; QDir dir(path); if (!dir.exists()) return false; - QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | - QDir::System | QDir::Hidden | - QDir::AllDirs)); - foreach(const QFileInfo &info, list) { - if (info.isSymLink()) - continue; + QStringList directories(findDirectories(dirName, path, recursive)); - if (info.isDir()) { - if (info.fileName() == dirName) { - qDebug() << "Directory found at: " << info.absoluteFilePath(); - emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); - return copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); - } else { - if (recursive) - result = installDirectory(dirName, info.absoluteFilePath()); - } - } + foreach (const QString &dir, directories) { + QFileInfo info(dir); + emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); + if (!copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName(), keepSource)) + return false; } - return result; + return true; } void Wizard::UnshieldWorker::extract() { - qDebug() << "extract!"; - if (getInstallComponent(Wizard::Component_Morrowind)) { if (!getComponentDone(Wizard::Component_Morrowind)) @@ -383,23 +364,23 @@ void Wizard::UnshieldWorker::extract() // Update Morrowind configuration if (getInstallComponent(Wizard::Component_Tribunal)) { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); + mIniSettings.setValue(QLatin1String("Archives/Archive 0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile1"), QVariant(QString("Tribunal.esm"))); } if (getInstallComponent(Wizard::Component_Bloodmoon)) { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); + mIniSettings.setValue(QLatin1String("Archives/Archive 0"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile1"), QVariant(QString("Bloodmoon.esm"))); } if (getInstallComponent(Wizard::Component_Tribunal) && getInstallComponent(Wizard::Component_Bloodmoon)) { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); - mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); + mIniSettings.setValue(QLatin1String("Archives/Archive 0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Archives/Archive 1"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile1"), QVariant(QString("Tribunal.esm"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile2"), QVariant(QString("Bloodmoon.esm"))); } // Write the settings to the Morrowind config file @@ -423,8 +404,6 @@ void Wizard::UnshieldWorker::extract() emit textChanged(tr("Installation finished!")); emit progressChanged(total); emit finished(); - - qDebug() << "installation finished!"; } bool Wizard::UnshieldWorker::setupComponent(Component component) @@ -466,10 +445,26 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) QStringList list(findFiles(QLatin1String("data1.hdr"), disk.absolutePath())); foreach (const QString &file, list) { - qDebug() << "current cab file is: " << file; - if (findInCab(file, name + QLatin1String(".bsa"))) { - cabFile = file; - found = true; + + if (component == Wizard::Component_Morrowind) + { + bool morrowindFound = findInCab(file, QLatin1String("Morrowind.bsa")); + bool tribunalFound = findInCab(file, QLatin1String("Tribunal.bsa")); + bool bloodmoonFound = findInCab(file, QLatin1String("Bloodmoon.bsa")); + + if (morrowindFound) { + // Check if we have correct archive, other archives have Morrowind.bsa too + if ((tribunalFound && bloodmoonFound) + || (!tribunalFound && !bloodmoonFound)) { + cabFile = file; + found = true; // We have a GoTY disk + } + } + } else { + if (findInCab(file, name + QLatin1String(".bsa"))) { + cabFile = file; + found = true; + } } } @@ -484,7 +479,6 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) setComponentDone(component, true); return true; } else { - qDebug() << "Erorr installing " << name; return false; } @@ -512,9 +506,6 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString return false; } - qDebug() << "Install " << name << " from " << path; - - emit textChanged(tr("Installing %1").arg(name)); QFileInfo info(path); @@ -569,19 +560,34 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString << QLatin1String("Video"); foreach (const QString &dir, directories) { - installDirectory(dir, temp.absolutePath()); + if (!installDirectories(dir, temp.absolutePath())) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(dir, temp.absolutePath())); + return false; + } } // Install directories from disk foreach (const QString &dir, directories) { - qDebug() << "\n\nDISK DIRS!"; - installDirectory(dir, info.absolutePath(), false); + if (!installDirectories(dir, info.absolutePath(), false, true)) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(dir, info.absolutePath())); + } + } - QFileInfo datafiles(info.absolutePath() + QDir::separator() + QLatin1String("Data Files")); - if (datafiles.exists()) { - emit textChanged(tr("Installing: Data Files directory")); - copyDirectory(datafiles.absoluteFilePath(), getPath()); + // Install translation files + QStringList extensions; + extensions << QLatin1String(".cel") + << QLatin1String(".top") + << QLatin1String(".mrk"); + + foreach (const QString &extension, extensions) { + if (!installFiles(extension, info.absolutePath(), Qt::MatchEndsWith)) { + emit error(tr("Could not install translation file!"), + tr("Failed to install *%1 files.").arg(extension)); + return false; + } } if (component == Wizard::Component_Morrowind) @@ -592,14 +598,16 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { - emit error(tr("Could not find Morrowind data file!"), tr("Failed to find %1.").arg(file)); + emit error(tr("Could not install Morrowind data file!"), + tr("Failed to install %1.").arg(file)); return false; } } // Copy Morrowind configuration file if (!installFile(QLatin1String("Morrowind.ini"), temp.absolutePath())) { - emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %1.").arg(QLatin1String("Morrowind.ini"))); + emit error(tr("Could not install Morrowind configuration file!"), + tr("Failed to install %1.").arg(QLatin1String("Morrowind.ini"))); return false; } @@ -611,21 +619,25 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString if (component == Wizard::Component_Tribunal) { QFileInfo sounds(temp.absoluteFilePath(QLatin1String("Sounds"))); + QString dest(getPath() + QDir::separator() + QLatin1String("Sound")); if (sounds.exists()) { emit textChanged(tr("Installing: Sound directory")); - copyDirectory(sounds.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Sound")); + if (!copyDirectory(sounds.absoluteFilePath(), dest)) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(sounds.absoluteFilePath(), dest)); + } + } QStringList files; files << QLatin1String("Tribunal.esm") - << QLatin1String("Tribunal.bsa") - << QLatin1String("Morrowind.esm") - << QLatin1String("Morrowind.bsa"); + << QLatin1String("Tribunal.bsa"); foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { - emit error(tr("Could not find Tribunal data file!"), tr("Failed to find %1.").arg(file)); + emit error(tr("Could not find Tribunal data file!"), + tr("Failed to find %1.").arg(file)); return false; } } @@ -637,20 +649,20 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString if (original.exists()) { if (!installFile(QLatin1String("Tribunal.esm"), temp.absolutePath())) { - emit error(tr("Could not find Tribunal patch file!"), tr("Failed to find %1.").arg(QLatin1String("Tribunal.esm"))); + emit error(tr("Could not find Tribunal patch file!"), + tr("Failed to find %1.").arg(QLatin1String("Tribunal.esm"))); return false; } } QStringList files; files << QLatin1String("Bloodmoon.esm") - << QLatin1String("Bloodmoon.bsa") - << QLatin1String("Morrowind.esm") - << QLatin1String("Morrowind.bsa"); + << QLatin1String("Bloodmoon.bsa"); foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { - emit error(tr("Could not find Bloodmoon data file!"), tr("Failed to find %1.").arg(file)); + emit error(tr("Could not find Bloodmoon data file!"), + tr("Failed to find %1.").arg(file)); return false; } } @@ -665,6 +677,21 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString } } + // Finally, install Data Files directories from temp and disk + QStringList datafiles(findDirectories(QLatin1String("Data Files"), temp.absolutePath())); + datafiles.append(findDirectories(QLatin1String("Data Files"), info.absolutePath())); + + foreach (const QString &dir, datafiles) { + QFileInfo info(dir); + emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); + + if (!copyDirectory(info.absoluteFilePath(), getPath())) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(info.absoluteFilePath(), getPath())); + return false; + } + } + emit textChanged(tr("%1 installation finished!").arg(name)); return true; @@ -718,15 +745,15 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &dest return success; } -QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path, int depth) +QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path) { - return findFiles(fileName, path, depth).first(); + return findFiles(fileName, path).first(); } -QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QString &path, int depth) +QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QString &path, int depth, bool recursive, + bool directories, Qt::MatchFlags flags) { - qDebug() << "Searching path: " << path << " for: " << fileName; - static const int MAXIMUM_DEPTH = 5; + static const int MAXIMUM_DEPTH = 10; if (depth >= MAXIMUM_DEPTH) { qWarning("Maximum directory depth limit reached."); @@ -748,11 +775,31 @@ QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QSt continue; if (info.isDir()) { - result.append(findFiles(fileName, info.absoluteFilePath(), depth + 1)); + if (directories) + { + if (info.fileName() == fileName) { + result.append(info.absoluteFilePath()); + } else { + if (recursive) + result.append(findFiles(fileName, info.absoluteFilePath(), depth + 1, recursive, true)); + } + } else { + if (recursive) + result.append(findFiles(fileName, info.absoluteFilePath(), depth + 1)); + } } else { - if (info.fileName() == fileName) { - qDebug() << "File found at: " << info.absoluteFilePath(); - result.append(info.absoluteFilePath()); + if (directories) + break; + + switch (flags) { + case Qt::MatchExactly: + if (info.fileName() == fileName) + result.append(info.absoluteFilePath()); + break; + case Qt::MatchEndsWith: + if (info.fileName().endsWith(fileName)) + result.append(info.absoluteFilePath()); + break; } } } @@ -760,6 +807,11 @@ QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QSt return result; } +QStringList Wizard::UnshieldWorker::findDirectories(const QString &dirName, const QString &path, bool recursive) +{ + return findFiles(dirName, path, 0, true, true); +} + bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fileName) { QByteArray array(cabFile.toUtf8()); diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index bda951456..f23dc1aa7 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -31,7 +32,6 @@ namespace Wizard void setInstallComponent(Wizard::Component component, bool install); -// void setComponentPath(Wizard::Component component, const QString &path); void setDiskPath(const QString &path); void setPath(const QString &path); @@ -50,7 +50,6 @@ namespace Wizard bool getInstallComponent(Component component); - //QString getComponentPath(Component component); QString getDiskPath(); void setComponentDone(Component component, bool done = true); @@ -61,18 +60,25 @@ namespace Wizard bool copyFile(const QString &source, const QString &destination, bool keepSource = true); bool copyDirectory(const QString &source, const QString &destination, bool keepSource = true); - bool moveFile(const QString &source, const QString &destination); - bool moveDirectory(const QString &source, const QString &destination); - bool extractCab(const QString &cabFile, const QString &destination); bool extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter); bool findInCab(const QString &cabFile, const QString &fileName); - QString findFile(const QString &fileName, const QString &path, int depth = 0); - QStringList findFiles(const QString &fileName, const QString &path, int depth = 0); + QString findFile(const QString &fileName, const QString &path); + + QStringList findFiles(const QString &fileName, const QString &path, int depth = 0, bool recursive = true, + bool directories = false, Qt::MatchFlags flags = Qt::MatchExactly); + + QStringList findDirectories(const QString &dirName, const QString &path, bool recursive = true); - bool installFile(const QString &fileName, const QString &path); - bool installDirectory(const QString &dirName, const QString &path, bool recursive = true); + bool installFile(const QString &fileName, const QString &path, Qt::MatchFlags flags = Qt::MatchExactly, + bool keepSource = false); + + bool installFiles(const QString &fileName, const QString &path, Qt::MatchFlags flags = Qt::MatchExactly, + bool keepSource = false, bool single = false); + + bool installDirectories(const QString &dirName, const QString &path, + bool recursive = true, bool keepSource = false); bool installComponent(Component component, const QString &path); bool setupComponent(Component component); @@ -111,7 +117,6 @@ namespace Wizard void error(const QString &text, const QString &details); void progressChanged(int progress); - }; } From ae5f783a1650b8d65a2b65eeae149ab5729ef473 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 17 Mar 2014 17:50:51 +0100 Subject: [PATCH 40/69] Added some eye-candy to the wizard: a watermark and a placeholder icon --- apps/wizard/intropage.cpp | 2 +- apps/wizard/mainwizard.cpp | 3 ++- files/ui/wizard/intropage.ui | 6 ++++-- files/wizard/images/intropage-background.png | Bin 0 -> 150614 bytes files/wizard/images/openmw-wizard.png | Bin 0 -> 72530 bytes files/wizard/wizard.qrc | 4 ++++ 6 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 files/wizard/images/intropage-background.png create mode 100644 files/wizard/images/openmw-wizard.png diff --git a/apps/wizard/intropage.cpp b/apps/wizard/intropage.cpp index 93510cd21..91e7d5dc0 100644 --- a/apps/wizard/intropage.cpp +++ b/apps/wizard/intropage.cpp @@ -5,9 +5,9 @@ Wizard::IntroPage::IntroPage(MainWizard *wizard) : QWizardPage(wizard), mWizard(wizard) - { setupUi(this); + setPixmap(QWizard::WatermarkPixmap, QPixmap(QLatin1String(":/images/intropage-background.png"))); } int Wizard::IntroPage::nextId() const diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 2fec3cec3..6a72d71fb 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -25,7 +25,6 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : mGameSettings(mCfgMgr), QWizard(parent) { - #ifndef Q_OS_MAC setWizardStyle(QWizard::ModernStyle); #else @@ -33,6 +32,8 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : #endif setWindowTitle(tr("OpenMW Wizard")); + setWindowIcon(QIcon(QLatin1String(":/images/openmw-wizard.png"))); + setMinimumWidth(550); // Set the property for comboboxes to the text instead of index setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); diff --git a/files/ui/wizard/intropage.ui b/files/ui/wizard/intropage.ui index bb597e3a6..b06f7f170 100644 --- a/files/ui/wizard/intropage.ui +++ b/files/ui/wizard/intropage.ui @@ -20,7 +20,7 @@ - This Wizard will help you install Morrowind and its add-ons for OpenMW to use. + This Wizard will help you install Morrowindand its add-ons for OpenMW to use. true @@ -29,6 +29,8 @@ - + + + diff --git a/files/wizard/images/intropage-background.png b/files/wizard/images/intropage-background.png new file mode 100644 index 0000000000000000000000000000000000000000..0ce13804b4f5b3a2ad8338c8f6a5a88870acf778 GIT binary patch literal 150614 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�zTe*@)AGZt5mTa*$uAtDLDJ!rOVe5> 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 literal 0 HcmV?d00001 diff --git a/files/wizard/wizard.qrc b/files/wizard/wizard.qrc index b3bb722c9..99623e985 100644 --- a/files/wizard/wizard.qrc +++ b/files/wizard/wizard.qrc @@ -5,4 +5,8 @@ icons/tango/48x48/folder.png icons/tango/48x48/system-installer.png + + images/intropage-background.png + images/openmw-wizard.png + From 9e5e19f9cb6387f0d9f50bfe820ddc0e2ffcebd4 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 17 Mar 2014 18:28:05 +0100 Subject: [PATCH 41/69] Improved existing installation handling --- apps/wizard/existinginstallationpage.cpp | 95 ++++++++++++--------- apps/wizard/existinginstallationpage.hpp | 3 + apps/wizard/mainwizard.hpp | 1 - files/ui/wizard/existinginstallationpage.ui | 11 +-- 4 files changed, 60 insertions(+), 50 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 91d4f2df1..56743eb44 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -14,6 +14,12 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : { setupUi(this); + mEmptyItem = new QListWidgetItem(tr("No existing installations detected")); + mEmptyItem->setFlags(Qt::NoItemFlags); + installationsList->addItem(mEmptyItem); + mEmptyItem->setHidden(true); + + connect(installationsList, SIGNAL(currentTextChanged(QString)), this, SLOT(textChanged(QString))); @@ -21,51 +27,17 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : this, SIGNAL(completeChanged())); } -void Wizard::ExistingInstallationPage::on_browseButton_clicked() -{ - QString selectedFile = QFileDialog::getOpenFileName( - this, - tr("Select master file"), - QDir::currentPath(), - QString(tr("Morrowind master file (*.esm)")), - NULL, - QFileDialog::DontResolveSymlinks); - - QFileInfo info(selectedFile); - if (!info.exists()) - return; - - QString path(QDir::toNativeSeparators(info.absolutePath())); - QList items = installationsList->findItems(path, Qt::MatchExactly); - - if (items.isEmpty()) { - // Path is not yet in the list, add it - mWizard->addInstallation(path); - - QListWidgetItem *item = new QListWidgetItem(path); - installationsList->addItem(item); - installationsList->setCurrentItem(item); // Select it too - } else { - installationsList->setCurrentItem(items.first()); - } - -} - -void Wizard::ExistingInstallationPage::textChanged(const QString &text) -{ - // Set the installation path manually, as registerField doesn't work - // Because it doesn't accept two widgets operating on a single field - if (!text.isEmpty()) - mWizard->setField(QLatin1String("installation.path"), text); -} - void Wizard::ExistingInstallationPage::initializePage() { QStringList paths(mWizard->mInstallations.keys()); - if (paths.isEmpty()) + if (paths.isEmpty()) { + mEmptyItem->setHidden(false); return; + } + // Make to clear list before adding items + // to prevent duplicates when going back and forth between pages installationsList->clear(); foreach (const QString &path, paths) { @@ -119,6 +91,51 @@ bool Wizard::ExistingInstallationPage::validatePage() return true; } +void Wizard::ExistingInstallationPage::on_browseButton_clicked() +{ + QString selectedFile = QFileDialog::getOpenFileName( + this, + tr("Select master file"), + QDir::currentPath(), + QString(tr("Morrowind master file (*.esm)")), + NULL, + QFileDialog::DontResolveSymlinks); + + QFileInfo info(selectedFile); + + if (!info.exists()) + return; + + if (!mWizard->findFiles(QLatin1String("Morrowind"), info.absolutePath())) + return; // No valid Morrowind installation found + + QString path(QDir::toNativeSeparators(info.absolutePath())); + QList items = installationsList->findItems(path, Qt::MatchExactly); + + if (items.isEmpty()) { + // Path is not yet in the list, add it + mWizard->addInstallation(path); + + // Hide the default item + mEmptyItem->setHidden(true); + + QListWidgetItem *item = new QListWidgetItem(path); + installationsList->addItem(item); + installationsList->setCurrentItem(item); // Select it too + } else { + installationsList->setCurrentItem(items.first()); + } + +} + +void Wizard::ExistingInstallationPage::textChanged(const QString &text) +{ + // Set the installation path manually, as registerField doesn't work + // Because it doesn't accept two widgets operating on a single field + if (!text.isEmpty()) + mWizard->setField(QLatin1String("installation.path"), text); +} + bool Wizard::ExistingInstallationPage::isComplete() const { if (installationsList->selectionModel()->hasSelection()) { diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index d243bb868..04385893a 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -2,6 +2,7 @@ #define EXISTINGINSTALLATIONPAGE_HPP #include +#include #include "ui_existinginstallationpage.h" @@ -26,6 +27,8 @@ namespace Wizard private: MainWizard *mWizard; + QListWidgetItem *mEmptyItem; + protected: void initializePage(); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 962571ac8..f55709bad 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -9,7 +9,6 @@ namespace Wizard { - class MainWizard : public QWizard { Q_OBJECT diff --git a/files/ui/wizard/existinginstallationpage.ui b/files/ui/wizard/existinginstallationpage.ui index b887fb06c..921b2eb2a 100644 --- a/files/ui/wizard/existinginstallationpage.ui +++ b/files/ui/wizard/existinginstallationpage.ui @@ -28,16 +28,7 @@ - - - - No existing installations detected - - - NoItemFlags - - - + From 2e1248537e73e28e5ee8c345507cf8c59cca893b Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 17 Mar 2014 21:33:44 +0100 Subject: [PATCH 42/69] Minor fixes, installation should stop after one error message now --- apps/wizard/conclusionpage.cpp | 10 +++++----- apps/wizard/unshield/unshieldworker.cpp | 17 +++++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 7b638813b..932a9715c 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -13,11 +13,11 @@ Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : void Wizard::ConclusionPage::initializePage() { -// // Write the path to openmw.cfg -// if (field(QLatin1String("installation.new")).toBool() == true) { -// QString path(field(QLatin1String("installation.path")).toString()); -// mWizard->addInstallation(path); -// } + // Write the path to openmw.cfg + if (field(QLatin1String("installation.new")).toBool() == true) { + QString path(field(QLatin1String("installation.path")).toString()); + mWizard->addInstallation(path); + } if (!mWizard->mError) { diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 2319ca0df..aceac69b8 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -251,16 +251,15 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString { QDir sourceDir(source); QDir destDir(destination); - bool result = false; + bool result = true; if (!destDir.exists()) { - if (!sourceDir.mkpath(destination)) { + if (!sourceDir.mkpath(destination)) return false; - } - - destDir.refresh(); } + destDir.refresh(); + if (!destDir.exists()) return false; @@ -387,7 +386,7 @@ void Wizard::UnshieldWorker::extract() writeSettings(); // Remove the temporary directory - removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); + //removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); // Fill the progress bar int total = 0; @@ -572,6 +571,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString if (!installDirectories(dir, info.absolutePath(), false, true)) { emit error(tr("Could not install directory!"), tr("Installing %1 to %2 failed.").arg(dir, info.absolutePath())); + return false; } } @@ -626,6 +626,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString if (!copyDirectory(sounds.absoluteFilePath(), dest)) { emit error(tr("Could not install directory!"), tr("Installing %1 to %2 failed.").arg(sounds.absoluteFilePath(), dest)); + return false; } } @@ -699,7 +700,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter) { - bool success; + bool success = false; QString path(destination); path.append(QDir::separator()); @@ -845,7 +846,7 @@ bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fi bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &destination) { - bool success; + bool success = false; QByteArray array(cabFile.toUtf8()); From 667b0de250149fcb19d03ba065a1dc077dde828e Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 17 Mar 2014 23:39:21 +0100 Subject: [PATCH 43/69] Fixed problems with cd content autodetection --- apps/wizard/unshield/unshieldworker.cpp | 111 ++++++++++++++---------- apps/wizard/unshield/unshieldworker.hpp | 4 +- 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index aceac69b8..679730715 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -386,7 +386,7 @@ void Wizard::UnshieldWorker::extract() writeSettings(); // Remove the temporary directory - //removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); + removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); // Fill the progress bar int total = 0; @@ -445,26 +445,39 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) foreach (const QString &file, list) { + qDebug() << "current archive: " << file; + + // Try to open the archive + Unshield *unshield = NULL; + unshield = openCab(file); + + if (!unshield) + return false; + if (component == Wizard::Component_Morrowind) { - bool morrowindFound = findInCab(file, QLatin1String("Morrowind.bsa")); - bool tribunalFound = findInCab(file, QLatin1String("Tribunal.bsa")); - bool bloodmoonFound = findInCab(file, QLatin1String("Bloodmoon.bsa")); + bool morrowindFound = findInCab(QLatin1String("Morrowind.bsa"), unshield); + bool tribunalFound = findInCab(QLatin1String("Tribunal.bsa"), unshield); + bool bloodmoonFound = findInCab(QLatin1String("Bloodmoon.bsa"), unshield); if (morrowindFound) { // Check if we have correct archive, other archives have Morrowind.bsa too if ((tribunalFound && bloodmoonFound) || (!tribunalFound && !bloodmoonFound)) { cabFile = file; - found = true; // We have a GoTY disk + found = true; // We have a GoTY disk or a Morrowind-only disk } } } else { - if (findInCab(file, name + QLatin1String(".bsa"))) { + + if (findInCab(name + QLatin1String(".bsa"), unshield)) { cabFile = file; found = true; } } + + // Close the current archive + unshield_close(unshield); } if (!found) { @@ -746,6 +759,39 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &dest return success; } +bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &destination) +{ + bool success = false; + + QByteArray array(cabFile.toUtf8()); + + Unshield *unshield; + unshield = unshield_open(array.constData()); + + if (!unshield) { + emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); + return false; + } + + int counter = 0; + + for (int i=0; ifirst_file; j<=group->last_file; ++j) + { + if (unshield_file_is_valid(unshield, j)) { + success = extractFile(unshield, destination, group->name, j, counter); + ++counter; + } + } + } + + unshield_close(unshield); + return success; +} + QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path) { return findFiles(fileName, path).first(); @@ -764,14 +810,16 @@ QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QSt QStringList result; QDir dir(path); + // Prevent parsing over the complete filesystem + if (dir == QDir::rootPath()) + return QStringList(); + if (!dir.exists()) return QStringList(); QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | - QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)); foreach(QFileInfo info, list) { - if (info.isSymLink()) continue; @@ -813,7 +861,7 @@ QStringList Wizard::UnshieldWorker::findDirectories(const QString &dirName, cons return findFiles(dirName, path, 0, true, true); } -bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fileName) +Unshield* Wizard::UnshieldWorker::openCab(const QString &cabFile) { QByteArray array(cabFile.toUtf8()); @@ -822,43 +870,17 @@ bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fi if (!unshield) { emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); - return false; + unshield_close(unshield); + return NULL; } - for (int i=0; ifirst_file; j<=group->last_file; ++j) - { - - if (unshield_file_is_valid(unshield, j)) { - QString current(QString::fromUtf8(unshield_file_name(unshield, j))); - if (current.toLower() == fileName.toLower()) - return true; // File is found! - } - } - } - - unshield_close(unshield); - return false; + return unshield; } -bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &destination) +bool Wizard::UnshieldWorker::findInCab(const QString &fileName, Unshield *unshield) { - bool success = false; - - QByteArray array(cabFile.toUtf8()); - - Unshield *unshield; - unshield = unshield_open(array.constData()); - - if (!unshield) { - emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); + if (!unshield) return false; - } - - int counter = 0; for (int i=0; ifirst_file; j<=group->last_file; ++j) { + if (unshield_file_is_valid(unshield, j)) { - success = extractFile(unshield, destination, group->name, j, counter); - ++counter; + QString current(QString::fromUtf8(unshield_file_name(unshield, j))); + if (current.toLower() == fileName.toLower()) + return true; // File is found! } } } - unshield_close(unshield); - return success; + return false; } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index f23dc1aa7..36215bf5d 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -62,7 +62,9 @@ namespace Wizard bool extractCab(const QString &cabFile, const QString &destination); bool extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter); - bool findInCab(const QString &cabFile, const QString &fileName); + + Unshield* openCab(const QString &cabFile); + bool findInCab(const QString &fileName, Unshield *unshield); QString findFile(const QString &fileName, const QString &path); From ef617fdf3df484563ef3fd3e1a515042766b57e9 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 18 Mar 2014 00:33:31 +0100 Subject: [PATCH 44/69] Fail on the first error whilst extracting an archive --- apps/wizard/unshield/unshieldworker.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 679730715..70c780b3b 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -752,7 +752,7 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &dest success = unshield_file_save(unshield, index, array.constData()); if (!success) { - emit error(tr("Failed to extract %1.").arg(QString::fromUtf8(unshield_file_name(unshield, index))), tr("Complete path: %1.").arg(fileName)); + qDebug() << "error"; dir.remove(fileName); } @@ -783,6 +783,15 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &d { if (unshield_file_is_valid(unshield, j)) { success = extractFile(unshield, destination, group->name, j, counter); + + if (!success) { + QString name(QString::fromUtf8(unshield_file_name(unshield, j))); + + emit error(tr("Failed to extract %1.").arg(name), + tr("Complete path: %1").arg(destination + QDir::separator() + name)); + return false; + } + ++counter; } } From eb04fa85b7076d83e59fe28f1a9b97bb0109d0b7 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sat, 29 Mar 2014 11:57:25 +0100 Subject: [PATCH 45/69] Added background image to the last page too --- apps/wizard/conclusionpage.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 932a9715c..4407ac547 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -9,6 +9,7 @@ Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : mWizard(wizard) { setupUi(this); + setPixmap(QWizard::WatermarkPixmap, QPixmap(QLatin1String(":/images/intropage-background.png"))); } void Wizard::ConclusionPage::initializePage() From 10d2ca82f7a90a28b944057a32805a28c010b13e Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 30 Mar 2014 22:58:50 +0200 Subject: [PATCH 46/69] Implemented a simple logger --- apps/wizard/existinginstallationpage.cpp | 1 - apps/wizard/installationpage.cpp | 11 +++++- apps/wizard/mainwizard.cpp | 44 ++++++++++++++++++++++++ apps/wizard/mainwizard.hpp | 6 ++++ apps/wizard/unshield/unshieldworker.cpp | 24 ++++++++----- apps/wizard/unshield/unshieldworker.hpp | 5 ++- 6 files changed, 78 insertions(+), 13 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 56743eb44..d37dd949d 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -19,7 +19,6 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : installationsList->addItem(mEmptyItem); mEmptyItem->setHidden(true); - connect(installationsList, SIGNAL(currentTextChanged(QString)), this, SLOT(textChanged(QString))); diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 74666c394..a0d3dfd21 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -80,6 +80,9 @@ void Wizard::InstallationPage::startInstallation() connect(mUnshield, SIGNAL(textChanged(QString)), logTextEdit, SLOT(appendPlainText(QString)), Qt::QueuedConnection); + connect(mUnshield, SIGNAL(textChanged(QString)), + mWizard, SLOT(addLogText(QString)), Qt::QueuedConnection); + connect(mUnshield, SIGNAL(progressChanged(int)), installProgressBar, SLOT(setValue(int)), Qt::QueuedConnection); @@ -153,6 +156,8 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) if (path.isEmpty()) { logTextEdit->appendHtml(tr("


\ Error: The installation was aborted by the user

")); + + mWizard->addLogText(QLatin1String("Error: The installation was aborted by the user")); mWizard->mError = true; emit completeChanged(); @@ -185,6 +190,10 @@ void Wizard::InstallationPage::installationError(const QString &text, const QStr logTextEdit->appendHtml(tr("

\ %1

").arg(details)); + mWizard->addLogText(QLatin1String("Error: ") + text); + mWizard->addLogText(details); + + mWizard->mError = true; QMessageBox msgBox; msgBox.setWindowTitle(tr("An error occurred")); msgBox.setIcon(QMessageBox::Critical); @@ -196,7 +205,7 @@ void Wizard::InstallationPage::installationError(const QString &text, const QStr msgBox.setDetailedText(details); msgBox.exec(); - mWizard->mError = true; + emit completeChanged(); } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 6a72d71fb..11e44c6b0 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -4,6 +4,8 @@ #include +#include +#include #include #include #include @@ -40,11 +42,53 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); + setupLog(); setupGameSettings(); setupInstallations(); setupPages(); } +void Wizard::MainWizard::setupLog() +{ + QString logPath(QString::fromUtf8(mCfgMgr.getLogPath().string().c_str())); + logPath.append(QLatin1String("wizard.log")); + + QString message(tr("

Could not open %1 for writing

\ +

Please make sure you have the right permissions \ + and try again.

")); + + QFile *file = new QFile(logPath); + + if (!file->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening Wizard log file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(message.arg(file->fileName())); + msgBox.exec(); + return qApp->quit(); + } + + qDebug() << logPath; + + mLog = new QTextStream(file); + mLog->setCodec(QTextCodec::codecForName("UTF-8")); + + //addLogText(QLatin1String("test test 123 test")); +} + +void Wizard::MainWizard::addLogText(const QString &text) +{ + + qDebug() << "AddLogText called! " << text; + if (!text.isEmpty()) { + qDebug() << "logging " << text; + *mLog << text << endl; + } + +// file.close(); +} + void Wizard::MainWizard::setupGameSettings() { QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index f55709bad..dcb07a8c2 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -46,8 +46,12 @@ namespace Wizard bool mError; + public slots: + void addLogText(const QString &text); + private: + void setupLog(); void setupGameSettings(); void setupInstallations(); void setupPages(); @@ -56,6 +60,8 @@ namespace Wizard Config::GameSettings mGameSettings; + QTextStream *mLog; + private slots: void accept(); void reject(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 70c780b3b..f0b9a8674 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -153,37 +153,39 @@ void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) mIniCodec = codec; } -void Wizard::UnshieldWorker::setupSettings() +bool Wizard::UnshieldWorker::setupSettings() { // Create Morrowind.ini settings map if (getIniPath().isEmpty()) - return; + return false; QFile file(getIniPath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { emit error(tr("Failed to open Morrowind configuration file!"), tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); - return; + return false; } QTextStream stream(&file); stream.setCodec(mIniCodec); mIniSettings.readFile(stream); + + return true; } -void Wizard::UnshieldWorker::writeSettings() +bool Wizard::UnshieldWorker::writeSettings() { if (getIniPath().isEmpty()) - return; + return false; QFile file(getIniPath()); if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { emit error(tr("Failed to open Morrowind configuration file!"), tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); - return; + return false; } QTextStream stream(&file); @@ -192,7 +194,10 @@ void Wizard::UnshieldWorker::writeSettings() if (!mIniSettings.writeFile(getIniPath(), stream)) { emit error(tr("Failed to write Morrowind configuration file!"), tr("Writing to %1 failed: %2.").arg(getIniPath(), file.errorString())); + return false; } + + return true; } bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) @@ -383,7 +388,8 @@ void Wizard::UnshieldWorker::extract() } // Write the settings to the Morrowind config file - writeSettings(); + if (!writeSettings()) + return false; // Remove the temporary directory removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); @@ -626,7 +632,9 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString // Setup Morrowind configuration setIniPath(getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); - setupSettings(); + + if (!setupSettings()) + return false; } if (component == Wizard::Component_Tribunal) diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 36215bf5d..e5a712977 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -42,11 +42,11 @@ namespace Wizard void setIniCodec(QTextCodec *codec); - void setupSettings(); + bool setupSettings(); private: - void writeSettings(); + bool writeSettings(); bool getInstallComponent(Component component); @@ -114,7 +114,6 @@ namespace Wizard void requestFileDialog(Wizard::Component component); void textChanged(const QString &text); - void logTextChanged(const QString &text); void error(const QString &text, const QString &details); void progressChanged(int progress); From 14b164a03d60615f67a0e0cb97a7fe93c36f2fcd Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 30 Mar 2014 23:11:38 +0200 Subject: [PATCH 47/69] Reverted an unneeded fix --- apps/wizard/unshield/unshieldworker.cpp | 32 +++++++------------------ apps/wizard/unshield/unshieldworker.hpp | 3 +-- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index f0b9a8674..81282f547 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -389,7 +389,7 @@ void Wizard::UnshieldWorker::extract() // Write the settings to the Morrowind config file if (!writeSettings()) - return false; + return; // Remove the temporary directory removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); @@ -453,18 +453,11 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) qDebug() << "current archive: " << file; - // Try to open the archive - Unshield *unshield = NULL; - unshield = openCab(file); - - if (!unshield) - return false; - if (component == Wizard::Component_Morrowind) { - bool morrowindFound = findInCab(QLatin1String("Morrowind.bsa"), unshield); - bool tribunalFound = findInCab(QLatin1String("Tribunal.bsa"), unshield); - bool bloodmoonFound = findInCab(QLatin1String("Bloodmoon.bsa"), unshield); + bool morrowindFound = findInCab(QLatin1String("Morrowind.bsa"), file); + bool tribunalFound = findInCab(QLatin1String("Tribunal.bsa"), file); + bool bloodmoonFound = findInCab(QLatin1String("Bloodmoon.bsa"), file); if (morrowindFound) { // Check if we have correct archive, other archives have Morrowind.bsa too @@ -476,14 +469,12 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) } } else { - if (findInCab(name + QLatin1String(".bsa"), unshield)) { + if (findInCab(name + QLatin1String(".bsa"), file)) { cabFile = file; found = true; } } - // Close the current archive - unshield_close(unshield); } if (!found) { @@ -778,6 +769,7 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &d if (!unshield) { emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); + unshield_close(unshield); return false; } @@ -878,7 +870,7 @@ QStringList Wizard::UnshieldWorker::findDirectories(const QString &dirName, cons return findFiles(dirName, path, 0, true, true); } -Unshield* Wizard::UnshieldWorker::openCab(const QString &cabFile) +bool Wizard::UnshieldWorker::findInCab(const QString &fileName, const QString &cabFile) { QByteArray array(cabFile.toUtf8()); @@ -888,16 +880,8 @@ Unshield* Wizard::UnshieldWorker::openCab(const QString &cabFile) if (!unshield) { emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); unshield_close(unshield); - return NULL; - } - - return unshield; -} - -bool Wizard::UnshieldWorker::findInCab(const QString &fileName, Unshield *unshield) -{ - if (!unshield) return false; + } for (int i=0; i Date: Sun, 30 Mar 2014 23:57:30 +0200 Subject: [PATCH 48/69] Started working on the settings tab --- apps/launcher/settingspage.cpp | 17 +++++++++++++++++ apps/launcher/settingspage.hpp | 7 +++++++ files/ui/settingspage.ui | 11 +++++++---- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 96dd6e529..142af5f79 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -1,5 +1,11 @@ #include "settingspage.hpp" +#include + +#include + +using namespace Process; + Launcher::SettingsPage::SettingsPage(QWidget *parent) : QWidget(parent) { setupUi(this); @@ -16,3 +22,14 @@ Launcher::SettingsPage::SettingsPage(QWidget *parent) : QWidget(parent) languageComboBox->addItems(languages); } +void Launcher::SettingsPage::on_wizardButton_clicked() +{ + if (!ProcessInvoker::startProcess(QLatin1String("openmw-wizard"), true)) + qDebug() << "an error occurred"; + +} + +void Launcher::SettingsPage::on_importerButton_clicked() +{ + +} diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index c87f6e46f..4b0773244 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -11,9 +11,16 @@ namespace Launcher class SettingsPage : public QWidget, private Ui::SettingsPage { Q_OBJECT + public: SettingsPage(QWidget *parent = 0); + void saveSettings(); + bool loadSettings(); + + private slots: + void on_wizardButton_clicked(); + void on_importerButton_clicked(); }; } diff --git a/files/ui/settingspage.ui b/files/ui/settingspage.ui index 95b62761e..ce5aa57ec 100644 --- a/files/ui/settingspage.ui +++ b/files/ui/settingspage.ui @@ -6,8 +6,8 @@ 0 0 - 518 - 401 + 516 + 399
@@ -22,8 +22,11 @@ + + <html><head/><body><p>The language of the original Morrowind installation files (used for the character encoding)</p></body></html> + - Morrowind installation language: + Morrowind content language: @@ -105,7 +108,7 @@ - Import previously selected add-ons (creates a new Content List) + Import add-on and plugin selection (creates a new Content List) true From 21c406316f21cde0922efdde7f9e7a54184d2fa5 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 16 Apr 2014 16:54:55 +0200 Subject: [PATCH 49/69] Working on the Settings tab: start the importer/wizard --- apps/launcher/datafilespage.cpp | 4 +- apps/launcher/maindialog.cpp | 32 +++++- apps/launcher/maindialog.hpp | 4 +- apps/launcher/settingspage.cpp | 116 +++++++++++++++++++- apps/launcher/settingspage.hpp | 32 +++++- apps/launcher/utils/textinputdialog.cpp | 46 +++----- apps/launcher/utils/textinputdialog.hpp | 20 ++-- apps/wizard/mainwizard.cpp | 4 +- components/config/launchersettings.cpp | 10 +- components/process/processinvoker.cpp | 139 ++++++++++++++++++------ components/process/processinvoker.hpp | 28 ++++- files/ui/settingspage.ui | 12 +- 12 files changed, 340 insertions(+), 107 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index db5975102..efd9765ac 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -1,5 +1,7 @@ #include "datafilespage.hpp" +#include + #include #include #include @@ -221,7 +223,7 @@ void Launcher::DataFilesPage::on_newProfileAction_triggered() if (newDialog.exec() != QDialog::Accepted) return; - QString profile = newDialog.getText(); + QString profile = newDialog.lineEdit()->text(); if (profile.isEmpty()) return; diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 7f129f4e9..b8f9efd1a 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -138,7 +137,7 @@ void Launcher::MainDialog::createPages() mPlayPage = new PlayPage(this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mGraphicsPage = new GraphicsPage(mCfgMgr, mGraphicsSettings, this); - mSettingsPage = new SettingsPage(this); + mSettingsPage = new SettingsPage(mGameSettings, mLauncherSettings, this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); @@ -285,7 +284,9 @@ bool Launcher::MainDialog::showFirstRunDialog() arguments.append(QString("--cfg")); arguments.append(path); - if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) + ProcessInvoker invoker(this); + + if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) return false; // Re-read the game settings @@ -337,6 +338,26 @@ bool Launcher::MainDialog::setup() return true; } +bool Launcher::MainDialog::reloadSettings() +{ + if (!setupLauncherSettings()) + return false; + + if (!setupGameSettings()) + return false; + + if (!setupGraphicsSettings()) + return false; + +// if (!mSettingsPage->loadSettings()) +// return false; + + if (!mGraphicsPage->loadSettings()) + return false; + + return true; +} + void Launcher::MainDialog::changePage(QListWidgetItem *current, QListWidgetItem *previous) { if (!current) @@ -799,6 +820,7 @@ bool Launcher::MainDialog::writeSettings() void Launcher::MainDialog::closeEvent(QCloseEvent *event) { + qDebug() << "close event!"; writeSettings(); event->accept(); } @@ -822,6 +844,8 @@ void Launcher::MainDialog::play() } // Launch the game detached - if (ProcessInvoker::startProcess(QLatin1String("openmw"), true)) + ProcessInvoker invoker(this); + + if (invoker.startProcess(QLatin1String("openmw"), true)) qApp->quit(); } diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index 718acb5a9..7f468de9c 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -39,6 +39,9 @@ namespace Launcher bool setup(); bool showFirstRunDialog(); + bool reloadSettings(); + bool writeSettings(); + public slots: void changePage(QListWidgetItem *current, QListWidgetItem *previous); void play(); @@ -53,7 +56,6 @@ namespace Launcher void loadSettings(); void saveSettings(); - bool writeSettings(); inline bool startProgram(const QString &name, bool detached = false) { return startProgram(name, QStringList(), detached); } bool startProgram(const QString &name, const QStringList &arguments, bool detached = false); diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 142af5f79..b2d03ba95 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -1,12 +1,21 @@ #include "settingspage.hpp" -#include - +#include #include +#include +#include + +#include "utils/textinputdialog.hpp" + using namespace Process; -Launcher::SettingsPage::SettingsPage(QWidget *parent) : QWidget(parent) +Launcher::SettingsPage::SettingsPage(Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, MainDialog *parent) : + mGameSettings(gameSettings), + mLauncherSettings(launcherSettings), + QWidget(parent), + mMain(parent) { setupUi(this); @@ -20,16 +29,111 @@ Launcher::SettingsPage::SettingsPage(QWidget *parent) : QWidget(parent) << "Spanish"; languageComboBox->addItems(languages); + + mWizardInvoker = new ProcessInvoker(this); + mImporterInvoker = new ProcessInvoker(this); + + connect(mWizardInvoker->getProcess(), SIGNAL(started()), + this, SLOT(wizardStarted())); + + connect(mWizardInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(wizardFinished(int,QProcess::ExitStatus))); + + connect(mImporterInvoker->getProcess(), SIGNAL(started()), + this, SLOT(importerStarted())); + + connect(mImporterInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(importerFinished(int,QProcess::ExitStatus))); + + mProfileDialog = new TextInputDialog(tr("New Profile"), tr("Profile name:"), this); + + connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), + this, SLOT(updateOkButton(QString))); + +// // Detect Morrowind configuration files +// foreach (const QString &path, mGameSettings.getDataDirs()) { +// QDir dir(path); +// dir.setPath(dir.canonicalPath()); // Resolve symlinks + +// if (dir.exists(QString("Morrowind.ini"))) +// iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); +// else +// { +// if (!dir.cdUp()) +// continue; // Cannot move from Data Files + +// if (dir.exists(QString("Morrowind.ini"))) +// iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); +// } +// } } void Launcher::SettingsPage::on_wizardButton_clicked() { - if (!ProcessInvoker::startProcess(QLatin1String("openmw-wizard"), true)) - qDebug() << "an error occurred"; - + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) + return; } void Launcher::SettingsPage::on_importerButton_clicked() { + if (!mImporterInvoker->startProcess(QLatin1String("mwiniimport"), false)) + return; +} + +void Launcher::SettingsPage::wizardStarted() +{ + qDebug() << "wizard started!"; + wizardButton->setEnabled(false); +} + +void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + qDebug() << "wizard finished!"; + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return; + + mMain->writeSettings(); + mMain->reloadSettings(); + wizardButton->setEnabled(true); +} + +void Launcher::SettingsPage::importerStarted() +{ + qDebug() << "importer started!"; + importerButton->setEnabled(false); +} + +void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + qDebug() << "importer finished!"; + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return; + + mMain->writeSettings(); + mMain->reloadSettings(); + + + if (addonsCheckBox->isChecked()) { + + if (mProfileDialog->exec() == QDialog::Accepted) { + QString profile = mProfileDialog->lineEdit()->text(); + qDebug() << profile; + } + } + + importerButton->setEnabled(true); +} + +void Launcher::SettingsPage::updateOkButton(const QString &text) +{ + // We do this here because we need the profiles combobox text + if (text.isEmpty()) { + mProfileDialog->setOkButtonEnabled(false); + return; + } + +// (profilesComboBox->findText(text) == -1) +// ? mNewProfileDialog->setOkButtonEnabled(true) +// : mNewProfileDialog->setOkButtonEnabled(false); } diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index 4b0773244..ed5c068fb 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -2,18 +2,28 @@ #define SETTINGSPAGE_HPP #include +#include + +#include #include "ui_settingspage.h" +#include "maindialog.hpp" + +namespace Config { class GameSettings; + class LauncherSettings; } + namespace Launcher { + class TextInputDialog; class SettingsPage : public QWidget, private Ui::SettingsPage { Q_OBJECT public: - SettingsPage(QWidget *parent = 0); + SettingsPage( Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, MainDialog *parent = 0); void saveSettings(); bool loadSettings(); @@ -21,6 +31,26 @@ namespace Launcher private slots: void on_wizardButton_clicked(); void on_importerButton_clicked(); + + void wizardStarted(); + void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); + + void importerStarted(); + void importerFinished(int exitCode, QProcess::ExitStatus exitStatus); + + void updateOkButton(const QString &text); + + private: + Process::ProcessInvoker *mWizardInvoker; + Process::ProcessInvoker *mImporterInvoker; + + Config::GameSettings &mGameSettings; + Config::LauncherSettings &mLauncherSettings; + + MainDialog *mMain; + TextInputDialog *mProfileDialog; + + }; } diff --git a/apps/launcher/utils/textinputdialog.cpp b/apps/launcher/utils/textinputdialog.cpp index 76cbe32d0..74e40a81c 100644 --- a/apps/launcher/utils/textinputdialog.cpp +++ b/apps/launcher/utils/textinputdialog.cpp @@ -14,17 +14,17 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & mButtonBox = new QDialogButtonBox(this); mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); - mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); +// mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); + + QLabel *label = new QLabel(this); + label->setText(text); // Line edit QValidator *validator = new QRegExpValidator(QRegExp("^[a-zA-Z0-9_]*$"), this); // Alpha-numeric + underscore - mLineEdit = new DialogLineEdit(this); + mLineEdit = new LineEdit(this); mLineEdit->setValidator(validator); mLineEdit->setCompleter(0); - QLabel *label = new QLabel(this); - label->setText(text); - QVBoxLayout *dialogLayout = new QVBoxLayout(this); dialogLayout->addWidget(label); dialogLayout->addWidget(mLineEdit); @@ -41,8 +41,10 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & connect(mButtonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(mButtonBox, SIGNAL(rejected()), this, SLOT(reject())); - connect(mLineEdit, SIGNAL(textChanged(QString)), this, SLOT(slotUpdateOkButton(QString))); +} +Launcher::TextInputDialog::~TextInputDialog() +{ } int Launcher::TextInputDialog::exec() @@ -52,36 +54,18 @@ int Launcher::TextInputDialog::exec() return QDialog::exec(); } -QString Launcher::TextInputDialog::getText() const +void Launcher::TextInputDialog::setOkButtonEnabled(bool enabled) { - return mLineEdit->text(); -} + QPushButton *okButton = mButtonBox->button(QDialogButtonBox::Ok); + okButton->setEnabled(enabled); -void Launcher::TextInputDialog::slotUpdateOkButton(QString text) -{ - bool enabled = !(text.isEmpty()); - mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(enabled); + QPalette *palette = new QPalette(); + palette->setColor(QPalette::Text, Qt::red); - if (enabled) + if (enabled) { mLineEdit->setPalette(QApplication::palette()); - else - { + } else { // Existing profile name, make the text red - QPalette *palette = new QPalette(); - palette->setColor(QPalette::Text,Qt::red); mLineEdit->setPalette(*palette); } } - -Launcher::TextInputDialog::DialogLineEdit::DialogLineEdit (QWidget *parent) : - LineEdit (parent) -{ - int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); - - setObjectName(QString("LineEdit")); - setStyleSheet(QString("LineEdit { padding-right: %1px; } ").arg(mClearButton->sizeHint().width() + frameWidth + 1)); - QSize msz = minimumSizeHint(); - setMinimumSize(qMax(msz.width(), mClearButton->sizeHint().height() + frameWidth * 2 + 2), - qMax(msz.height(), mClearButton->sizeHint().height() + frameWidth * 2 + 2)); - -} diff --git a/apps/launcher/utils/textinputdialog.hpp b/apps/launcher/utils/textinputdialog.hpp index bb01778be..9eb9c717d 100644 --- a/apps/launcher/utils/textinputdialog.hpp +++ b/apps/launcher/utils/textinputdialog.hpp @@ -13,26 +13,20 @@ namespace Launcher { Q_OBJECT - class DialogLineEdit : public LineEdit - { - public: - explicit DialogLineEdit (QWidget *parent = 0); - }; - - DialogLineEdit *mLineEdit; - QDialogButtonBox *mButtonBox; - public: explicit TextInputDialog(const QString& title, const QString &text, QWidget *parent = 0); - ~TextInputDialog () {} + ~TextInputDialog (); - QString getText() const; + inline LineEdit *lineEdit() { return mLineEdit; } + void setOkButtonEnabled(bool enabled); int exec(); - private slots: - void slotUpdateOkButton(QString text); + private: + + QDialogButtonBox *mButtonBox; + LineEdit *mLineEdit; }; } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 11e44c6b0..0a53e10c5 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -211,8 +211,8 @@ void Wizard::MainWizard::runSettingsImporter() arguments.append(QLatin1String("--cfg")); arguments.append(userPath + QLatin1String("openmw.cfg")); - if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) - return qApp->quit();; +// if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) +// return qApp->quit();; // Re-read the game settings setupGameSettings(); diff --git a/components/config/launchersettings.cpp b/components/config/launchersettings.cpp index 14d66bba7..c014579dc 100644 --- a/components/config/launchersettings.cpp +++ b/components/config/launchersettings.cpp @@ -41,14 +41,20 @@ QStringList Config::LauncherSettings::subKeys(const QString &key) QMap settings = SettingsBase::getSettings(); QStringList keys = settings.uniqueKeys(); + qDebug() << keys; + QRegExp keyRe("(.+)/"); QStringList result; foreach (const QString ¤tKey, keys) { - if (keyRe.indexIn(currentKey) != -1) { + + if (keyRe.indexIn(currentKey) != -1) + { QString prefixedKey = keyRe.cap(1); - if(prefixedKey.startsWith(key)) { + + if(prefixedKey.startsWith(key)) + { QString subKey = prefixedKey.remove(key); if (!subKey.isEmpty()) result.append(subKey); diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp index e75daae9b..7ac8d3c93 100644 --- a/components/process/processinvoker.cpp +++ b/components/process/processinvoker.cpp @@ -1,23 +1,63 @@ #include "processinvoker.hpp" #include -#include #include #include #include #include #include +#include -Process::ProcessInvoker::ProcessInvoker() +Process::ProcessInvoker::ProcessInvoker(QWidget *parent) { + mProcess = new QProcess(this); + + mName = QString(); + mArguments = QStringList(); } Process::ProcessInvoker::~ProcessInvoker() { } +//void Process::ProcessInvoker::setProcessName(const QString &name) +//{ +// mName = name; +//} + +//void Process::ProcessInvoker::setProcessArguments(const QStringList &arguments) +//{ +// mArguments = arguments; +//} + +QProcess* Process::ProcessInvoker::getProcess() +{ + return mProcess; +} + +//QString Process::ProcessInvoker::getProcessName() +//{ +// return mName; +//} + +//QStringList Process::ProcessInvoker::getProcessArguments() +//{ +// return mArguments; +//} + bool Process::ProcessInvoker::startProcess(const QString &name, const QStringList &arguments, bool detached) { +// mProcess = new QProcess(this); + + connect(mProcess, SIGNAL(error(QProcess::ProcessError)), + this, SLOT(processError(QProcess::ProcessError))); + + connect(mProcess, SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(processFinished(int,QProcess::ExitStatus))); + + mName = name; + mArguments = arguments; + QString path(name); #ifdef Q_OS_WIN path.append(QLatin1String(".exe")); @@ -28,7 +68,6 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis path.prepend(QLatin1String("./")); #endif - QProcess process; QFileInfo info(path); if (!info.exists()) { @@ -57,7 +96,7 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis // Start the executable if (detached) { - if (!process.startDetached(path, arguments)) { + if (!mProcess->startDetached(path, arguments)) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Error starting executable")); msgBox.setIcon(QMessageBox::Critical); @@ -65,45 +104,79 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis msgBox.setText(tr("

Could not start %1

\

An error occurred while starting %1.

\

Press \"Show Details...\" for more information.

").arg(info.fileName())); - msgBox.setDetailedText(process.errorString()); + msgBox.setDetailedText(mProcess->errorString()); msgBox.exec(); return false; } } else { - process.start(path, arguments); - if (!process.waitForFinished()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("

Could not start %1

\ -

An error occurred while starting %1.

\ -

Press \"Show Details...\" for more information.

").arg(info.fileName())); - msgBox.setDetailedText(process.errorString()); - msgBox.exec(); + mProcess->start(path, arguments); - return false; - } +// if (!mProcess->waitForFinished()) { +// QMessageBox msgBox; +// msgBox.setWindowTitle(tr("Error starting executable")); +// msgBox.setIcon(QMessageBox::Critical); +// msgBox.setStandardButtons(QMessageBox::Ok); +// msgBox.setText(tr("

Could not start %1

\ +//

An error occurred while starting %1.

\ +//

Press \"Show Details...\" for more information.

").arg(info.fileName())); +// msgBox.setDetailedText(mProcess->errorString()); +// msgBox.exec(); - if (process.exitCode() != 0 || process.exitStatus() == QProcess::CrashExit) { - QString error(process.readAllStandardError()); - error.append(tr("\nArguments:\n")); - error.append(arguments.join(" ")); +// return false; +// } - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error running executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("

Executable %1 returned an error

\ -

An error occurred while running %1.

\ -

Press \"Show Details...\" for more information.

").arg(info.fileName())); - msgBox.setDetailedText(error); - msgBox.exec(); +// if (mProcess->exitCode() != 0 || mProcess->exitStatus() == QProcess::CrashExit) { +// QString error(mProcess->readAllStandardError()); +// error.append(tr("\nArguments:\n")); +// error.append(arguments.join(" ")); - return false; - } +// QMessageBox msgBox; +// msgBox.setWindowTitle(tr("Error running executable")); +// msgBox.setIcon(QMessageBox::Critical); +// msgBox.setStandardButtons(QMessageBox::Ok); +// msgBox.setText(tr("

Executable %1 returned an error

\ +//

An error occurred while running %1.

\ +//

Press \"Show Details...\" for more information.

").arg(info.fileName())); +// msgBox.setDetailedText(error); +// msgBox.exec(); + +// return false; +// } } return true; } + +void Process::ProcessInvoker::processError(QProcess::ProcessError error) +{ + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error running executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Executable %1 returned an error

\ +

An error occurred while running %1.

\ +

Press \"Show Details...\" for more information.

").arg(mName)); + msgBox.setDetailedText(mProcess->errorString()); + msgBox.exec(); + +} + +void Process::ProcessInvoker::processFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitCode != 0 || exitStatus == QProcess::CrashExit) { + QString error(mProcess->readAllStandardError()); + error.append(tr("\nArguments:\n")); + error.append(mArguments.join(" ")); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error running executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Executable %1 returned an error

\ +

An error occurred while running %1.

\ +

Press \"Show Details...\" for more information.

").arg(mName)); + msgBox.setDetailedText(error); + msgBox.exec(); + } +} diff --git a/components/process/processinvoker.hpp b/components/process/processinvoker.hpp index d59d2f012..07907e3b4 100644 --- a/components/process/processinvoker.hpp +++ b/components/process/processinvoker.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace Process { @@ -10,13 +11,32 @@ namespace Process { Q_OBJECT - ProcessInvoker(); + public: + + ProcessInvoker(QWidget *parent = 0); ~ProcessInvoker(); - public: +// void setProcessName(const QString &name); +// void setProcessArguments(const QStringList &arguments); + + QProcess* getProcess(); +// QString getProcessName(); +// QStringList getProcessArguments(); + +// inline bool startProcess(bool detached = false) { return startProcess(mName, mArguments, detached); } + inline bool startProcess(const QString &name, bool detached = false) { return startProcess(name, QStringList(), detached); } + bool startProcess(const QString &name, const QStringList &arguments, bool detached = false); + + private: + QProcess *mProcess; + + QString mName; + QStringList mArguments; + + private slots: + void processError(QProcess::ProcessError error); + void processFinished(int exitCode, QProcess::ExitStatus exitStatus); - inline static bool startProcess(const QString &name, bool detached = false) { return startProcess(name, QStringList(), detached); } - bool static startProcess(const QString &name, const QStringList &arguments, bool detached = false); }; } diff --git a/files/ui/settingspage.ui b/files/ui/settingspage.ui index ce5aa57ec..6c873ea92 100644 --- a/files/ui/settingspage.ui +++ b/files/ui/settingspage.ui @@ -6,8 +6,8 @@ 0 0 - 516 - 399 + 514 + 397
@@ -88,13 +88,7 @@
- - - - /home/user/.local/share/openmw/data/Morrowind.ini - - - + From f8bb797b8a384f33e44436a58184382872b83cbf Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 16 Apr 2014 18:34:24 +0200 Subject: [PATCH 50/69] Settings tab is fully functional now --- apps/launcher/datafilespage.cpp | 30 +++--- apps/launcher/settingspage.cpp | 111 ++++++++++++++++------- apps/launcher/settingspage.hpp | 1 + apps/wizard/existinginstallationpage.cpp | 5 +- apps/wizard/languageselectionpage.cpp | 14 +-- 5 files changed, 106 insertions(+), 55 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index efd9765ac..72afe1c1d 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -42,7 +42,7 @@ void Launcher::DataFilesPage::loadSettings() QString profileName = ui.profilesComboBox->currentText(); - QStringList files = mLauncherSettings.values(QString("Profiles/") + profileName, Qt::MatchExactly); + QStringList files = mLauncherSettings.values(QString("Profiles/") + profileName + QString("/content"), Qt::MatchExactly); QStringList filepaths; @@ -75,7 +75,7 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile) mLauncherSettings.setValue(QString("Profiles/currentprofile"), ui.profilesComboBox->currentText()); foreach(const ContentSelectorModel::EsmFile *item, items) { - mLauncherSettings.setMultiValue(QString("Profiles/") + profileName, item->fileName()); + mLauncherSettings.setMultiValue(QString("Profiles/") + profileName + QString("/content"), item->fileName()); mGameSettings.setMultiValue(QString("content"), item->fileName()); } @@ -192,21 +192,21 @@ void Launcher::DataFilesPage::setupDataFiles() if (!mDataLocal.isEmpty()) mSelector->addFiles(mDataLocal); - QStringList profiles; + QStringList profiles = mLauncherSettings.subKeys(QString("Profiles/")); QString currentProfile = mLauncherSettings.getSettings().value("Profiles/currentprofile"); - foreach (QString key, mLauncherSettings.getSettings().keys()) - { - if (key.contains("Profiles/")) - { - QString profile = key.mid (9); - if (profile != "currentprofile") - { - if (!profiles.contains(profile)) - profiles << profile; - } - } - } +// foreach (QString key, mLauncherSettings.getSettings().keys()) +// { +// if (key.contains("Profiles/")) +// { +// QString profile = key.mid (9); +// if (profile != "currentprofile") +// { +// if (!profiles.contains(profile)) +// profiles << profile; +// } +// } +// } foreach (const QString &item, profiles) addProfile (item, false); diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index b2d03ba95..3c8c119ec 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -1,7 +1,9 @@ #include "settingspage.hpp" +#include #include #include +#include #include #include @@ -20,13 +22,13 @@ Launcher::SettingsPage::SettingsPage(Config::GameSettings &gameSettings, setupUi(this); QStringList languages; - languages << "English" - << "French" - << "German" - << "Italian" - << "Polish" - << "Russian" - << "Spanish"; + languages << QLatin1String("English") + << QLatin1String("French") + << QLatin1String("German") + << QLatin1String("Italian") + << QLatin1String("Polish") + << QLatin1String("Russian") + << QLatin1String("Spanish"); languageComboBox->addItems(languages); @@ -50,22 +52,31 @@ Launcher::SettingsPage::SettingsPage(Config::GameSettings &gameSettings, connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(updateOkButton(QString))); -// // Detect Morrowind configuration files -// foreach (const QString &path, mGameSettings.getDataDirs()) { -// QDir dir(path); -// dir.setPath(dir.canonicalPath()); // Resolve symlinks - -// if (dir.exists(QString("Morrowind.ini"))) -// iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); -// else -// { -// if (!dir.cdUp()) -// continue; // Cannot move from Data Files - -// if (dir.exists(QString("Morrowind.ini"))) -// iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); -// } -// } + // Detect Morrowind configuration files + QStringList iniPaths; + + foreach (const QString &path, mGameSettings.getDataDirs()) { + QDir dir(path); + dir.setPath(dir.canonicalPath()); // Resolve symlinks + + if (dir.exists(QString("Morrowind.ini"))) + iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); + else + { + if (!dir.cdUp()) + continue; // Cannot move from Data Files + + if (dir.exists(QString("Morrowind.ini"))) + iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); + } + } + + if (!iniPaths.isEmpty()) { + settingsComboBox->addItems(iniPaths); + importerButton->setEnabled(true); + } else { + importerButton->setEnabled(false); + } } void Launcher::SettingsPage::on_wizardButton_clicked() @@ -80,6 +91,32 @@ void Launcher::SettingsPage::on_importerButton_clicked() return; } +void Launcher::SettingsPage::on_browseButton_clicked() +{ + QString iniFile = QFileDialog::getOpenFileName( + this, + QObject::tr("Select configuration file"), + QDir::currentPath(), + QString(tr("Morrowind configuration file (*.ini)"))); + + + if (iniFile.isEmpty()) + return; + + QFileInfo info(iniFile); + + if (!info.exists() || !info.isReadable()) + return; + + const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); + + if (settingsComboBox->findText(path) == -1) { + settingsComboBox->addItem(path); + settingsComboBox->setCurrentIndex(settingsComboBox->findText(path)); + importerButton->setEnabled(true); + } +} + void Launcher::SettingsPage::wizardStarted() { qDebug() << "wizard started!"; @@ -109,18 +146,27 @@ void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus if (exitCode != 0 || exitStatus == QProcess::CrashExit) return; - mMain->writeSettings(); - mMain->reloadSettings(); + // Import selected data files from openmw.cfg + if (addonsCheckBox->isChecked()) + { + if (mProfileDialog->exec() == QDialog::Accepted) + { + const QString profile(mProfileDialog->lineEdit()->text()); + const QStringList files(mGameSettings.values(QLatin1String("content"))); + // Doesn't quite work right now + mLauncherSettings.setValue(QLatin1String("Profiles/currentprofile"), profile); - if (addonsCheckBox->isChecked()) { + foreach (const QString &file, files) { + mLauncherSettings.setMultiValue(QLatin1String("Profiles/") + profile + QLatin1String("/content"), file); + } - if (mProfileDialog->exec() == QDialog::Accepted) { - QString profile = mProfileDialog->lineEdit()->text(); - qDebug() << profile; + mGameSettings.remove(QLatin1String("content")); } } + mMain->writeSettings(); + mMain->reloadSettings(); importerButton->setEnabled(true); } @@ -132,8 +178,9 @@ void Launcher::SettingsPage::updateOkButton(const QString &text) return; } + const QStringList profiles(mLauncherSettings.subKeys(QString("Profiles/"))); -// (profilesComboBox->findText(text) == -1) -// ? mNewProfileDialog->setOkButtonEnabled(true) -// : mNewProfileDialog->setOkButtonEnabled(false); + (profiles.contains(text)) + ? mProfileDialog->setOkButtonEnabled(false) + : mProfileDialog->setOkButtonEnabled(true); } diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index ed5c068fb..d43141fc4 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -31,6 +31,7 @@ namespace Launcher private slots: void on_wizardButton_clicked(); void on_importerButton_clicked(); + void on_browseButton_clicked(); void wizardStarted(); void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index d37dd949d..13dcf9941 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -72,7 +72,7 @@ bool Wizard::ExistingInstallationPage::validatePage() QString iniFile; if (msgBox.clickedButton() == browseButton) { iniFile = QFileDialog::getOpenFileName( - NULL, + this, QObject::tr("Select configuration file"), QDir::currentPath(), QString(tr("Morrowind configuration file (*.ini)"))); @@ -100,6 +100,9 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() NULL, QFileDialog::DontResolveSymlinks); + if (selectedFile.isEmpty()) + return; + QFileInfo info(selectedFile); if (!info.exists()) diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index b921d5ddb..8beb106a2 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -14,13 +14,13 @@ Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : void Wizard::LanguageSelectionPage::initializePage() { QStringList languages; - languages << "English" - << "French" - << "German" - << "Italian" - << "Polish" - << "Russian" - << "Spanish"; + languages << QLatin1String("English") + << QLatin1String("French") + << QLatin1String("German") + << QLatin1String("Italian") + << QLatin1String("Polish") + << QLatin1String("Russian") + << QLatin1String("Spanish"); languageComboBox->addItems(languages); } From 6ed76858d9ce85f9ead853bab070ed48cb48574d Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 16 Apr 2014 23:59:25 +0200 Subject: [PATCH 51/69] Fixed some minor problems with the wizard --- apps/wizard/existinginstallationpage.cpp | 48 +++++---------- apps/wizard/existinginstallationpage.hpp | 5 -- apps/wizard/languageselectionpage.cpp | 15 ++++- apps/wizard/mainwizard.cpp | 76 +++++++++++++++++++++++- apps/wizard/mainwizard.hpp | 3 + 5 files changed, 106 insertions(+), 41 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 13dcf9941..66edc1e8a 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -14,36 +14,30 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : { setupUi(this); - mEmptyItem = new QListWidgetItem(tr("No existing installations detected")); - mEmptyItem->setFlags(Qt::NoItemFlags); - installationsList->addItem(mEmptyItem); - mEmptyItem->setHidden(true); + QListWidgetItem *emptyItem = new QListWidgetItem(tr("No existing installations detected")); + emptyItem->setFlags(Qt::NoItemFlags); + installationsList->addItem(emptyItem); - connect(installationsList, SIGNAL(currentTextChanged(QString)), - this, SLOT(textChanged(QString))); - - connect(installationsList,SIGNAL(itemSelectionChanged()), - this, SIGNAL(completeChanged())); -} - -void Wizard::ExistingInstallationPage::initializePage() -{ + // Add the available installation paths QStringList paths(mWizard->mInstallations.keys()); + // Hide the default item if there are installations to choose from if (paths.isEmpty()) { - mEmptyItem->setHidden(false); - return; + installationsList->item(0)->setHidden(false); + } else { + installationsList->item(0)->setHidden(true); } - // Make to clear list before adding items - // to prevent duplicates when going back and forth between pages - installationsList->clear(); - foreach (const QString &path, paths) { QListWidgetItem *item = new QListWidgetItem(path); installationsList->addItem(item); } + connect(installationsList, SIGNAL(currentTextChanged(QString)), + this, SLOT(textChanged(QString))); + + connect(installationsList,SIGNAL(itemSelectionChanged()), + this, SIGNAL(completeChanged())); } bool Wizard::ExistingInstallationPage::validatePage() @@ -119,7 +113,7 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() mWizard->addInstallation(path); // Hide the default item - mEmptyItem->setHidden(true); + installationsList->item(0)->setHidden(true); QListWidgetItem *item = new QListWidgetItem(path); installationsList->addItem(item); @@ -149,17 +143,5 @@ bool Wizard::ExistingInstallationPage::isComplete() const int Wizard::ExistingInstallationPage::nextId() const { - QString path(field(QLatin1String("installation.path")).toString()); - - if (path.isEmpty()) - return MainWizard::Page_LanguageSelection; - - if (mWizard->mInstallations[path]->hasMorrowind == true && - mWizard->mInstallations[path]->hasTribunal == true && - mWizard->mInstallations[path]->hasBloodmoon == true) - { - return MainWizard::Page_Import; - } else { - return MainWizard::Page_LanguageSelection; - } + return MainWizard::Page_LanguageSelection; } diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index 04385893a..54ee8d2e2 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -2,7 +2,6 @@ #define EXISTINGINSTALLATIONPAGE_HPP #include -#include #include "ui_existinginstallationpage.h" @@ -27,11 +26,7 @@ namespace Wizard private: MainWizard *mWizard; - QListWidgetItem *mEmptyItem; - - protected: - void initializePage(); }; } diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index 8beb106a2..7f97e6918 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -27,5 +27,18 @@ void Wizard::LanguageSelectionPage::initializePage() int Wizard::LanguageSelectionPage::nextId() const { - return MainWizard::Page_ComponentSelection; + // Check if we have to install something + QString path(field(QLatin1String("installation.path")).toString()); + + if (path.isEmpty()) + return MainWizard::Page_ComponentSelection; + + if (mWizard->mInstallations[path]->hasMorrowind == true && + mWizard->mInstallations[path]->hasTribunal == true && + mWizard->mInstallations[path]->hasBloodmoon == true) + { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_ComponentSelection; + } } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 0a53e10c5..fa27b548c 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -44,6 +44,7 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setupLog(); setupGameSettings(); + setupLauncherSettings(); setupInstallations(); setupPages(); } @@ -149,6 +150,40 @@ void Wizard::MainWizard::setupGameSettings() } } +void Wizard::MainWizard::setupLauncherSettings() +{ + QString path(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + path.append(QLatin1String("launcher.cfg")); + + QString message(tr("

Could not open %1 for reading

\ +

Please make sure you have the right permissions \ + and try again.

")); + + + QFile file(path); + + qDebug() << "Loading config file:" << qPrintable(path); + + if (file.exists()) { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(message.arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mLauncherSettings.readFile(stream); + } + + file.close(); + +} + void Wizard::MainWizard::setupInstallations() { // Check if the paths actually contain a Morrowind installation @@ -211,8 +246,10 @@ void Wizard::MainWizard::runSettingsImporter() arguments.append(QLatin1String("--cfg")); arguments.append(userPath + QLatin1String("openmw.cfg")); -// if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) -// return qApp->quit();; + ProcessInvoker invoker(this); + + if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) + return qApp->quit();; // Re-read the game settings setupGameSettings(); @@ -285,6 +322,19 @@ void Wizard::MainWizard::reject() void Wizard::MainWizard::writeSettings() { + // Write the encoding and language settings + QString language(field(QLatin1String("installation.language")).toString()); + mLauncherSettings.setValue(QLatin1String("Settings/language"), language); + + if (language == QLatin1String("Polish")) { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250")); + } else if (language == QLatin1String("Russian")) { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251")); + } else { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); + } + + // Write the installation path so that openmw can find them QString path(field(QLatin1String("installation.path")).toString()); // Make sure the installation path is the last data= entry @@ -329,6 +379,28 @@ void Wizard::MainWizard::writeSettings() mGameSettings.writeFile(stream); file.close(); + + // Launcher settings + file.setFileName(userPath + QLatin1String("launcher.cfg")); + + if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { + // File cannot be opened or created + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not open %1 for writing

\ +

Please make sure you have the right permissions \ + and try again.

").arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + + stream.setDevice(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mLauncherSettings.writeFile(stream); + file.close(); } bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index dcb07a8c2..63b7e62e8 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -6,6 +6,7 @@ #include #include +#include namespace Wizard { @@ -53,12 +54,14 @@ namespace Wizard void setupLog(); void setupGameSettings(); + void setupLauncherSettings(); void setupInstallations(); void setupPages(); void writeSettings(); Config::GameSettings mGameSettings; + Config::LauncherSettings mLauncherSettings; QTextStream *mLog; From 30c3c3e245c8b19a18ce08df208d24e96594f480 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 17 Apr 2014 00:01:19 +0200 Subject: [PATCH 52/69] Working on importing content lists in the launcher --- apps/launcher/datafilespage.cpp | 126 ++++++----- apps/launcher/datafilespage.hpp | 5 +- apps/launcher/maindialog.cpp | 269 ++++++++++-------------- apps/launcher/settingspage.cpp | 95 ++++++++- apps/launcher/settingspage.hpp | 7 +- apps/launcher/utils/textinputdialog.cpp | 2 +- 6 files changed, 269 insertions(+), 235 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 72afe1c1d..21652a8f4 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -34,16 +34,68 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: setupDataFiles(); } -void Launcher::DataFilesPage::loadSettings() +void Launcher::DataFilesPage::buildView() +{ + ui.verticalLayout->insertWidget (0, mSelector->uiWidget()); + + //tool buttons + ui.newProfileButton->setToolTip ("Create a new profile"); + ui.deleteProfileButton->setToolTip ("Delete an existing profile"); + + //combo box + ui.profilesComboBox->addItem ("Default"); + ui.profilesComboBox->setPlaceholderText (QString("Select a profile...")); + ui.profilesComboBox->setCurrentIndex(ui.profilesComboBox->findText(QLatin1String("Default"))); + + // Add the actions to the toolbuttons + ui.newProfileButton->setDefaultAction (ui.newProfileAction); + ui.deleteProfileButton->setDefaultAction (ui.deleteProfileAction); + + //establish connections + connect (ui.profilesComboBox, SIGNAL (currentIndexChanged(int)), + this, SLOT (slotProfileChanged(int))); + + connect (ui.profilesComboBox, SIGNAL (profileRenamed(QString, QString)), + this, SLOT (slotProfileRenamed(QString, QString))); + + connect (ui.profilesComboBox, SIGNAL (signalProfileChanged(QString, QString)), + this, SLOT (slotProfileChangedByUser(QString, QString))); +} + +void Launcher::DataFilesPage::setupDataFiles() +{ + QStringList paths = mGameSettings.getDataDirs(); + + foreach (const QString &path, paths) + mSelector->addFiles(path); + + mDataLocal = mGameSettings.getDataLocal(); + + if (!mDataLocal.isEmpty()) + mSelector->addFiles(mDataLocal); + + loadSettings(); +} + +bool Launcher::DataFilesPage::loadSettings() { QStringList paths = mGameSettings.getDataDirs(); paths.insert (0, mDataLocal); PathIterator pathIterator (paths); - QString profileName = ui.profilesComboBox->currentText(); + QStringList profiles = mLauncherSettings.subKeys(QString("Profiles/")); + QString currentProfile = mLauncherSettings.getSettings().value("Profiles/currentprofile"); + + qDebug() << "current profile is: " << currentProfile; - QStringList files = mLauncherSettings.values(QString("Profiles/") + profileName + QString("/content"), Qt::MatchExactly); + foreach (const QString &item, profiles) + addProfile (item, false); + + // Hack: also add the current profile + if (!currentProfile.isEmpty()) + addProfile(currentProfile, true); + QStringList files = mLauncherSettings.values(QString("Profiles/") + currentProfile + QString("/content"), Qt::MatchExactly); QStringList filepaths; foreach (const QString &file, files) @@ -55,6 +107,8 @@ void Launcher::DataFilesPage::loadSettings() } mSelector->setProfileContent (filepaths); + + return true; } void Launcher::DataFilesPage::saveSettings(const QString &profile) @@ -81,33 +135,6 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile) } -void Launcher::DataFilesPage::buildView() -{ - ui.verticalLayout->insertWidget (0, mSelector->uiWidget()); - - //tool buttons - ui.newProfileButton->setToolTip ("Create a new profile"); - ui.deleteProfileButton->setToolTip ("Delete an existing profile"); - - //combo box - ui.profilesComboBox->addItem ("Default"); - ui.profilesComboBox->setPlaceholderText (QString("Select a profile...")); - - // Add the actions to the toolbuttons - ui.newProfileButton->setDefaultAction (ui.newProfileAction); - ui.deleteProfileButton->setDefaultAction (ui.deleteProfileAction); - - //establish connections - connect (ui.profilesComboBox, SIGNAL (currentIndexChanged(int)), - this, SLOT (slotProfileChanged(int))); - - connect (ui.profilesComboBox, SIGNAL (profileRenamed(QString, QString)), - this, SLOT (slotProfileRenamed(QString, QString))); - - connect (ui.profilesComboBox, SIGNAL (signalProfileChanged(QString, QString)), - this, SLOT (slotProfileChangedByUser(QString, QString))); -} - void Launcher::DataFilesPage::removeProfile(const QString &profile) { mLauncherSettings.remove(QString("Profiles/") + profile); @@ -140,6 +167,9 @@ void Launcher::DataFilesPage::setProfile (const QString &previous, const QString if (previous == current) return; + if (previous.isEmpty()) + return; + if (!previous.isEmpty() && savePrevious) saveSettings (previous); @@ -180,42 +210,6 @@ void Launcher::DataFilesPage::slotProfileChanged(int index) setProfile (index, true); } -void Launcher::DataFilesPage::setupDataFiles() -{ - QStringList paths = mGameSettings.getDataDirs(); - - foreach (const QString &path, paths) - mSelector->addFiles(path); - - mDataLocal = mGameSettings.getDataLocal(); - - if (!mDataLocal.isEmpty()) - mSelector->addFiles(mDataLocal); - - QStringList profiles = mLauncherSettings.subKeys(QString("Profiles/")); - QString currentProfile = mLauncherSettings.getSettings().value("Profiles/currentprofile"); - -// foreach (QString key, mLauncherSettings.getSettings().keys()) -// { -// if (key.contains("Profiles/")) -// { -// QString profile = key.mid (9); -// if (profile != "currentprofile") -// { -// if (!profiles.contains(profile)) -// profiles << profile; -// } -// } -// } - - foreach (const QString &item, profiles) - addProfile (item, false); - - setProfile (ui.profilesComboBox->findText(currentProfile), false); - - loadSettings(); -} - void Launcher::DataFilesPage::on_newProfileAction_triggered() { TextInputDialog newDialog (tr("New Profile"), tr("Profile name:"), this); diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 642668cb7..06efdfca3 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -39,7 +39,9 @@ namespace Launcher //void writeConfig(QString profile = QString()); void saveSettings(const QString &profile = ""); - void loadSettings(); + bool loadSettings(); + + void setupDataFiles(); signals: void signalProfileChanged (int index); @@ -70,7 +72,6 @@ namespace Launcher void setPluginsCheckstates(Qt::CheckState state); void buildView(); - void setupDataFiles(); void setupConfig(); void readConfig(); void setProfile (int index, bool savePrevious); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index b8f9efd1a..9c6e33e1b 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -137,7 +137,7 @@ void Launcher::MainDialog::createPages() mPlayPage = new PlayPage(this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mGraphicsPage = new GraphicsPage(mCfgMgr, mGraphicsSettings, this); - mSettingsPage = new SettingsPage(mGameSettings, mLauncherSettings, this); + mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); @@ -161,150 +161,103 @@ void Launcher::MainDialog::createPages() bool Launcher::MainDialog::showFirstRunDialog() { - QStringList iniPaths; - foreach (const QString &path, mGameSettings.getDataDirs()) { - QDir dir(path); - dir.setPath(dir.canonicalPath()); // Resolve symlinks - - if (dir.exists(QString("Morrowind.ini"))) - iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); - else - { - if (!dir.cdUp()) - continue; // Cannot move from Data Files - - if (dir.exists(QString("Morrowind.ini"))) - iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); - } - } - - // Ask the user where the Morrowind.ini is - if (iniPaths.empty()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error detecting Morrowind configuration")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setStandardButtons(QMessageBox::Cancel); - msgBox.setText(QObject::tr("
Could not find Morrowind.ini

\ - OpenMW needs to import settings from this file.

\ - Press \"Browse...\" to specify the location manually.
")); - - QAbstractButton *dirSelectButton = - msgBox.addButton(QObject::tr("B&rowse..."), QMessageBox::ActionRole); - - msgBox.exec(); - - QString iniFile; - if (msgBox.clickedButton() == dirSelectButton) { - iniFile = QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select configuration file"), - QDir::currentPath(), - QString(tr("Morrowind configuration file (*.ini)"))); - } - - if (iniFile.isEmpty()) - return false; // Cancel was clicked; - - QFileInfo info(iniFile); - iniPaths.clear(); - iniPaths.append(info.absoluteFilePath()); - } - - CheckableMessageBox msgBox(this); - msgBox.setWindowTitle(tr("Morrowind installation detected")); - - QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion); - int size = QApplication::style()->pixelMetric(QStyle::PM_MessageBoxIconSize); - msgBox.setIconPixmap(icon.pixmap(size, size)); - - QAbstractButton *importerButton = - msgBox.addButton(tr("Import"), QDialogButtonBox::AcceptRole); // ActionRole doesn't work?! - QAbstractButton *skipButton = - msgBox.addButton(tr("Skip"), QDialogButtonBox::RejectRole); - - Q_UNUSED(skipButton); // Surpress compiler unused warning - - msgBox.setStandardButtons(QDialogButtonBox::NoButton); - msgBox.setText(tr("
An existing Morrowind configuration was detected
\ -
Would you like to import settings from Morrowind.ini?
\ -
Warning: In most cases OpenMW needs these settings to run properly
")); - msgBox.setCheckBoxText(tr("Include selected masters and plugins (creates a new profile)")); - msgBox.exec(); - - - if (msgBox.clickedButton() == importerButton) { - - if (iniPaths.count() > 1) { - // Multiple Morrowind.ini files found - bool ok; - QString path = QInputDialog::getItem(this, tr("Multiple configurations found"), - tr("
There are multiple Morrowind.ini files found.

\ - Please select the one you wish to import from:"), iniPaths, 0, false, &ok); - if (ok && !path.isEmpty()) { - iniPaths.clear(); - iniPaths.append(path); - } else { - // Cancel was clicked - return false; - } - } - - // Create the file if it doesn't already exist, else the importer will fail - QString path = QString::fromStdString(mCfgMgr.getUserConfigPath().string()) + QString("openmw.cfg"); - QFile file(path); - - if (!file.exists()) { - if (!file.open(QIODevice::ReadWrite)) { - // File cannot be created - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not open or create %0 for writing

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); - return false; - } - file.close(); - } - - // Construct the arguments to run the importer - QStringList arguments; - - if (msgBox.isChecked()) - arguments.append(QString("--game-files")); - - arguments.append(QString("--encoding")); - arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); - arguments.append(QString("--ini")); - arguments.append(iniPaths.first()); - arguments.append(QString("--cfg")); - arguments.append(path); - ProcessInvoker invoker(this); - - if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) - return false; - - // Re-read the game settings - if (!setupGameSettings()) - return false; - - // Add a new profile - if (msgBox.isChecked()) { - mLauncherSettings.setValue(QString("Profiles/currentprofile"), QString("Imported")); - mLauncherSettings.remove(QString("Profiles/Imported/content")); - - QStringList contents = mGameSettings.values(QString("content")); - foreach (const QString &content, contents) { - mLauncherSettings.setMultiValue(QString("Profiles/Imported/content"), content); - } - } - - } +// CheckableMessageBox msgBox(this); +// msgBox.setWindowTitle(tr("Morrowind installation detected")); + +// QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion); +// int size = QApplication::style()->pixelMetric(QStyle::PM_MessageBoxIconSize); +// msgBox.setIconPixmap(icon.pixmap(size, size)); + +// QAbstractButton *importerButton = +// msgBox.addButton(tr("Import"), QDialogButtonBox::AcceptRole); // ActionRole doesn't work?! +// QAbstractButton *skipButton = +// msgBox.addButton(tr("Skip"), QDialogButtonBox::RejectRole); + +// Q_UNUSED(skipButton); // Surpress compiler unused warning + +// msgBox.setStandardButtons(QDialogButtonBox::NoButton); +// msgBox.setText(tr("
An existing Morrowind configuration was detected
\ +//
Would you like to import settings from Morrowind.ini?
\ +//
Warning: In most cases OpenMW needs these settings to run properly
")); +// msgBox.setCheckBoxText(tr("Include selected masters and plugins (creates a new profile)")); +// msgBox.exec(); + + +// if (msgBox.clickedButton() == importerButton) { + +// if (iniPaths.count() > 1) { +// // Multiple Morrowind.ini files found +// bool ok; +// QString path = QInputDialog::getItem(this, tr("Multiple configurations found"), +// tr("
There are multiple Morrowind.ini files found.

\ +// Please select the one you wish to import from:"), iniPaths, 0, false, &ok); +// if (ok && !path.isEmpty()) { +// iniPaths.clear(); +// iniPaths.append(path); +// } else { +// // Cancel was clicked +// return false; +// } +// } + +// // Create the file if it doesn't already exist, else the importer will fail +// QString path = QString::fromStdString(mCfgMgr.getUserConfigPath().string()) + QString("openmw.cfg"); +// QFile file(path); + +// if (!file.exists()) { +// if (!file.open(QIODevice::ReadWrite)) { +// // File cannot be created +// QMessageBox msgBox; +// msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); +// msgBox.setIcon(QMessageBox::Critical); +// msgBox.setStandardButtons(QMessageBox::Ok); +// msgBox.setText(tr("
Could not open or create %0 for writing

\ +// Please make sure you have the right permissions \ +// and try again.
").arg(file.fileName())); +// msgBox.exec(); +// return false; +// } + +// file.close(); +// } + +// // Construct the arguments to run the importer +// QStringList arguments; + +// if (msgBox.isChecked()) +// arguments.append(QString("--game-files")); + +// arguments.append(QString("--encoding")); +// arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); +// arguments.append(QString("--ini")); +// arguments.append(iniPaths.first()); +// arguments.append(QString("--cfg")); +// arguments.append(path); + +// ProcessInvoker invoker(this); + +// if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) +// return false; + +// // Re-read the game settings +// if (!setupGameSettings()) +// return false; + +// // Add a new profile +// if (msgBox.isChecked()) { +// mLauncherSettings.setValue(QString("Profiles/currentprofile"), QString("Imported")); +// mLauncherSettings.remove(QString("Profiles/Imported/content")); + +// QStringList contents = mGameSettings.values(QString("content")); +// foreach (const QString &content, contents) { +// mLauncherSettings.setMultiValue(QString("Profiles/Imported/content"), content); +// } +// } + +// } return true; } @@ -349,8 +302,11 @@ bool Launcher::MainDialog::reloadSettings() if (!setupGraphicsSettings()) return false; -// if (!mSettingsPage->loadSettings()) -// return false; + if (!mSettingsPage->loadSettings()) + return false; + + if (!mDataFilesPage->loadSettings()) + return false; if (!mGraphicsPage->loadSettings()) return false; @@ -371,17 +327,17 @@ void Launcher::MainDialog::changePage(QListWidgetItem *current, QListWidgetItem DataFilesPage *previousPage = dynamic_cast(pagesWidget->widget(previousIndex)); DataFilesPage *currentPage = dynamic_cast(pagesWidget->widget(currentIndex)); - //special call to update/save data files page list view when it's displayed/hidden. - if (previousPage) - { - if (previousPage->objectName() == "DataFilesPage") - previousPage->saveSettings(); - } - else if (currentPage) - { - if (currentPage->objectName() == "DataFilesPage") - currentPage->loadSettings(); - } +// //special call to update/save data files page list view when it's displayed/hidden. +// if (previousPage) +// { +// if (previousPage->objectName() == "DataFilesPage") +// previousPage->saveSettings(); +// } +// else if (currentPage) +// { +// if (currentPage->objectName() == "DataFilesPage") +// currentPage->loadSettings(); +// } } bool Launcher::MainDialog::setupLauncherSettings() @@ -729,8 +685,9 @@ bool Launcher::MainDialog::writeSettings() { // Now write all config files saveSettings(); - mGraphicsPage->saveSettings(); mDataFilesPage->saveSettings(); + mGraphicsPage->saveSettings(); + mSettingsPage->saveSettings(); QString userPath = QString::fromStdString(mCfgMgr.getUserConfigPath().string()); QDir dir(userPath); diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 3c8c119ec..f673917fd 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include @@ -12,12 +14,14 @@ using namespace Process; -Launcher::SettingsPage::SettingsPage(Config::GameSettings &gameSettings, - Config::LauncherSettings &launcherSettings, MainDialog *parent) : - mGameSettings(gameSettings), - mLauncherSettings(launcherSettings), - QWidget(parent), - mMain(parent) +Launcher::SettingsPage::SettingsPage(Files::ConfigurationManager &cfg, + Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, MainDialog *parent) + : mCfgMgr(cfg) + , mGameSettings(gameSettings) + , mLauncherSettings(launcherSettings) + , QWidget(parent) + , mMain(parent) { setupUi(this); @@ -77,17 +81,60 @@ Launcher::SettingsPage::SettingsPage(Config::GameSettings &gameSettings, } else { importerButton->setEnabled(false); } + + loadSettings(); } void Launcher::SettingsPage::on_wizardButton_clicked() { + saveSettings(); + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) return; } void Launcher::SettingsPage::on_importerButton_clicked() { - if (!mImporterInvoker->startProcess(QLatin1String("mwiniimport"), false)) + saveSettings(); + + // Create the file if it doesn't already exist, else the importer will fail + QString path(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + path.append(QLatin1String("openmw.cfg")); + QFile file(path); + + if (!file.exists()) { + if (!file.open(QIODevice::ReadWrite)) { + // File cannot be created + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not open or create %1 for writing

\ +

Please make sure you have the right permissions \ + and try again.

").arg(file.fileName())); + msgBox.exec(); + return; + } + + file.close(); + } + + // Construct the arguments to run the importer + QStringList arguments; + + if (addonsCheckBox->isChecked()) + arguments.append(QString("--game-files")); + + arguments.append(QString("--encoding")); + arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); + arguments.append(QString("--ini")); + arguments.append(settingsComboBox->currentText()); + arguments.append(QString("--cfg")); + arguments.append(path); + + qDebug() << "arguments " << arguments; + + if (!mImporterInvoker->startProcess(QLatin1String("mwiniimport"), arguments, false)) return; } @@ -129,7 +176,6 @@ void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus e if (exitCode != 0 || exitStatus == QProcess::CrashExit) return; - mMain->writeSettings(); mMain->reloadSettings(); wizardButton->setEnabled(true); } @@ -146,6 +192,9 @@ void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus if (exitCode != 0 || exitStatus == QProcess::CrashExit) return; + // Re-read the settings in their current state + mMain->reloadSettings(); + // Import selected data files from openmw.cfg if (addonsCheckBox->isChecked()) { @@ -154,6 +203,8 @@ void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus const QString profile(mProfileDialog->lineEdit()->text()); const QStringList files(mGameSettings.values(QLatin1String("content"))); + qDebug() << "Profile " << profile << files; + // Doesn't quite work right now mLauncherSettings.setValue(QLatin1String("Profiles/currentprofile"), profile); @@ -165,7 +216,6 @@ void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus } } - mMain->writeSettings(); mMain->reloadSettings(); importerButton->setEnabled(true); } @@ -184,3 +234,30 @@ void Launcher::SettingsPage::updateOkButton(const QString &text) ? mProfileDialog->setOkButtonEnabled(false) : mProfileDialog->setOkButtonEnabled(true); } + +void Launcher::SettingsPage::saveSettings() +{ + QString language(languageComboBox->currentText()); + + mLauncherSettings.setValue(QLatin1String("Settings/language"), language); + + if (language == QLatin1String("Polish")) { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250")); + } else if (language == QLatin1String("Russian")) { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251")); + } else { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); + } +} + +bool Launcher::SettingsPage::loadSettings() +{ + QString language(mLauncherSettings.value(QLatin1String("Settings/language"))); + + int index = languageComboBox->findText(language); + + if (index != -1) + languageComboBox->setCurrentIndex(index); + + return true; +} diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index d43141fc4..846f5681c 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -10,6 +10,7 @@ #include "maindialog.hpp" +namespace Files { struct ConfigurationManager; } namespace Config { class GameSettings; class LauncherSettings; } @@ -22,13 +23,14 @@ namespace Launcher Q_OBJECT public: - SettingsPage( Config::GameSettings &gameSettings, + SettingsPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, MainDialog *parent = 0); void saveSettings(); bool loadSettings(); private slots: + void on_wizardButton_clicked(); void on_importerButton_clicked(); void on_browseButton_clicked(); @@ -42,9 +44,12 @@ namespace Launcher void updateOkButton(const QString &text); private: + Process::ProcessInvoker *mWizardInvoker; Process::ProcessInvoker *mImporterInvoker; + Files::ConfigurationManager &mCfgMgr; + Config::GameSettings &mGameSettings; Config::LauncherSettings &mLauncherSettings; diff --git a/apps/launcher/utils/textinputdialog.cpp b/apps/launcher/utils/textinputdialog.cpp index 74e40a81c..b47fdb6e6 100644 --- a/apps/launcher/utils/textinputdialog.cpp +++ b/apps/launcher/utils/textinputdialog.cpp @@ -14,7 +14,7 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & mButtonBox = new QDialogButtonBox(this); mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); -// mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); + mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); QLabel *label = new QLabel(this); label->setText(text); From 5d94cb112f0574a95f79f2773dadc693f1c5fc7f Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 17 Apr 2014 02:15:06 +0200 Subject: [PATCH 53/69] Fixed segfault bug due to accessing members of unitialized struct --- apps/wizard/componentselectionpage.cpp | 12 ++++---- apps/wizard/existinginstallationpage.cpp | 4 +-- apps/wizard/installationpage.cpp | 10 +++---- apps/wizard/languageselectionpage.cpp | 28 +++++++++++------- apps/wizard/mainwizard.cpp | 37 +++++++++++++++++++----- apps/wizard/mainwizard.hpp | 7 ++++- 6 files changed, 65 insertions(+), 33 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 19a07438d..ac084616c 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -74,7 +74,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(bloodmoonItem); } else { - if (mWizard->mInstallations[path]->hasMorrowind) { + if (mWizard->mInstallations[path].hasMorrowind) { morrowindItem->setText(tr("Morrowind\t\t(installed)")); morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); morrowindItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -85,7 +85,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(morrowindItem); - if (mWizard->mInstallations[path]->hasTribunal) { + if (mWizard->mInstallations[path].hasTribunal) { tribunalItem->setText(tr("Tribunal\t\t(installed)")); tribunalItem->setFlags(tribunalItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); tribunalItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -96,7 +96,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(tribunalItem); - if (mWizard->mInstallations[path]->hasBloodmoon) { + if (mWizard->mInstallations[path].hasBloodmoon) { bloodmoonItem->setText(tr("Bloodmoon\t\t(installed)")); bloodmoonItem->setFlags(bloodmoonItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); bloodmoonItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -114,12 +114,12 @@ bool Wizard::ComponentSelectionPage::validatePage() QStringList components(field(QLatin1String("installation.components")).toStringList()); QString path(field(QLatin1String("installation.path")).toString()); - qDebug() << components << path << mWizard->mInstallations[path]; +// qDebug() << components << path << mWizard->mInstallations[path]; if (field(QLatin1String("installation.new")).toBool() == false) { if (components.contains(QLatin1String("Tribunal")) && !components.contains(QLatin1String("Bloodmoon"))) { - if (mWizard->mInstallations[path]->hasBloodmoon) + if (mWizard->mInstallations[path].hasBloodmoon) { QMessageBox msgBox; msgBox.setWindowTitle(tr("About to install Tribunal after Bloodmoon")); @@ -136,7 +136,7 @@ bool Wizard::ComponentSelectionPage::validatePage() if (msgBox.clickedButton() == reinstallButton) { // Force reinstallation - mWizard->mInstallations[path]->hasBloodmoon = false; + mWizard->mInstallations[path].hasBloodmoon = false; QList items = componentsList->findItems(QLatin1String("Bloodmoon"), Qt::MatchStartsWith); foreach (QListWidgetItem *item, items) { diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 66edc1e8a..0cbdce673 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -47,7 +47,7 @@ bool Wizard::ExistingInstallationPage::validatePage() // Or failed to be detected due to the target being a symlink QString path(field(QLatin1String("installation.path")).toString()); - QFile file(mWizard->mInstallations[path]->iniPath); + QFile file(mWizard->mInstallations[path].iniPath); if (!file.exists()) { QMessageBox msgBox; @@ -78,7 +78,7 @@ bool Wizard::ExistingInstallationPage::validatePage() // A proper Morrowind.ini was selected, set it QFileInfo info(iniFile); - mWizard->mInstallations[path]->iniPath = info.absoluteFilePath(); + mWizard->mInstallations[path].iniPath = info.absoluteFilePath(); } return true; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index a0d3dfd21..f59b6d640 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -36,11 +36,11 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setMaximum((components.count() * 100)); } else { if (components.contains(QLatin1String("Tribunal")) - && !mWizard->mInstallations[path]->hasTribunal) + && !mWizard->mInstallations[path].hasTribunal) installProgressBar->setMaximum(100); if (components.contains(QLatin1String("Bloodmoon")) - && !mWizard->mInstallations[path]->hasBloodmoon) + && !mWizard->mInstallations[path].hasBloodmoon) installProgressBar->setMaximum(installProgressBar->maximum() + 100); } @@ -104,15 +104,15 @@ void Wizard::InstallationPage::startInstallation() mUnshield->setInstallComponent(Wizard::Component_Morrowind, false); if (components.contains(QLatin1String("Tribunal")) - && !mWizard->mInstallations[path]->hasTribunal) + && !mWizard->mInstallations[path].hasTribunal) mUnshield->setInstallComponent(Wizard::Component_Tribunal, true); if (components.contains(QLatin1String("Bloodmoon")) - && !mWizard->mInstallations[path]->hasBloodmoon) + && !mWizard->mInstallations[path].hasBloodmoon) mUnshield->setInstallComponent(Wizard::Component_Bloodmoon, true); // Set the location of the Morrowind.ini to update - mUnshield->setIniPath(mWizard->mInstallations[path]->iniPath); + mUnshield->setIniPath(mWizard->mInstallations[path].iniPath); mUnshield->setupSettings(); } diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index 7f97e6918..fbd08ffcd 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -2,6 +2,8 @@ #include "mainwizard.hpp" +#include + Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : QWizardPage(wizard), mWizard(wizard) @@ -27,18 +29,22 @@ void Wizard::LanguageSelectionPage::initializePage() int Wizard::LanguageSelectionPage::nextId() const { - // Check if we have to install something - QString path(field(QLatin1String("installation.path")).toString()); - - if (path.isEmpty()) + if (field(QLatin1String("installation.new")).toBool() == true) { return MainWizard::Page_ComponentSelection; - - if (mWizard->mInstallations[path]->hasMorrowind == true && - mWizard->mInstallations[path]->hasTribunal == true && - mWizard->mInstallations[path]->hasBloodmoon == true) - { - return MainWizard::Page_Import; } else { - return MainWizard::Page_ComponentSelection; + QString path(field(QLatin1String("installation.path")).toString()); + + if (path.isEmpty()) + return MainWizard::Page_ComponentSelection; + + // Check if we have to install something + if (mWizard->mInstallations[path].hasMorrowind == true && + mWizard->mInstallations[path].hasTribunal == true && + mWizard->mInstallations[path].hasBloodmoon == true) + { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_ComponentSelection; + } } } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index fa27b548c..2a4d9f70c 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -246,23 +246,29 @@ void Wizard::MainWizard::runSettingsImporter() arguments.append(QLatin1String("--cfg")); arguments.append(userPath + QLatin1String("openmw.cfg")); - ProcessInvoker invoker(this); + ProcessInvoker *invoker = new ProcessInvoker(this); - if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) + if (!invoker->startProcess(QLatin1String("mwiniimport"), arguments, false)) return qApp->quit();; + connect(invoker->getProcess(), SIGNAL(started()), + this, SLOT(importerStarted())); + + connect(invoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(importerFinished(int,QProcess::ExitStatus))); + // Re-read the game settings - setupGameSettings(); + // setupGameSettings(); } void Wizard::MainWizard::addInstallation(const QString &path) { qDebug() << "add installation in: " << path; - Installation* install = new Installation(); + Installation install;// = new Installation(); - install->hasMorrowind = findFiles(QLatin1String("Morrowind"), path); - install->hasTribunal = findFiles(QLatin1String("Tribunal"), path); - install->hasBloodmoon = findFiles(QLatin1String("Bloodmoon"), path); + install.hasMorrowind = findFiles(QLatin1String("Morrowind"), path); + install.hasTribunal = findFiles(QLatin1String("Tribunal"), path); + install.hasBloodmoon = findFiles(QLatin1String("Bloodmoon"), path); // Try to autodetect the Morrowind.ini location QDir dir(path); @@ -276,7 +282,7 @@ void Wizard::MainWizard::addInstallation(const QString &path) } if (file.exists()) - install->iniPath = file.fileName(); + install.iniPath = file.fileName(); mInstallations.insert(QDir::toNativeSeparators(path), install); @@ -301,6 +307,21 @@ void Wizard::MainWizard::setupPages() setStartId(Page_Intro); } +void Wizard::MainWizard::importerStarted() +{ + qDebug() << "importer started!"; +} + +void Wizard::MainWizard::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + qDebug() << "importer finished!"; + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return; + + // Re-read the settings + setupGameSettings(); +} + void Wizard::MainWizard::accept() { writeSettings(); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 63b7e62e8..ed1a38879 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -1,6 +1,7 @@ #ifndef MAINWIZARD_HPP #define MAINWIZARD_HPP +#include #include #include @@ -41,7 +42,7 @@ namespace Wizard void addInstallation(const QString &path); void runSettingsImporter(); - QMap mInstallations; + QMap mInstallations; Files::ConfigurationManager mCfgMgr; @@ -66,6 +67,10 @@ namespace Wizard QTextStream *mLog; private slots: + + void importerStarted(); + void importerFinished(int exitCode, QProcess::ExitStatus exitStatus); + void accept(); void reject(); From af883991e2d3410ec67be345c0065f3ee332dcb1 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 17 Apr 2014 02:17:18 +0200 Subject: [PATCH 54/69] Reimplemented the old new profile dialog behaviour --- apps/launcher/datafilespage.cpp | 26 +++++++++++++++++++++----- apps/launcher/datafilespage.hpp | 4 +++- apps/launcher/settingspage.cpp | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 21652a8f4..6b53d89a4 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -30,6 +30,11 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: setObjectName ("DataFilesPage"); mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); + mProfileDialog = new TextInputDialog(tr("New Profile"), tr("Profile name:"), this); + + connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), + this, SLOT(updateOkButton(QString))); + buildView(); setupDataFiles(); } @@ -212,15 +217,13 @@ void Launcher::DataFilesPage::slotProfileChanged(int index) void Launcher::DataFilesPage::on_newProfileAction_triggered() { - TextInputDialog newDialog (tr("New Profile"), tr("Profile name:"), this); - - if (newDialog.exec() != QDialog::Accepted) + if (!mProfileDialog->exec() == QDialog::Accepted) return; - QString profile = newDialog.lineEdit()->text(); + QString profile = mProfileDialog->lineEdit()->text(); if (profile.isEmpty()) - return; + return; saveSettings(); @@ -271,6 +274,19 @@ void Launcher::DataFilesPage::on_deleteProfileAction_triggered() checkForDefaultProfile(); } +void Launcher::DataFilesPage::updateOkButton(const QString &text) +{ + // We do this here because we need the profiles combobox text + if (text.isEmpty()) { + mProfileDialog->setOkButtonEnabled(false); + return; + } + + (ui.profilesComboBox->findText(text) == -1) + ? mProfileDialog->setOkButtonEnabled(true) + : mProfileDialog->setOkButtonEnabled(false); +} + void Launcher::DataFilesPage::checkForDefaultProfile() { //don't allow deleting "Default" profile diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 06efdfca3..908394ae1 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -55,12 +55,14 @@ namespace Launcher void slotProfileRenamed(const QString &previous, const QString ¤t); void slotProfileDeleted(const QString &item); + void updateOkButton(const QString &text); + void on_newProfileAction_triggered(); void on_deleteProfileAction_triggered(); private: - QMenu *mContextMenu; + TextInputDialog *mProfileDialog; Files::ConfigurationManager &mCfgMgr; diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index f673917fd..8eaec95db 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -222,7 +222,7 @@ void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus void Launcher::SettingsPage::updateOkButton(const QString &text) { - // We do this here because we need the profiles combobox text + // We do this here because we need to access the profiles if (text.isEmpty()) { mProfileDialog->setOkButtonEnabled(false); return; From aa07a33906bf4b75c27dba1394b7c3d6b3870173 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 18 Apr 2014 13:17:37 +0200 Subject: [PATCH 55/69] Fixed most of the memory leaks and started working on correctly exiting the thread --- apps/launcher/maindialog.cpp | 49 +++++++------ apps/launcher/maindialog.hpp | 6 ++ apps/launcher/settingspage.cpp | 10 ++- apps/launcher/settingspage.hpp | 1 + apps/wizard/componentselectionpage.cpp | 7 +- apps/wizard/componentselectionpage.hpp | 2 +- apps/wizard/conclusionpage.cpp | 7 +- apps/wizard/conclusionpage.hpp | 2 +- apps/wizard/existinginstallationpage.cpp | 17 ++++- apps/wizard/existinginstallationpage.hpp | 5 +- apps/wizard/importpage.cpp | 7 +- apps/wizard/importpage.hpp | 2 +- apps/wizard/installationpage.cpp | 90 ++++++++++++++---------- apps/wizard/installationpage.hpp | 3 +- apps/wizard/installationtargetpage.cpp | 7 +- apps/wizard/installationtargetpage.hpp | 2 +- apps/wizard/intropage.cpp | 7 +- apps/wizard/intropage.hpp | 2 +- apps/wizard/languageselectionpage.cpp | 7 +- apps/wizard/languageselectionpage.hpp | 2 +- apps/wizard/mainwizard.cpp | 80 +++++++++++++-------- apps/wizard/mainwizard.hpp | 7 +- apps/wizard/methodselectionpage.cpp | 7 +- apps/wizard/methodselectionpage.hpp | 2 +- apps/wizard/unshield/unshieldworker.cpp | 23 +++++- apps/wizard/unshield/unshieldworker.hpp | 4 ++ components/process/processinvoker.cpp | 16 ++--- components/process/processinvoker.hpp | 2 +- files/ui/wizard/importpage.ui | 7 +- 29 files changed, 246 insertions(+), 137 deletions(-) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 9c6e33e1b..918cd3d41 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -1,7 +1,6 @@ #include "maindialog.hpp" #include -#include #include #include @@ -35,27 +34,10 @@ using namespace Process; Launcher::MainDialog::MainDialog(QWidget *parent) : mGameSettings(mCfgMgr), QMainWindow (parent) { - // Install the stylesheet font - QFile file; - QFontDatabase fontDatabase; - - const QStringList fonts = fontDatabase.families(); - - // Check if the font is installed - if (!fonts.contains("EB Garamond")) { - - QString font = QString::fromStdString(mCfgMgr.getGlobalDataPath().string()) + QString("resources/mygui/EBGaramond-Regular.ttf"); - file.setFileName(font); - - if (!file.exists()) { - font = QString::fromStdString(mCfgMgr.getLocalPath().string()) + QString("resources/mygui/EBGaramond-Regular.ttf"); - } - - fontDatabase.addApplicationFont(font); - } - setupUi(this); + mGameInvoker = new ProcessInvoker(); + iconWidget->setViewMode(QListView::IconMode); iconWidget->setWrapping(false); iconWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Just to be sure @@ -94,9 +76,33 @@ Launcher::MainDialog::MainDialog(QWidget *parent) QLatin1String("hh:mm:ss")).toString(Qt::SystemLocaleShortDate))); } + // Install the stylesheet font + QFile file; + QFontDatabase fontDatabase; + + const QStringList fonts = fontDatabase.families(); + + // Check if the font is installed + if (!fonts.contains("EB Garamond")) { + + QString font = QString::fromStdString(mCfgMgr.getGlobalDataPath().string()) + QString("resources/mygui/EBGaramond-Regular.ttf"); + file.setFileName(font); + + if (!file.exists()) { + font = QString::fromStdString(mCfgMgr.getLocalPath().string()) + QString("resources/mygui/EBGaramond-Regular.ttf"); + } + + fontDatabase.addApplicationFont(font); + } + createIcons(); } +Launcher::MainDialog::~MainDialog() +{ + delete mGameInvoker; +} + void Launcher::MainDialog::createIcons() { if (!QIcon::hasThemeIcon("document-new")) @@ -801,8 +807,7 @@ void Launcher::MainDialog::play() } // Launch the game detached - ProcessInvoker invoker(this); - if (invoker.startProcess(QLatin1String("openmw"), true)) + if (mGameInvoker->startProcess(QLatin1String("openmw"), true)) qApp->quit(); } diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index 7f468de9c..fefbaecaf 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -5,6 +5,9 @@ #ifndef Q_MOC_RUN #include #endif + +#include + #include #include @@ -36,6 +39,8 @@ namespace Launcher public: explicit MainDialog(QWidget *parent = 0); + ~MainDialog(); + bool setup(); bool showFirstRunDialog(); @@ -67,6 +72,7 @@ namespace Launcher DataFilesPage *mDataFilesPage; SettingsPage *mSettingsPage; + Process::ProcessInvoker *mGameInvoker; Files::ConfigurationManager mCfgMgr; diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 8eaec95db..fe9369769 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -36,8 +36,8 @@ Launcher::SettingsPage::SettingsPage(Files::ConfigurationManager &cfg, languageComboBox->addItems(languages); - mWizardInvoker = new ProcessInvoker(this); - mImporterInvoker = new ProcessInvoker(this); + mWizardInvoker = new ProcessInvoker(); + mImporterInvoker = new ProcessInvoker(); connect(mWizardInvoker->getProcess(), SIGNAL(started()), this, SLOT(wizardStarted())); @@ -85,6 +85,12 @@ Launcher::SettingsPage::SettingsPage(Files::ConfigurationManager &cfg, loadSettings(); } +Launcher::SettingsPage::~SettingsPage() +{ + delete mWizardInvoker; + delete mImporterInvoker; +} + void Launcher::SettingsPage::on_wizardButton_clicked() { saveSettings(); diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index 846f5681c..124c80600 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -25,6 +25,7 @@ namespace Launcher public: SettingsPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, MainDialog *parent = 0); + ~SettingsPage(); void saveSettings(); bool loadSettings(); diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index ac084616c..d372f677d 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -7,10 +7,11 @@ #include "mainwizard.hpp" -Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::ComponentSelectionPage::ComponentSelectionPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); setCommitPage(true); diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index ed007fd08..ca4347108 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - ComponentSelectionPage(MainWizard *wizard); + ComponentSelectionPage(QWidget *parent); int nextId() const; virtual bool validatePage(); diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 4407ac547..87154732a 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -4,10 +4,11 @@ #include "mainwizard.hpp" -Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::ConclusionPage::ConclusionPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); setPixmap(QWizard::WatermarkPixmap, QPixmap(QLatin1String(":/images/intropage-background.png"))); } diff --git a/apps/wizard/conclusionpage.hpp b/apps/wizard/conclusionpage.hpp index e050684e1..0e9abed72 100644 --- a/apps/wizard/conclusionpage.hpp +++ b/apps/wizard/conclusionpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - ConclusionPage(MainWizard *wizard); + ConclusionPage(QWidget *parent); int nextId() const; diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 0cbdce673..7c5d10a80 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -8,16 +8,27 @@ #include "mainwizard.hpp" -Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::ExistingInstallationPage::ExistingInstallationPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); +} + +void Wizard::ExistingInstallationPage::initializePage() +{ QListWidgetItem *emptyItem = new QListWidgetItem(tr("No existing installations detected")); emptyItem->setFlags(Qt::NoItemFlags); installationsList->addItem(emptyItem); + // Test + if (mWizard->mInstallations.isEmpty()) { + qDebug() << "crashy crash"; + return; + } + // Add the available installation paths QStringList paths(mWizard->mInstallations.keys()); diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index 54ee8d2e2..601295464 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - ExistingInstallationPage(MainWizard *wizard); + ExistingInstallationPage(QWidget *parent); int nextId() const; virtual bool isComplete() const; @@ -27,6 +27,9 @@ namespace Wizard private: MainWizard *mWizard; + protected: + void initializePage(); + }; } diff --git a/apps/wizard/importpage.cpp b/apps/wizard/importpage.cpp index b49105faa..71c5544e6 100644 --- a/apps/wizard/importpage.cpp +++ b/apps/wizard/importpage.cpp @@ -2,10 +2,11 @@ #include "mainwizard.hpp" -Wizard::ImportPage::ImportPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::ImportPage::ImportPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); registerField(QLatin1String("installation.import-settings"), importCheckBox); diff --git a/apps/wizard/importpage.hpp b/apps/wizard/importpage.hpp index 2eae08531..386cd59af 100644 --- a/apps/wizard/importpage.hpp +++ b/apps/wizard/importpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - ImportPage(MainWizard *wizard); + ImportPage(QWidget *parent); int nextId() const; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index f59b6d640..b76aeea22 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -9,48 +9,14 @@ #include "mainwizard.hpp" #include "inisettings.hpp" -Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::InstallationPage::InstallationPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); mFinished = false; -} - -void Wizard::InstallationPage::initializePage() -{ - QString path(field(QLatin1String("installation.path")).toString()); - QStringList components(field(QLatin1String("installation.components")).toStringList()); - - logTextEdit->appendPlainText(QString("Installing to %1").arg(path)); - logTextEdit->appendPlainText(QString("Installing %1.").arg(components.join(", "))); - - installProgressBar->setMinimum(0); - - // Set the progressbar maximum to a multiple of 100 - // That way installing all three components would yield 300% - // When one component is done the bar will be filled by 33% - - if (field(QLatin1String("installation.new")).toBool() == true) { - installProgressBar->setMaximum((components.count() * 100)); - } else { - if (components.contains(QLatin1String("Tribunal")) - && !mWizard->mInstallations[path].hasTribunal) - installProgressBar->setMaximum(100); - - if (components.contains(QLatin1String("Bloodmoon")) - && !mWizard->mInstallations[path].hasBloodmoon) - installProgressBar->setMaximum(installProgressBar->maximum() + 100); - } - - startInstallation(); -} - -void Wizard::InstallationPage::startInstallation() -{ - QStringList components(field(QLatin1String("installation.components")).toStringList()); - QString path(field(QLatin1String("installation.path")).toString()); mThread = new QThread(); mUnshield = new UnshieldWorker(); @@ -88,6 +54,54 @@ void Wizard::InstallationPage::startInstallation() connect(mUnshield, SIGNAL(requestFileDialog(Wizard::Component)), this, SLOT(showFileDialog(Wizard::Component)), Qt::QueuedConnection); +} + +Wizard::InstallationPage::~InstallationPage() +{ + qDebug() << "stop!"; + + if (mThread->isRunning()) { + mUnshield->stopWorker(); + mThread->wait(); + } + + delete mUnshield; + delete mThread; +} + +void Wizard::InstallationPage::initializePage() +{ + QString path(field(QLatin1String("installation.path")).toString()); + QStringList components(field(QLatin1String("installation.components")).toStringList()); + + logTextEdit->appendPlainText(QString("Installing to %1").arg(path)); + logTextEdit->appendPlainText(QString("Installing %1.").arg(components.join(", "))); + + installProgressBar->setMinimum(0); + + // Set the progressbar maximum to a multiple of 100 + // That way installing all three components would yield 300% + // When one component is done the bar will be filled by 33% + + if (field(QLatin1String("installation.new")).toBool() == true) { + installProgressBar->setMaximum((components.count() * 100)); + } else { + if (components.contains(QLatin1String("Tribunal")) + && !mWizard->mInstallations[path].hasTribunal) + installProgressBar->setMaximum(100); + + if (components.contains(QLatin1String("Bloodmoon")) + && !mWizard->mInstallations[path].hasBloodmoon) + installProgressBar->setMaximum(installProgressBar->maximum() + 100); + } + + startInstallation(); +} + +void Wizard::InstallationPage::startInstallation() +{ + QStringList components(field(QLatin1String("installation.components")).toStringList()); + QString path(field(QLatin1String("installation.path")).toString()); if (field(QLatin1String("installation.new")).toBool() == true) { diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 5e58477be..822cd21cd 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -19,7 +19,8 @@ namespace Wizard { Q_OBJECT public: - InstallationPage(MainWizard *wizard); + InstallationPage(QWidget *parent); + ~InstallationPage(); int nextId() const; virtual bool isComplete() const; diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index d89d54a00..3a179a2cb 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -6,11 +6,12 @@ #include "mainwizard.hpp" -Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard, const Files::ConfigurationManager &cfg) : - QWizardPage(wizard), - mWizard(wizard), +Wizard::InstallationTargetPage::InstallationTargetPage(QWidget *parent, const Files::ConfigurationManager &cfg) : + QWizardPage(parent), mCfgMgr(cfg) { + mWizard = qobject_cast(parent); + setupUi(this); registerField(QLatin1String("installation.path*"), targetLineEdit); diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp index 0711c1c05..ca3b505b7 100644 --- a/apps/wizard/installationtargetpage.hpp +++ b/apps/wizard/installationtargetpage.hpp @@ -18,7 +18,7 @@ namespace Wizard { Q_OBJECT public: - InstallationTargetPage(MainWizard *wizard, const Files::ConfigurationManager &cfg); + InstallationTargetPage(QWidget *parent, const Files::ConfigurationManager &cfg); int nextId() const; virtual bool validatePage(); diff --git a/apps/wizard/intropage.cpp b/apps/wizard/intropage.cpp index 91e7d5dc0..0a98ae5f3 100644 --- a/apps/wizard/intropage.cpp +++ b/apps/wizard/intropage.cpp @@ -2,10 +2,11 @@ #include "mainwizard.hpp" -Wizard::IntroPage::IntroPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::IntroPage::IntroPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); setPixmap(QWizard::WatermarkPixmap, QPixmap(QLatin1String(":/images/intropage-background.png"))); } diff --git a/apps/wizard/intropage.hpp b/apps/wizard/intropage.hpp index 78bac3155..4ad9b4111 100644 --- a/apps/wizard/intropage.hpp +++ b/apps/wizard/intropage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - IntroPage(MainWizard *wizard); + IntroPage(QWidget *parent); int nextId() const; diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index fbd08ffcd..0d5132f5b 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -4,10 +4,11 @@ #include -Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::LanguageSelectionPage::LanguageSelectionPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); registerField(QLatin1String("installation.language"), languageComboBox); diff --git a/apps/wizard/languageselectionpage.hpp b/apps/wizard/languageselectionpage.hpp index 3c17514f9..abc3edb98 100644 --- a/apps/wizard/languageselectionpage.hpp +++ b/apps/wizard/languageselectionpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - LanguageSelectionPage(MainWizard *wizard); + LanguageSelectionPage(QWidget *parent); int nextId() const; diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 2a4d9f70c..92743417e 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -1,7 +1,5 @@ #include "mainwizard.hpp" -#include - #include #include @@ -25,7 +23,9 @@ using namespace Process; Wizard::MainWizard::MainWizard(QWidget *parent) : mGameSettings(mCfgMgr), - QWizard(parent) + QWizard(parent), + mError(false), + mInstallations() { #ifndef Q_OS_MAC setWizardStyle(QWizard::ModernStyle); @@ -37,11 +37,26 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setWindowIcon(QIcon(QLatin1String(":/images/openmw-wizard.png"))); setMinimumWidth(550); + // This prevents initializePage() being called multiple times + setOption(QWizard::IndependentPages); + // Set the property for comboboxes to the text instead of index setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); + mImporterInvoker = new ProcessInvoker(); + + connect(mImporterInvoker->getProcess(), SIGNAL(started()), + this, SLOT(importerStarted())); + + connect(mImporterInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(importerFinished(int,QProcess::ExitStatus))); + + mLogError = tr("

Could not open %1 for writing

\ +

Please make sure you have the right permissions \ + and try again.

"); + setupLog(); setupGameSettings(); setupLauncherSettings(); @@ -49,44 +64,58 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setupPages(); } +Wizard::MainWizard::~MainWizard() +{ + delete mImporterInvoker; +} + void Wizard::MainWizard::setupLog() { QString logPath(QString::fromUtf8(mCfgMgr.getLogPath().string().c_str())); logPath.append(QLatin1String("wizard.log")); - QString message(tr("

Could not open %1 for writing

\ -

Please make sure you have the right permissions \ - and try again.

")); - - QFile *file = new QFile(logPath); + QFile file(logPath); - if (!file->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) { + if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Error opening Wizard log file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(message.arg(file->fileName())); + msgBox.setText(mLogError.arg(file.fileName())); msgBox.exec(); return qApp->quit(); } - qDebug() << logPath; - - mLog = new QTextStream(file); - mLog->setCodec(QTextCodec::codecForName("UTF-8")); + addLogText(QString("Started OpenMW Wizard on %1").arg(QDateTime::currentDateTime().toString())); - //addLogText(QLatin1String("test test 123 test")); + qDebug() << logPath; } void Wizard::MainWizard::addLogText(const QString &text) { + QString logPath(QString::fromUtf8(mCfgMgr.getLogPath().string().c_str())); + logPath.append(QLatin1String("wizard.log")); + + QFile file(logPath); - qDebug() << "AddLogText called! " << text; - if (!text.isEmpty()) { - qDebug() << "logging " << text; - *mLog << text << endl; + if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening Wizard log file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(mLogError.arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); } + if (!file.isSequential()) + file.seek(file.size()); + + QTextStream out(&file); + + if (!text.isEmpty()) + out << text << endl; + // file.close(); } @@ -246,16 +275,8 @@ void Wizard::MainWizard::runSettingsImporter() arguments.append(QLatin1String("--cfg")); arguments.append(userPath + QLatin1String("openmw.cfg")); - ProcessInvoker *invoker = new ProcessInvoker(this); - - if (!invoker->startProcess(QLatin1String("mwiniimport"), arguments, false)) - return qApp->quit();; - - connect(invoker->getProcess(), SIGNAL(started()), - this, SLOT(importerStarted())); - - connect(invoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), - this, SLOT(importerFinished(int,QProcess::ExitStatus))); + if (!mImporterInvoker->startProcess(QLatin1String("mwiniimport"), arguments, false)) + return qApp->quit(); // Re-read the game settings // setupGameSettings(); @@ -305,6 +326,7 @@ void Wizard::MainWizard::setupPages() setPage(Page_Import, new ImportPage(this)); setPage(Page_Conclusion, new ConclusionPage(this)); setStartId(Page_Intro); + } void Wizard::MainWizard::importerStarted() diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index ed1a38879..6b8c4931f 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include #include @@ -37,6 +39,7 @@ namespace Wizard }; MainWizard(QWidget *parent = 0); + ~MainWizard(); bool findFiles(const QString &name, const QString &path); void addInstallation(const QString &path); @@ -46,6 +49,8 @@ namespace Wizard Files::ConfigurationManager mCfgMgr; + Process::ProcessInvoker *mImporterInvoker; + bool mError; public slots: @@ -64,7 +69,7 @@ namespace Wizard Config::GameSettings mGameSettings; Config::LauncherSettings mLauncherSettings; - QTextStream *mLog; + QString mLogError; private slots: diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index b1483ac3a..d7c64f3b0 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -2,10 +2,11 @@ #include #include "mainwizard.hpp" -Wizard::MethodSelectionPage::MethodSelectionPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); registerField(QLatin1String("installation.new"), newLocationRadioButton); diff --git a/apps/wizard/methodselectionpage.hpp b/apps/wizard/methodselectionpage.hpp index de34ff555..60941c651 100644 --- a/apps/wizard/methodselectionpage.hpp +++ b/apps/wizard/methodselectionpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - MethodSelectionPage(MainWizard *wizard); + MethodSelectionPage(QWidget *parent); int nextId() const; diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 81282f547..5fa043347 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -35,6 +35,8 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : mTribunalDone = false; mBloodmoonDone = false; + mStopped = false; + qRegisterMetaType("Wizard::Component"); } @@ -42,6 +44,13 @@ Wizard::UnshieldWorker::~UnshieldWorker() { } +void Wizard::UnshieldWorker::stopWorker() +{ + mMutex.lock(); + mStopped = true; + mMutex.unlock(); +} + void Wizard::UnshieldWorker::setInstallComponent(Wizard::Component component, bool install) { QWriteLocker writeLock(&mLock); @@ -781,6 +790,13 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &d for (size_t j=group->first_file; j<=group->last_file; ++j) { + if (mStopped) { + qDebug() << "We're asked to stop!"; + + unshield_close(unshield); + return true; + } + if (unshield_file_is_valid(unshield, j)) { success = extractFile(unshield, destination, group->name, j, counter); @@ -789,6 +805,8 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &d emit error(tr("Failed to extract %1.").arg(name), tr("Complete path: %1").arg(destination + QDir::separator() + name)); + + unshield_close(unshield); return false; } @@ -892,11 +910,14 @@ bool Wizard::UnshieldWorker::findInCab(const QString &fileName, const QString &c if (unshield_file_is_valid(unshield, j)) { QString current(QString::fromUtf8(unshield_file_name(unshield, j))); - if (current.toLower() == fileName.toLower()) + if (current.toLower() == fileName.toLower()) { + unshield_close(unshield); return true; // File is found! + } } } } + unshield_close(unshield); return false; } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 2e509d116..5ea7b04ae 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -30,6 +30,8 @@ namespace Wizard UnshieldWorker(QObject *parent = 0); ~UnshieldWorker(); + void stopWorker(); + void setInstallComponent(Wizard::Component component, bool install); void setDiskPath(const QString &path); @@ -92,6 +94,8 @@ namespace Wizard bool mTribunalDone; bool mBloodmoonDone; + bool mStopped; + QString mPath; QString mIniPath; QString mDiskPath; diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp index 7ac8d3c93..44a11cc4e 100644 --- a/components/process/processinvoker.cpp +++ b/components/process/processinvoker.cpp @@ -8,10 +8,17 @@ #include #include -Process::ProcessInvoker::ProcessInvoker(QWidget *parent) +Process::ProcessInvoker::ProcessInvoker() { mProcess = new QProcess(this); + connect(mProcess, SIGNAL(error(QProcess::ProcessError)), + this, SLOT(processError(QProcess::ProcessError))); + + connect(mProcess, SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(processFinished(int,QProcess::ExitStatus))); + + mName = QString(); mArguments = QStringList(); } @@ -48,13 +55,6 @@ QProcess* Process::ProcessInvoker::getProcess() bool Process::ProcessInvoker::startProcess(const QString &name, const QStringList &arguments, bool detached) { // mProcess = new QProcess(this); - - connect(mProcess, SIGNAL(error(QProcess::ProcessError)), - this, SLOT(processError(QProcess::ProcessError))); - - connect(mProcess, SIGNAL(finished(int,QProcess::ExitStatus)), - this, SLOT(processFinished(int,QProcess::ExitStatus))); - mName = name; mArguments = arguments; diff --git a/components/process/processinvoker.hpp b/components/process/processinvoker.hpp index 07907e3b4..8fff6658c 100644 --- a/components/process/processinvoker.hpp +++ b/components/process/processinvoker.hpp @@ -13,7 +13,7 @@ namespace Process public: - ProcessInvoker(QWidget *parent = 0); + ProcessInvoker(); ~ProcessInvoker(); // void setProcessName(const QString &name); diff --git a/files/ui/wizard/importpage.ui b/files/ui/wizard/importpage.ui index 52f9ac4f2..9fd4ea500 100644 --- a/files/ui/wizard/importpage.ui +++ b/files/ui/wizard/importpage.ui @@ -6,8 +6,8 @@ 0 0 - 510 - 324 + 508 + 322
@@ -45,6 +45,9 @@ Import add-on and plugin selection + + true + From 5af12d193acfe5f0470ed947cffff1072b56daf3 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 29 May 2014 17:57:41 +0200 Subject: [PATCH 56/69] Cleaned up old wizard stuff from launcher --- CMakeLists.txt | 16 +- apps/launcher/CMakeLists.txt | 24 +- apps/launcher/main.cpp | 9 +- apps/launcher/maindialog.cpp | 393 +++++---------- apps/launcher/maindialog.hpp | 7 + apps/launcher/settingspage.cpp | 6 +- apps/launcher/unshieldthread.cpp | 521 -------------------- apps/launcher/unshieldthread.hpp | 58 --- apps/launcher/utils/checkablemessagebox.cpp | 258 ---------- apps/launcher/utils/checkablemessagebox.hpp | 116 ----- 10 files changed, 144 insertions(+), 1264 deletions(-) delete mode 100644 apps/launcher/unshieldthread.cpp delete mode 100644 apps/launcher/unshieldthread.hpp delete mode 100644 apps/launcher/utils/checkablemessagebox.cpp delete mode 100644 apps/launcher/utils/checkablemessagebox.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a9f618d1..66746b22c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -406,7 +406,7 @@ IF(NOT WIN32 AND NOT APPLE) IF(BUILD_WIZARD) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/wizard" DESTINATION "${BINDIR}" ) ENDIF(BUILD_WIZARD) - + # Install licenses INSTALL(FILES "DejaVu Font License.txt" DESTINATION "${LICDIR}" ) @@ -460,7 +460,7 @@ if(WIN32) IF(BUILD_WIZARD) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/openmw-wizard.exe" DESTINATION ".") ENDIF(BUILD_WIZARD) - + INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION ".") @@ -543,13 +543,6 @@ if (BUILD_ESMTOOL) endif() if (BUILD_LAUNCHER) - if(NOT WIN32) - find_package(LIBUNSHIELD REQUIRED) - if(NOT LIBUNSHIELD_FOUND) - message(SEND_ERROR "Failed to find libunshield") - endif(NOT LIBUNSHIELD_FOUND) - endif(NOT WIN32) - add_subdirectory( apps/launcher ) endif() @@ -562,6 +555,11 @@ if (BUILD_OPENCS) endif() if (BUILD_WIZARD) + find_package(LIBUNSHIELD REQUIRED) + if(NOT LIBUNSHIELD_FOUND) + message(FATAL_ERROR "Failed to find Unshield library") + endif(NOT LIBUNSHIELD_FOUND) + add_subdirectory(apps/wizard) endif() diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index b777856e8..eb11cab49 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -9,16 +9,12 @@ set(LAUNCHER settings/graphicssettings.cpp - utils/checkablemessagebox.cpp utils/profilescombobox.cpp utils/textinputdialog.cpp utils/lineedit.cpp ${CMAKE_SOURCE_DIR}/files/launcher/launcher.rc ) -if(NOT WIN32) - LIST(APPEND LAUNCHER unshieldthread.cpp) -endif(NOT WIN32) set(LAUNCHER_HEADER datafilespage.hpp @@ -30,15 +26,10 @@ set(LAUNCHER_HEADER settings/graphicssettings.hpp - utils/checkablemessagebox.hpp utils/profilescombobox.hpp utils/textinputdialog.hpp utils/lineedit.hpp ) -if(NOT WIN32) - LIST(APPEND LAUNCHER_HEADER unshieldthread.hpp) -endif(NOT WIN32) - # Headers that must be pre-processed set(LAUNCHER_HEADER_MOC @@ -50,17 +41,11 @@ set(LAUNCHER_HEADER_MOC settingspage.hpp utils/textinputdialog.hpp - utils/checkablemessagebox.hpp utils/profilescombobox.hpp utils/lineedit.hpp ) -if(NOT WIN32) - LIST(APPEND LAUNCHER_HEADER_MOC unshieldthread.hpp) -endif(NOT WIN32) - - set(LAUNCHER_UI ${CMAKE_SOURCE_DIR}/files/ui/datafilespage.ui ${CMAKE_SOURCE_DIR}/files/ui/graphicspage.ui @@ -119,19 +104,12 @@ target_link_libraries(omwlauncher ${QT_LIBRARIES} components ) -if(NOT WIN32) - target_link_libraries(omwlauncher - ${LIBUNSHIELD_LIBRARY} - ) -endif(NOT WIN32) - - if(DPKG_PROGRAM) INSTALL(TARGETS omwlauncher RUNTIME DESTINATION games COMPONENT omwlauncher) endif() -if (BUILD_WITH_CODE_COVERAGE) +if(BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(omwlauncher gcov) endif() diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index fabf77d90..67320bff0 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -52,11 +52,12 @@ int main(int argc, char *argv[]) Launcher::MainDialog mainWin; - if (mainWin.setup()) { - mainWin.show(); - } else { + if (!mainWin.setup()) { return 0; - } + //mainWin.show(); + }/* else { + return 0; + }*/ int returnValue = app.exec(); SDL_Quit(); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 3f9f27554..76b9ad3c5 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -16,14 +17,6 @@ #include -#ifndef WIN32 - #include "unshieldthread.hpp" -#endif - -#include "textslotmsgbox.hpp" - -#include "utils/checkablemessagebox.hpp" - #include "playpage.hpp" #include "graphicspage.hpp" #include "datafilespage.hpp" @@ -56,6 +49,10 @@ Launcher::MainDialog::MainDialog(QWidget *parent) setupUi(this); mGameInvoker = new ProcessInvoker(); + mWizardInvoker = new ProcessInvoker(); + + connect(mWizardInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(wizardFinished(int,QProcess::ExitStatus))); iconWidget->setViewMode(QListView::IconMode); iconWidget->setWrapping(false); @@ -83,13 +80,13 @@ Launcher::MainDialog::MainDialog(QWidget *parent) if (!revision.isEmpty() && !tag.isEmpty()) { if (revision == tag) { - versionLabel->setText(tr("OpenMW %0 release").arg(OPENMW_VERSION)); + versionLabel->setText(tr("OpenMW %1 release").arg(OPENMW_VERSION)); } else { - versionLabel->setText(tr("OpenMW development (%0)").arg(revision.left(10))); + versionLabel->setText(tr("OpenMW development (%1)").arg(revision.left(10))); } // Add the compile date and time - versionLabel->setToolTip(tr("Compiled on %0 %1").arg(QLocale(QLocale::C).toDate(QString(__DATE__).simplified(), + versionLabel->setToolTip(tr("Compiled on %1 %2").arg(QLocale(QLocale::C).toDate(QString(__DATE__).simplified(), QLatin1String("MMM d yyyy")).toString(Qt::SystemLocaleLongDate), QLocale(QLocale::C).toTime(QString(__TIME__).simplified(), QLatin1String("hh:mm:ss")).toString(Qt::SystemLocaleShortDate))); @@ -101,6 +98,7 @@ Launcher::MainDialog::MainDialog(QWidget *parent) Launcher::MainDialog::~MainDialog() { delete mGameInvoker; + delete mWizardInvoker; } void Launcher::MainDialog::createIcons() @@ -167,102 +165,32 @@ void Launcher::MainDialog::createPages() bool Launcher::MainDialog::showFirstRunDialog() { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("First run")); + msgBox.setIcon(QMessageBox::Question); + msgBox.setStandardButtons(QMessageBox::NoButton); + msgBox.setText(tr("

Welcome to OpenMW!

\ +

It is recommended to run the Installation Wizard.

\ +

The Wizard will let you select an existing Morrowind installation, \ + or install Morrowind for OpenMW to use.

")); + QAbstractButton *wizardButton = + msgBox.addButton(tr("Run &Installation Wizard"), QMessageBox::AcceptRole); // ActionRole doesn't work?! + QAbstractButton *skipButton = + msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); + Q_UNUSED(skipButton); // Surpress compiler unused warning + + msgBox.exec(); + + if (msgBox.clickedButton() == wizardButton) + { + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { + return false; + } else { + return true; + } + } -// CheckableMessageBox msgBox(this); -// msgBox.setWindowTitle(tr("Morrowind installation detected")); - -// QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion); -// int size = QApplication::style()->pixelMetric(QStyle::PM_MessageBoxIconSize); -// msgBox.setIconPixmap(icon.pixmap(size, size)); - -// QAbstractButton *importerButton = -// msgBox.addButton(tr("Import"), QDialogButtonBox::AcceptRole); // ActionRole doesn't work?! -// QAbstractButton *skipButton = -// msgBox.addButton(tr("Skip"), QDialogButtonBox::RejectRole); - -// Q_UNUSED(skipButton); // Surpress compiler unused warning - -// msgBox.setStandardButtons(QDialogButtonBox::NoButton); -// msgBox.setText(tr("
An existing Morrowind configuration was detected
\ -//
Would you like to import settings from Morrowind.ini?
\ -//
Warning: In most cases OpenMW needs these settings to run properly
")); -// msgBox.setCheckBoxText(tr("Include selected masters and plugins (creates a new profile)")); -// msgBox.exec(); - - -// if (msgBox.clickedButton() == importerButton) { - -// if (iniPaths.count() > 1) { -// // Multiple Morrowind.ini files found -// bool ok; -// QString path = QInputDialog::getItem(this, tr("Multiple configurations found"), -// tr("
There are multiple Morrowind.ini files found.

\ -// Please select the one you wish to import from:"), iniPaths, 0, false, &ok); -// if (ok && !path.isEmpty()) { -// iniPaths.clear(); -// iniPaths.append(path); -// } else { -// // Cancel was clicked -// return false; -// } -// } - -// // Create the file if it doesn't already exist, else the importer will fail -// QString path = QString::fromStdString(mCfgMgr.getUserConfigPath().string()) + QString("openmw.cfg"); -// QFile file(path); - -// if (!file.exists()) { -// if (!file.open(QIODevice::ReadWrite)) { -// // File cannot be created -// QMessageBox msgBox; -// msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); -// msgBox.setIcon(QMessageBox::Critical); -// msgBox.setStandardButtons(QMessageBox::Ok); -// msgBox.setText(tr("
Could not open or create %0 for writing

\ -// Please make sure you have the right permissions \ -// and try again.
").arg(file.fileName())); -// msgBox.exec(); -// return false; -// } - -// file.close(); -// } - -// // Construct the arguments to run the importer -// QStringList arguments; - -// if (msgBox.isChecked()) -// arguments.append(QString("--game-files")); - -// arguments.append(QString("--encoding")); -// arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); -// arguments.append(QString("--ini")); -// arguments.append(iniPaths.first()); -// arguments.append(QString("--cfg")); -// arguments.append(path); - -// ProcessInvoker invoker(this); - -// if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) -// return false; - -// // Re-read the game settings -// if (!setupGameSettings()) -// return false; - -// // Add a new profile -// if (msgBox.isChecked()) { -// mLauncherSettings.setValue(QString("Profiles/currentprofile"), QString("Imported")); -// mLauncherSettings.remove(QString("Profiles/Imported/content")); - -// QStringList contents = mGameSettings.values(QString("content")); -// foreach (const QString &content, contents) { -// mLauncherSettings.setMultiValue(QString("Profiles/Imported/content"), content); -// } -// } - -// } - + show(); return true; } @@ -277,13 +205,6 @@ bool Launcher::MainDialog::setup() if (!setupGraphicsSettings()) return false; - // Check if we need to show the importer - if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) - { - if (!showFirstRunDialog()) - return false; - } - // Now create the pages as they need the settings createPages(); @@ -292,6 +213,18 @@ bool Launcher::MainDialog::setup() return false; loadSettings(); + + // Check if we need to run the wizard + if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) + { + if (!showFirstRunDialog()) { + return false; + } else { + return true; + } + } + + show(); // Show ourselves if the wizard is not being run return true; } @@ -328,20 +261,20 @@ void Launcher::MainDialog::changePage(QListWidgetItem *current, QListWidgetItem pagesWidget->setCurrentIndex(currentIndex); - DataFilesPage *previousPage = dynamic_cast(pagesWidget->widget(previousIndex)); - DataFilesPage *currentPage = dynamic_cast(pagesWidget->widget(currentIndex)); - -// //special call to update/save data files page list view when it's displayed/hidden. -// if (previousPage) -// { -// if (previousPage->objectName() == "DataFilesPage") -// previousPage->saveSettings(); -// } -// else if (currentPage) -// { -// if (currentPage->objectName() == "DataFilesPage") -// currentPage->loadSettings(); -// } + // DataFilesPage *previousPage = dynamic_cast(pagesWidget->widget(previousIndex)); + // DataFilesPage *currentPage = dynamic_cast(pagesWidget->widget(currentIndex)); + + // //special call to update/save data files page list view when it's displayed/hidden. + // if (previousPage) + // { + // if (previousPage->objectName() == "DataFilesPage") + // previousPage->saveSettings(); + // } + // else if (currentPage) + // { + // if (currentPage->objectName() == "DataFilesPage") + // currentPage->loadSettings(); + // } } bool Launcher::MainDialog::setupLauncherSettings() @@ -363,10 +296,10 @@ bool Launcher::MainDialog::setupLauncherSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); + msgBox.setText(tr("
Could not open %0 for reading

\ + Please make sure you have the right permissions \ + and try again.
").arg(file.fileName())); + msgBox.exec(); return false; } QTextStream stream(&file); @@ -380,78 +313,6 @@ bool Launcher::MainDialog::setupLauncherSettings() return true; } -#ifndef WIN32 -bool Launcher::expansions(Launcher::UnshieldThread& cd) -{ - if(cd.BloodmoonDone()) - { - cd.Done(); - return false; - } - - QMessageBox expansionsBox; - expansionsBox.setText(QObject::tr("
Would you like to install expansions now ? (make sure you have the disc)
\ - If you want to install both Bloodmoon and Tribunal, you have to install Tribunal first.
")); - - QAbstractButton* tribunalButton = NULL; - if(!cd.TribunalDone()) - tribunalButton = expansionsBox.addButton(QObject::tr("&Tribunal"), QMessageBox::ActionRole); - - QAbstractButton* bloodmoonButton = expansionsBox.addButton(QObject::tr("&Bloodmoon"), QMessageBox::ActionRole); - QAbstractButton* noneButton = expansionsBox.addButton(QObject::tr("&None"), QMessageBox::ActionRole); - - expansionsBox.exec(); - - if(expansionsBox.clickedButton() == noneButton) - { - cd.Done(); - return false; - } - else if(expansionsBox.clickedButton() == tribunalButton) - { - - TextSlotMsgBox cdbox; - cdbox.setStandardButtons(QMessageBox::Cancel); - - QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); - QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); - - cd.SetTribunalPath( - QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select data1.hdr from Tribunal Installation CD (Tribunal/data1.hdr on GOTY CDs)"), - QDir::currentPath(), - QString(QObject::tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); - - cd.start(); - cdbox.exec(); - } - else if(expansionsBox.clickedButton() == bloodmoonButton) - { - - TextSlotMsgBox cdbox; - cdbox.setStandardButtons(QMessageBox::Cancel); - - QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); - QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); - - cd.SetBloodmoonPath( - QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select data1.hdr from Bloodmoon Installation CD (Bloodmoon/data1.hdr on GOTY CDs)"), - QDir::currentPath(), - QString(QObject::tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); - - cd.start(); - cdbox.exec(); - } - - - - return true; -} -#endif // WIN32 - bool Launcher::MainDialog::setupGameSettings() { QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); @@ -470,7 +331,7 @@ bool Launcher::MainDialog::setupGameSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + msgBox.setText(tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); msgBox.exec(); @@ -498,7 +359,7 @@ bool Launcher::MainDialog::setupGameSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + msgBox.setText(tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); msgBox.exec(); @@ -530,63 +391,35 @@ bool Launcher::MainDialog::setupGameSettings() msgBox.setWindowTitle(tr("Error detecting Morrowind installation")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Cancel); - msgBox.setText(QObject::tr("
Could not find the Data Files location

\ + msgBox.setText(tr("
Could not find the Data Files location

\ The directory containing the data files was not found.

\ Press \"Browse...\" to specify the location manually.
")); - QAbstractButton *dirSelectButton = - msgBox.addButton(QObject::tr("Browse to &Install..."), QMessageBox::ActionRole); - - #ifndef WIN32 - QAbstractButton *cdSelectButton = - msgBox.addButton(QObject::tr("Browse to &CD..."), QMessageBox::ActionRole); - #endif + QAbstractButton *browseButton = + msgBox.addButton(tr("Browse..."), QMessageBox::ActionRole); + QAbstractButton *wizardButton = + msgBox.addButton(tr("Run &Installation Wizard..."), QMessageBox::ActionRole); - msgBox.exec(); + msgBox.exec(); QString selectedFile; - if (msgBox.clickedButton() == dirSelectButton) { + if (msgBox.clickedButton() == browseButton) + { selectedFile = QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select master file"), + this, + tr("Select master file"), QDir::currentPath(), - QString(tr("Morrowind master file (*.esm)"))); + tr("Morrowind master file (*.esm)")); } - #ifndef WIN32 - else if(msgBox.clickedButton() == cdSelectButton) { - UnshieldThread cd; - - { - TextSlotMsgBox cdbox; - cdbox.setStandardButtons(QMessageBox::Cancel); - - QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); - QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); - - cd.SetMorrowindPath( - QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select data1.hdr from Morrowind Installation CD"), - QDir::currentPath(), - QString(tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); - - cd.SetOutputPath( - QFileDialog::getExistingDirectory( - NULL, - QObject::tr("Select where to extract files to"), - QDir::currentPath(), - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks).toUtf8().constData()); - - cd.start(); - cdbox.exec(); + else if (msgBox.clickedButton() == wizardButton) + { + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { + return false; + } else { + return true; } - - while(expansions(cd)); - - selectedFile = QString::fromUtf8(cd.GetMWEsmPath().c_str()); } - #endif // WIN32 if (selectedFile.isEmpty()) return false; // Cancel was clicked; @@ -616,7 +449,7 @@ bool Launcher::MainDialog::setupGraphicsSettings() msgBox.setWindowTitle(tr("Error reading OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not find settings-default.cfg

\ + msgBox.setText(tr("
Could not find settings-default.cfg

\ The problem may be due to an incomplete installation of OpenMW.
\ Reinstalling OpenMW may resolve the problem.")); msgBox.exec(); @@ -638,7 +471,7 @@ bool Launcher::MainDialog::setupGraphicsSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + msgBox.setText(tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); msgBox.exec(); @@ -705,8 +538,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not create %0

\ Please make sure you have the right permissions \ and try again.
").arg(userPath)); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } } @@ -722,8 +555,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not open or create %0 for writing

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } QTextStream stream(&file); @@ -744,8 +577,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not open or create %0 for writing

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } stream.setDevice(&file); @@ -766,8 +599,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not open or create %0 for writing

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } stream.setDevice(&file); @@ -786,26 +619,38 @@ void Launcher::MainDialog::closeEvent(QCloseEvent *event) event->accept(); } +void Launcher::MainDialog::wizardStarted() +{ + qDebug() << "wizard started!"; +} + +void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return qApp->quit(); + + reloadSettings(); + show(); +} + void Launcher::MainDialog::play() { - if (!writeSettings()) { - qApp->quit(); - return; - } + if (!writeSettings()) + return qApp->quit(); - if(!mGameSettings.hasMaster()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("No game file selected")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
You do not have a game file selected.

\ - OpenMW will not start without a game file selected.
")); - msgBox.exec(); - return; + if (!mGameSettings.hasMaster()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("No game file selected")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("
You do not have a game file selected.

\ + OpenMW will not start without a game file selected.
")); + msgBox.exec(); + return; } // Launch the game detached if (mGameInvoker->startProcess(QLatin1String("openmw"), true)) - qApp->quit(); + return qApp->quit(); } diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index fefbaecaf..0708f7002 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -2,6 +2,8 @@ #define MAINDIALOG_H #include +#include + #ifndef Q_MOC_RUN #include #endif @@ -51,6 +53,10 @@ namespace Launcher void changePage(QListWidgetItem *current, QListWidgetItem *previous); void play(); + private slots: + void wizardStarted(); + void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); + private: void createIcons(); void createPages(); @@ -73,6 +79,7 @@ namespace Launcher SettingsPage *mSettingsPage; Process::ProcessInvoker *mGameInvoker; + Process::ProcessInvoker *mWizardInvoker; Files::ConfigurationManager mCfgMgr; diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index fe9369769..45e0f72a4 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -172,6 +172,8 @@ void Launcher::SettingsPage::on_browseButton_clicked() void Launcher::SettingsPage::wizardStarted() { + mMain->hide(); // Hide the launcher + qDebug() << "wizard started!"; wizardButton->setEnabled(false); } @@ -180,10 +182,12 @@ void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus e { qDebug() << "wizard finished!"; if (exitCode != 0 || exitStatus == QProcess::CrashExit) - return; + return qApp->quit(); mMain->reloadSettings(); wizardButton->setEnabled(true); + + mMain->show(); // Show the launcher again } void Launcher::SettingsPage::importerStarted() diff --git a/apps/launcher/unshieldthread.cpp b/apps/launcher/unshieldthread.cpp deleted file mode 100644 index 3d0f2e5da..000000000 --- a/apps/launcher/unshieldthread.cpp +++ /dev/null @@ -1,521 +0,0 @@ -#include "unshieldthread.hpp" - -#include -#include - -namespace bfs = boost::filesystem; - -namespace -{ - static bool make_sure_directory_exists(bfs::path directory) - { - - if(!bfs::exists(directory)) - { - bfs::create_directories(directory); - } - - return bfs::exists(directory); - } - - void fill_path(bfs::path& path, const std::string& name) - { - size_t start = 0; - - size_t i; - for(i = 0; i < name.length(); i++) - { - switch(name[i]) - { - case '\\': - path /= name.substr(start, i-start); - start = i+1; - break; - } - } - - path /= name.substr(start, i-start); - } - - std::string get_setting(const std::string& category, const std::string& setting, const std::string& inx) - { - size_t start = inx.find(category); - start = inx.find(setting, start) + setting.length() + 3; - - size_t end = inx.find("!", start); - - return inx.substr(start, end-start); - } - - std::string read_to_string(const bfs::path& path) - { - bfs::ifstream strstream(path, std::ios::in | std::ios::binary); - std::string str; - - strstream.seekg(0, std::ios::end); - str.resize(strstream.tellg()); - strstream.seekg(0, std::ios::beg); - strstream.read(&str[0], str.size()); - strstream.close(); - - return str; - } - - void add_setting(const std::string& category, const std::string& setting, const std::string& val, std::string& ini) - { - size_t loc; - loc = ini.find("[" + category + "]"); - - // If category is not found, create it - if(loc == std::string::npos) - { - loc = ini.size() + 2; - ini += ("\r\n[" + category + "]\r\n"); - } - - loc += category.length() +2 +2; - ini.insert(loc, setting + "=" + val + "\r\n"); - } - - #define FIX(setting) add_setting(category, setting, get_setting(category, setting, inx), ini) - - void bloodmoon_fix_ini(std::string& ini, const bfs::path inxPath) - { - std::string inx = read_to_string(inxPath); - - // Remove this one setting (the only one actually changed by bloodmoon, as opposed to just adding new ones) - size_t start = ini.find("[Weather Blight]"); - start = ini.find("Ambient Loop Sound ID", start); - size_t end = ini.find("\r\n", start) +2; - ini.erase(start, end-start); - - std::string category; - - category = "General"; - { - FIX("Werewolf FOV"); - } - category = "Moons"; - { - FIX("Script Color"); - } - category = "Weather"; - { - FIX("Snow Ripples"); - FIX("Snow Ripple Radius"); - FIX("Snow Ripples Per Flake"); - FIX("Snow Ripple Scale"); - FIX("Snow Ripple Speed"); - FIX("Snow Gravity Scale"); - FIX("Snow High Kill"); - FIX("Snow Low Kill"); - } - category = "Weather Blight"; - { - FIX("Ambient Loop Sound ID"); - } - category = "Weather Snow"; - { - FIX("Sky Sunrise Color"); - FIX("Sky Day Color"); - FIX("Sky Sunset Color"); - FIX("Sky Night Color"); - FIX("Fog Sunrise Color"); - FIX("Fog Day Color"); - FIX("Fog Sunset Color"); - FIX("Fog Night Color"); - FIX("Ambient Sunrise Color"); - FIX("Ambient Day Color"); - FIX("Ambient Sunset Color"); - FIX("Ambient Night Color"); - FIX("Sun Sunrise Color"); - FIX("Sun Day Color"); - FIX("Sun Sunset Color"); - FIX("Sun Night Color"); - FIX("Sun Disc Sunset Color"); - FIX("Transition Delta"); - FIX("Land Fog Day Depth"); - FIX("Land Fog Night Depth"); - FIX("Clouds Maximum Percent"); - FIX("Wind Speed"); - FIX("Cloud Speed"); - FIX("Glare View"); - FIX("Cloud Texture"); - FIX("Ambient Loop Sound ID"); - FIX("Snow Threshold"); - FIX("Snow Diameter"); - FIX("Snow Height Min"); - FIX("Snow Height Max"); - FIX("Snow Entrance Speed"); - FIX("Max Snowflakes"); - } - category = "Weather Blizzard"; - { - FIX("Sky Sunrise Color"); - FIX("Sky Day Color"); - FIX("Sky Sunset Color"); - FIX("Sky Night Color"); - FIX("Fog Sunrise Color"); - FIX("Fog Day Color"); - FIX("Fog Sunset Color"); - FIX("Fog Night Color"); - FIX("Ambient Sunrise Color"); - FIX("Ambient Day Color"); - FIX("Ambient Sunset Color"); - FIX("Ambient Night Color"); - FIX("Sun Sunrise Color"); - FIX("Sun Day Color"); - FIX("Sun Sunset Color"); - FIX("Sun Night Color"); - FIX("Sun Disc Sunset Color"); - FIX("Transition Delta"); - FIX("Land Fog Day Depth"); - FIX("Land Fog Night Depth"); - FIX("Clouds Maximum Percent"); - FIX("Wind Speed"); - FIX("Cloud Speed"); - FIX("Glare View"); - FIX("Cloud Texture"); - FIX("Ambient Loop Sound ID"); - FIX("Storm Threshold"); - } - } - - - void fix_ini(const bfs::path& output_dir, bfs::path cdPath, bool tribunal, bool bloodmoon) - { - bfs::path ini_path = output_dir; - ini_path /= "Morrowind.ini"; - - std::string ini = read_to_string(ini_path.string()); - - if(tribunal) - { - add_setting("Game Files", "GameFile1", "Tribunal.esm", ini); - add_setting("Archives", "Archive 0", "Tribunal.bsa", ini); - } - if(bloodmoon) - { - bloodmoon_fix_ini(ini, cdPath / "setup.inx"); - add_setting("Game Files", "GameFile2", "Bloodmoon.esm", ini); - add_setting("Archives", "Archive 1", "Bloodmoon.bsa", ini); - } - - bfs::ofstream inistream((ini_path)); - inistream << ini; - inistream.close(); - } - - void installToPath(const bfs::path& from, const bfs::path& to, bool copy = false) - { - make_sure_directory_exists(to); - - for ( bfs::directory_iterator end, dir(from); dir != end; ++dir ) - { - if(bfs::is_directory(dir->path())) - installToPath(dir->path(), to / dir->path().filename(), copy); - else - { - if(copy) - { - bfs::path dest = to / dir->path().filename(); - if(bfs::exists(dest)) - bfs::remove_all(dest); - bfs::copy_file(dir->path(), dest); - } - else - bfs::rename(dir->path(), to / dir->path().filename()); - } - } - } - - bfs::path findFile(const bfs::path& in, std::string filename, bool recursive = true) - { - if(recursive) - { - for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) - { - if(Misc::StringUtils::ciEqual(dir->path().filename().string(), filename)) - return dir->path(); - } - } - else - { - for ( bfs::directory_iterator end, dir(in); dir != end; ++dir ) - { - if(Misc::StringUtils::ciEqual(dir->path().filename().string(), filename)) - return dir->path(); - } - } - - return ""; - } - - bool contains(const bfs::path& in, std::string filename) - { - for(bfs::directory_iterator end, dir(in); dir != end; ++dir) - { - if(Misc::StringUtils::ciEqual(dir->path().filename().string(), filename)) - return true; - } - - return false; - } - - time_t getTime(const char* time) - { - struct tm tms; - memset(&tms, 0, sizeof(struct tm)); - strptime(time, "%d %B %Y", &tms); - return mktime(&tms); - } - - // Some cds have cab files which have the Data Files subfolders outside the Data Files folder - void install_dfiles_outside(const bfs::path& from, const bfs::path& dFiles) - { - bfs::path fonts = findFile(from, "fonts", false); - if(fonts.string() != "") - installToPath(fonts, dFiles / "Fonts"); - - bfs::path music = findFile(from, "music", false); - if(music.string() != "") - installToPath(music, dFiles / "Music"); - - bfs::path sound = findFile(from, "sound", false); - if(sound.string() != "") - installToPath(sound, dFiles / "Sound"); - - bfs::path splash = findFile(from, "splash", false); - if(splash.string() != "") - installToPath(splash, dFiles / "Splash"); - } - -} - -bool Launcher::UnshieldThread::SetMorrowindPath(const std::string& path) -{ - mMorrowindPath = path; - return true; -} - -bool Launcher::UnshieldThread::SetTribunalPath(const std::string& path) -{ - mTribunalPath = path; - return true; -} - -bool Launcher::UnshieldThread::SetBloodmoonPath(const std::string& path) -{ - mBloodmoonPath = path; - return true; -} - -void Launcher::UnshieldThread::SetOutputPath(const std::string& path) -{ - mOutputPath = path; -} - -bool Launcher::UnshieldThread::extract_file(Unshield* unshield, bfs::path output_dir, const char* prefix, int index) -{ - bool success; - bfs::path dirname; - bfs::path filename; - int directory = unshield_file_directory(unshield, index); - - dirname = output_dir; - - if (prefix && prefix[0]) - dirname /= prefix; - - if (directory >= 0) - { - const char* tmp = unshield_directory_name(unshield, directory); - if (tmp && tmp[0]) - fill_path(dirname, tmp); - } - - make_sure_directory_exists(dirname); - - filename = dirname; - filename /= unshield_file_name(unshield, index); - - emit signalGUI(QString("Extracting: ") + QString(filename.c_str())); - - success = unshield_file_save(unshield, index, filename.c_str()); - - if (!success) - bfs::remove(filename); - - return success; -} - -void Launcher::UnshieldThread::extract_cab(const bfs::path& cab, const bfs::path& output_dir, bool extract_ini) -{ - Unshield * unshield; - unshield = unshield_open(cab.c_str()); - - int i; - for (i = 0; i < unshield_file_group_count(unshield); i++) - { - UnshieldFileGroup* file_group = unshield_file_group_get(unshield, i); - - for (size_t j = file_group->first_file; j <= file_group->last_file; j++) - { - if (unshield_file_is_valid(unshield, j)) - extract_file(unshield, output_dir, file_group->name, j); - } - } - unshield_close(unshield); -} - - -bool Launcher::UnshieldThread::extract() -{ - bfs::path outputDataFilesDir = mOutputPath; - outputDataFilesDir /= "Data Files"; - bfs::path extractPath = mOutputPath; - extractPath /= "extract-temp"; - - if(!mMorrowindDone && mMorrowindPath.string().length() > 0) - { - mMorrowindDone = true; - - bfs::path mwExtractPath = extractPath / "morrowind"; - extract_cab(mMorrowindPath, mwExtractPath, true); - - bfs::path dFilesDir = findFile(mwExtractPath, "morrowind.esm").parent_path(); - - installToPath(dFilesDir, outputDataFilesDir); - - install_dfiles_outside(mwExtractPath, outputDataFilesDir); - - // Videos are often kept uncompressed on the cd - bfs::path videosPath = findFile(mMorrowindPath.parent_path(), "video", false); - if(videosPath.string() != "") - { - emit signalGUI(QString("Installing Videos...")); - installToPath(videosPath, outputDataFilesDir / "Video", true); - } - - bfs::path cdDFiles = findFile(mMorrowindPath.parent_path(), "data files", false); - if(cdDFiles.string() != "") - { - emit signalGUI(QString("Installing Uncompressed Data files from CD...")); - installToPath(cdDFiles, outputDataFilesDir, true); - } - - - bfs::rename(findFile(mwExtractPath, "morrowind.ini"), outputDataFilesDir / "Morrowind.ini"); - - mTribunalDone = contains(outputDataFilesDir, "tribunal.esm"); - mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); - - } - - else if(!mTribunalDone && mTribunalPath.string().length() > 0) - { - mTribunalDone = true; - - bfs::path tbExtractPath = extractPath / "tribunal"; - extract_cab(mTribunalPath, tbExtractPath, true); - - bfs::path dFilesDir = findFile(tbExtractPath, "tribunal.esm").parent_path(); - - installToPath(dFilesDir, outputDataFilesDir); - - install_dfiles_outside(tbExtractPath, outputDataFilesDir); - - // Mt GOTY CD has Sounds in a seperate folder from the rest of the data files - bfs::path soundsPath = findFile(tbExtractPath, "sounds", false); - if(soundsPath.string() != "") - installToPath(soundsPath, outputDataFilesDir / "Sounds"); - - bfs::path cdDFiles = findFile(mTribunalPath.parent_path(), "data files", false); - if(cdDFiles.string() != "") - { - emit signalGUI(QString("Installing Uncompressed Data files from CD...")); - installToPath(cdDFiles, outputDataFilesDir, true); - } - - mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); - - fix_ini(outputDataFilesDir, bfs::path(mTribunalPath).parent_path(), mTribunalDone, mBloodmoonDone); - } - - else if(!mBloodmoonDone && mBloodmoonPath.string().length() > 0) - { - mBloodmoonDone = true; - - bfs::path bmExtractPath = extractPath / "bloodmoon"; - extract_cab(mBloodmoonPath, bmExtractPath, true); - - bfs::path dFilesDir = findFile(bmExtractPath, "bloodmoon.esm").parent_path(); - - installToPath(dFilesDir, outputDataFilesDir); - - install_dfiles_outside(bmExtractPath, outputDataFilesDir); - - // My GOTY CD contains a folder within cab files called Tribunal patch, - // which contains Tribunal.esm - bfs::path tbPatchPath = findFile(bmExtractPath, "tribunal.esm"); - if(tbPatchPath.string() != "") - bfs::rename(tbPatchPath, outputDataFilesDir / "Tribunal.esm"); - - bfs::path cdDFiles = findFile(mBloodmoonPath.parent_path(), "data files", false); - if(cdDFiles.string() != "") - { - emit signalGUI(QString("Installing Uncompressed Data files from CD...")); - installToPath(cdDFiles, outputDataFilesDir, true); - } - - fix_ini(outputDataFilesDir, bfs::path(mBloodmoonPath).parent_path(), false, mBloodmoonDone); - } - - - return true; -} - -void Launcher::UnshieldThread::Done() -{ - // Get rid of unnecessary files - bfs::remove_all(mOutputPath / "extract-temp"); - - // Set modified time to release dates, to preserve load order - if(mMorrowindDone) - bfs::last_write_time(findFile(mOutputPath, "morrowind.esm"), getTime("1 May 2002")); - - if(mTribunalDone) - bfs::last_write_time(findFile(mOutputPath, "tribunal.esm"), getTime("6 November 2002")); - - if(mBloodmoonDone) - bfs::last_write_time(findFile(mOutputPath, "bloodmoon.esm"), getTime("3 June 2003")); -} - -std::string Launcher::UnshieldThread::GetMWEsmPath() -{ - return findFile(mOutputPath / "Data Files", "morrowind.esm").string(); -} - -bool Launcher::UnshieldThread::TribunalDone() -{ - return mTribunalDone; -} - -bool Launcher::UnshieldThread::BloodmoonDone() -{ - return mBloodmoonDone; -} - -void Launcher::UnshieldThread::run() -{ - extract(); - emit close(); -} - -Launcher::UnshieldThread::UnshieldThread() -{ - unshield_set_log_level(0); - mMorrowindDone = false; - mTribunalDone = false; - mBloodmoonDone = false; -} diff --git a/apps/launcher/unshieldthread.hpp b/apps/launcher/unshieldthread.hpp deleted file mode 100644 index de6a32b44..000000000 --- a/apps/launcher/unshieldthread.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef UNSHIELD_THREAD_H -#define UNSHIELD_THREAD_H - -#include - -#include - -#include - -namespace Launcher -{ - class UnshieldThread : public QThread - { - Q_OBJECT - - public: - bool SetMorrowindPath(const std::string& path); - bool SetTribunalPath(const std::string& path); - bool SetBloodmoonPath(const std::string& path); - - void SetOutputPath(const std::string& path); - - bool extract(); - - bool TribunalDone(); - bool BloodmoonDone(); - - void Done(); - - std::string GetMWEsmPath(); - - UnshieldThread(); - - private: - - void extract_cab(const boost::filesystem::path& cab, const boost::filesystem::path& output_dir, bool extract_ini = false); - bool extract_file(Unshield* unshield, boost::filesystem::path output_dir, const char* prefix, int index); - - boost::filesystem::path mMorrowindPath; - boost::filesystem::path mTribunalPath; - boost::filesystem::path mBloodmoonPath; - - bool mMorrowindDone; - bool mTribunalDone; - bool mBloodmoonDone; - - boost::filesystem::path mOutputPath; - - - protected: - virtual void run(); - - signals: - void signalGUI(QString); - void close(); - }; -} -#endif diff --git a/apps/launcher/utils/checkablemessagebox.cpp b/apps/launcher/utils/checkablemessagebox.cpp deleted file mode 100644 index 2f775af57..000000000 --- a/apps/launcher/utils/checkablemessagebox.cpp +++ /dev/null @@ -1,258 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of Qt Creator. -** -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and Digia. For licensing terms and -** conditions see http://qt.digia.com/licensing. For further information -** use the contact form at http://qt.digia.com/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Digia gives you certain additional -** rights. These rights are described in the Digia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -****************************************************************************/ - -#include "checkablemessagebox.hpp" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -/*! - \class Utils::CheckableMessageBox - - \brief A messagebox suitable for questions with a - "Do not ask me again" checkbox. - - Emulates the QMessageBox API with - static conveniences. The message label can open external URLs. -*/ -Launcher::CheckableMessageBoxPrivate::CheckableMessageBoxPrivate(QDialog *q) - : clickedButton(0) -{ - QSizePolicy sizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); - - pixmapLabel = new QLabel(q); - sizePolicy.setHorizontalStretch(0); - sizePolicy.setVerticalStretch(0); - sizePolicy.setHeightForWidth(pixmapLabel->sizePolicy().hasHeightForWidth()); - pixmapLabel->setSizePolicy(sizePolicy); - pixmapLabel->setVisible(false); - - QSpacerItem *pixmapSpacer = - new QSpacerItem(0, 5, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); - - messageLabel = new QLabel(q); - messageLabel->setMinimumSize(QSize(300, 0)); - messageLabel->setWordWrap(true); - messageLabel->setOpenExternalLinks(true); - messageLabel->setTextInteractionFlags(Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse); - - QSpacerItem *checkBoxRightSpacer = - new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); - QSpacerItem *buttonSpacer = - new QSpacerItem(0, 1, QSizePolicy::Minimum, QSizePolicy::Minimum); - - checkBox = new QCheckBox(q); - checkBox->setText(Launcher::CheckableMessageBox::tr("Do not ask again")); - - buttonBox = new QDialogButtonBox(q); - buttonBox->setOrientation(Qt::Horizontal); - buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); - - QVBoxLayout *verticalLayout = new QVBoxLayout(); - verticalLayout->addWidget(pixmapLabel); - verticalLayout->addItem(pixmapSpacer); - - QHBoxLayout *horizontalLayout_2 = new QHBoxLayout(); - horizontalLayout_2->addLayout(verticalLayout); - horizontalLayout_2->addWidget(messageLabel); - - QHBoxLayout *horizontalLayout = new QHBoxLayout(); - horizontalLayout->addWidget(checkBox); - horizontalLayout->addItem(checkBoxRightSpacer); - - QVBoxLayout *verticalLayout_2 = new QVBoxLayout(q); - verticalLayout_2->addLayout(horizontalLayout_2); - verticalLayout_2->addLayout(horizontalLayout); - verticalLayout_2->addItem(buttonSpacer); - verticalLayout_2->addWidget(buttonBox); -} - -Launcher::CheckableMessageBox::CheckableMessageBox(QWidget *parent) : - QDialog(parent), - d(new Launcher::CheckableMessageBoxPrivate(this)) -{ - setModal(true); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - connect(d->buttonBox, SIGNAL(accepted()), SLOT(accept())); - connect(d->buttonBox, SIGNAL(rejected()), SLOT(reject())); - connect(d->buttonBox, SIGNAL(clicked(QAbstractButton*)), - SLOT(slotClicked(QAbstractButton*))); -} - -Launcher::CheckableMessageBox::~CheckableMessageBox() -{ - delete d; -} - -void Launcher::CheckableMessageBox::slotClicked(QAbstractButton *b) -{ - d->clickedButton = b; -} - -QAbstractButton *Launcher::CheckableMessageBox::clickedButton() const -{ - return d->clickedButton; -} - -QDialogButtonBox::StandardButton Launcher::CheckableMessageBox::clickedStandardButton() const -{ - if (d->clickedButton) - return d->buttonBox->standardButton(d->clickedButton); - return QDialogButtonBox::NoButton; -} - -QString Launcher::CheckableMessageBox::text() const -{ - return d->messageLabel->text(); -} - -void Launcher::CheckableMessageBox::setText(const QString &t) -{ - d->messageLabel->setText(t); -} - -QPixmap Launcher::CheckableMessageBox::iconPixmap() const -{ - if (const QPixmap *p = d->pixmapLabel->pixmap()) - return QPixmap(*p); - return QPixmap(); -} - -void Launcher::CheckableMessageBox::setIconPixmap(const QPixmap &p) -{ - d->pixmapLabel->setPixmap(p); - d->pixmapLabel->setVisible(!p.isNull()); -} - -bool Launcher::CheckableMessageBox::isChecked() const -{ - return d->checkBox->isChecked(); -} - -void Launcher::CheckableMessageBox::setChecked(bool s) -{ - d->checkBox->setChecked(s); -} - -QString Launcher::CheckableMessageBox::checkBoxText() const -{ - return d->checkBox->text(); -} - -void Launcher::CheckableMessageBox::setCheckBoxText(const QString &t) -{ - d->checkBox->setText(t); -} - -bool Launcher::CheckableMessageBox::isCheckBoxVisible() const -{ - return d->checkBox->isVisible(); -} - -void Launcher::CheckableMessageBox::setCheckBoxVisible(bool v) -{ - d->checkBox->setVisible(v); -} - -QDialogButtonBox::StandardButtons Launcher::CheckableMessageBox::standardButtons() const -{ - return d->buttonBox->standardButtons(); -} - -void Launcher::CheckableMessageBox::setStandardButtons(QDialogButtonBox::StandardButtons s) -{ - d->buttonBox->setStandardButtons(s); -} - -QPushButton *Launcher::CheckableMessageBox::button(QDialogButtonBox::StandardButton b) const -{ - return d->buttonBox->button(b); -} - -QPushButton *Launcher::CheckableMessageBox::addButton(const QString &text, QDialogButtonBox::ButtonRole role) -{ - return d->buttonBox->addButton(text, role); -} - -QDialogButtonBox::StandardButton Launcher::CheckableMessageBox::defaultButton() const -{ - foreach (QAbstractButton *b, d->buttonBox->buttons()) - if (QPushButton *pb = qobject_cast(b)) - if (pb->isDefault()) - return d->buttonBox->standardButton(pb); - return QDialogButtonBox::NoButton; -} - -void Launcher::CheckableMessageBox::setDefaultButton(QDialogButtonBox::StandardButton s) -{ - if (QPushButton *b = d->buttonBox->button(s)) { - b->setDefault(true); - b->setFocus(); - } -} - -QDialogButtonBox::StandardButton -Launcher::CheckableMessageBox::question(QWidget *parent, - const QString &title, - const QString &question, - const QString &checkBoxText, - bool *checkBoxSetting, - QDialogButtonBox::StandardButtons buttons, - QDialogButtonBox::StandardButton defaultButton) -{ - CheckableMessageBox mb(parent); - mb.setWindowTitle(title); - mb.setIconPixmap(QMessageBox::standardIcon(QMessageBox::Question)); - mb.setText(question); - mb.setCheckBoxText(checkBoxText); - mb.setChecked(*checkBoxSetting); - mb.setStandardButtons(buttons); - mb.setDefaultButton(defaultButton); - mb.exec(); - *checkBoxSetting = mb.isChecked(); - return mb.clickedStandardButton(); -} - -QMessageBox::StandardButton Launcher::CheckableMessageBox::dialogButtonBoxToMessageBoxButton(QDialogButtonBox::StandardButton db) -{ - return static_cast(int(db)); -} diff --git a/apps/launcher/utils/checkablemessagebox.hpp b/apps/launcher/utils/checkablemessagebox.hpp deleted file mode 100644 index 09a501b9c..000000000 --- a/apps/launcher/utils/checkablemessagebox.hpp +++ /dev/null @@ -1,116 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of Qt Creator. -** -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and Digia. For licensing terms and -** conditions see http://qt.digia.com/licensing. For further information -** use the contact form at http://qt.digia.com/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Digia gives you certain additional -** rights. These rights are described in the Digia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -****************************************************************************/ - -#ifndef CHECKABLEMESSAGEBOX_HPP -#define CHECKABLEMESSAGEBOX_HPP - -#include -#include -#include - -class QCheckBox; - -namespace Launcher -{ - class CheckableMessageBoxPrivate - { - public: - - QLabel *pixmapLabel; - QLabel *messageLabel; - QCheckBox *checkBox; - QDialogButtonBox *buttonBox; - QAbstractButton *clickedButton; - - public: - CheckableMessageBoxPrivate(QDialog *q); - }; - - class CheckableMessageBox : public QDialog - { - Q_OBJECT - Q_PROPERTY(QString text READ text WRITE setText) - Q_PROPERTY(QPixmap iconPixmap READ iconPixmap WRITE setIconPixmap) - Q_PROPERTY(bool isChecked READ isChecked WRITE setChecked) - Q_PROPERTY(QString checkBoxText READ checkBoxText WRITE setCheckBoxText) - Q_PROPERTY(QDialogButtonBox::StandardButtons buttons READ standardButtons WRITE setStandardButtons) - Q_PROPERTY(QDialogButtonBox::StandardButton defaultButton READ defaultButton WRITE setDefaultButton) - - public: - explicit CheckableMessageBox(QWidget *parent); - virtual ~CheckableMessageBox(); - - static QDialogButtonBox::StandardButton - question(QWidget *parent, - const QString &title, - const QString &question, - const QString &checkBoxText, - bool *checkBoxSetting, - QDialogButtonBox::StandardButtons buttons = QDialogButtonBox::Yes|QDialogButtonBox::No, - QDialogButtonBox::StandardButton defaultButton = QDialogButtonBox::No); - - QString text() const; - void setText(const QString &); - - bool isChecked() const; - void setChecked(bool s); - - QString checkBoxText() const; - void setCheckBoxText(const QString &); - - bool isCheckBoxVisible() const; - void setCheckBoxVisible(bool); - - QDialogButtonBox::StandardButtons standardButtons() const; - void setStandardButtons(QDialogButtonBox::StandardButtons s); - QPushButton *button(QDialogButtonBox::StandardButton b) const; - QPushButton *addButton(const QString &text, QDialogButtonBox::ButtonRole role); - - QDialogButtonBox::StandardButton defaultButton() const; - void setDefaultButton(QDialogButtonBox::StandardButton s); - - // See static QMessageBox::standardPixmap() - QPixmap iconPixmap() const; - void setIconPixmap (const QPixmap &p); - - // Query the result - QAbstractButton *clickedButton() const; - QDialogButtonBox::StandardButton clickedStandardButton() const; - - // Conversion convenience - static QMessageBox::StandardButton dialogButtonBoxToMessageBoxButton(QDialogButtonBox::StandardButton); - - private slots: - void slotClicked(QAbstractButton *b); - - private: - CheckableMessageBoxPrivate *d; - }; -} -#endif // CHECKABLEMESSAGEBOX_HPP From 88064f49469f06b2c5b5a2a534ed175c05c560b6 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 29 May 2014 20:35:07 +0200 Subject: [PATCH 57/69] Fix for MOC error due to Boost includes --- apps/wizard/mainwizard.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 6b8c4931f..c22f20c25 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -7,7 +7,9 @@ #include +#ifndef Q_MOC_RUN #include +#endif #include #include From 2a6a26c0d050daddc31f2883d82c35e3af3d93fd Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 29 May 2014 21:02:06 +0200 Subject: [PATCH 58/69] Minor fixes --- apps/wizard/existinginstallationpage.cpp | 8 ++------ apps/wizard/mainwizard.cpp | 2 +- files/ui/wizard/intropage.ui | 10 ++++------ 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 7c5d10a80..7171cb26b 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -23,12 +23,6 @@ void Wizard::ExistingInstallationPage::initializePage() emptyItem->setFlags(Qt::NoItemFlags); installationsList->addItem(emptyItem); - // Test - if (mWizard->mInstallations.isEmpty()) { - qDebug() << "crashy crash"; - return; - } - // Add the available installation paths QStringList paths(mWizard->mInstallations.keys()); @@ -133,6 +127,8 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() installationsList->setCurrentItem(items.first()); } + // Update the button + emit completeChanged(); } void Wizard::ExistingInstallationPage::textChanged(const QString &text) diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 92743417e..33d08b50a 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -279,7 +279,7 @@ void Wizard::MainWizard::runSettingsImporter() return qApp->quit(); // Re-read the game settings - // setupGameSettings(); + setupGameSettings(); } void Wizard::MainWizard::addInstallation(const QString &path) diff --git a/files/ui/wizard/intropage.ui b/files/ui/wizard/intropage.ui index b06f7f170..117d2098e 100644 --- a/files/ui/wizard/intropage.ui +++ b/files/ui/wizard/intropage.ui @@ -6,8 +6,8 @@ 0 0 - 474 - 370 + 472 + 368
@@ -20,7 +20,7 @@ - This Wizard will help you install Morrowindand its add-ons for OpenMW to use. + This Wizard will help you install Morrowind and its add-ons for OpenMW to use. true @@ -29,8 +29,6 @@ - - - + From a390dde818180fe846dda7c5abcb72f38b649666 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 29 May 2014 21:17:49 +0200 Subject: [PATCH 59/69] Fixed importing configurations of existing installations --- apps/launcher/maindialog.cpp | 2 +- apps/wizard/mainwizard.cpp | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 76b9ad3c5..708969516 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -257,7 +257,7 @@ void Launcher::MainDialog::changePage(QListWidgetItem *current, QListWidgetItem current = previous; int currentIndex = iconWidget->row(current); - int previousIndex = iconWidget->row(previous); +// int previousIndex = iconWidget->row(previous); pagesWidget->setCurrentIndex(currentIndex); diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 33d08b50a..2469cfa8b 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -271,7 +271,13 @@ void Wizard::MainWizard::runSettingsImporter() // Now the paths arguments.append(QLatin1String("--ini")); - arguments.append(path + QDir::separator() + QLatin1String("Morrowind.ini")); + + if (field(QLatin1String("installation.new")).toBool() == true) { + arguments.append(path + QDir::separator() + QLatin1String("Morrowind.ini")); + } else { + arguments.append(mInstallations[path].iniPath); + } + arguments.append(QLatin1String("--cfg")); arguments.append(userPath + QLatin1String("openmw.cfg")); From 6348af586e80545cae112850ab369b1a93ba45c9 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 30 May 2014 02:12:48 +0200 Subject: [PATCH 60/69] Fixed some problems with the launcher and the wizard --- apps/launcher/datafilespage.cpp | 10 +--- apps/launcher/datafilespage.hpp | 2 - apps/launcher/main.cpp | 10 ++-- apps/launcher/maindialog.cpp | 71 ++++++++++++------------ apps/wizard/existinginstallationpage.cpp | 14 +++-- apps/wizard/mainwizard.cpp | 6 -- 6 files changed, 50 insertions(+), 63 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 6b53d89a4..17951f2e4 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -36,7 +36,7 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: this, SLOT(updateOkButton(QString))); buildView(); - setupDataFiles(); + loadSettings(); } void Launcher::DataFilesPage::buildView() @@ -67,7 +67,7 @@ void Launcher::DataFilesPage::buildView() this, SLOT (slotProfileChangedByUser(QString, QString))); } -void Launcher::DataFilesPage::setupDataFiles() +bool Launcher::DataFilesPage::loadSettings() { QStringList paths = mGameSettings.getDataDirs(); @@ -79,12 +79,6 @@ void Launcher::DataFilesPage::setupDataFiles() if (!mDataLocal.isEmpty()) mSelector->addFiles(mDataLocal); - loadSettings(); -} - -bool Launcher::DataFilesPage::loadSettings() -{ - QStringList paths = mGameSettings.getDataDirs(); paths.insert (0, mDataLocal); PathIterator pathIterator (paths); diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 908394ae1..5beeb0e03 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -41,8 +41,6 @@ namespace Launcher void saveSettings(const QString &profile = ""); bool loadSettings(); - void setupDataFiles(); - signals: void signalProfileChanged (int index); diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index 67320bff0..da319426d 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -52,12 +52,12 @@ int main(int argc, char *argv[]) Launcher::MainDialog mainWin; - if (!mainWin.setup()) { + if (!mainWin.showFirstRunDialog()) return 0; - //mainWin.show(); - }/* else { - return 0; - }*/ + +// if (!mainWin.setup()) +// return 0; + int returnValue = app.exec(); SDL_Quit(); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 708969516..c66de0b03 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -165,40 +165,44 @@ void Launcher::MainDialog::createPages() bool Launcher::MainDialog::showFirstRunDialog() { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("First run")); - msgBox.setIcon(QMessageBox::Question); - msgBox.setStandardButtons(QMessageBox::NoButton); - msgBox.setText(tr("

Welcome to OpenMW!

\ -

It is recommended to run the Installation Wizard.

\ -

The Wizard will let you select an existing Morrowind installation, \ - or install Morrowind for OpenMW to use.

")); - QAbstractButton *wizardButton = - msgBox.addButton(tr("Run &Installation Wizard"), QMessageBox::AcceptRole); // ActionRole doesn't work?! - QAbstractButton *skipButton = - msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); - Q_UNUSED(skipButton); // Surpress compiler unused warning - - msgBox.exec(); - - if (msgBox.clickedButton() == wizardButton) + if (!setupLauncherSettings()) + return false; + + if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) { - if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { - return false; - } else { - return true; + QMessageBox msgBox; + msgBox.setWindowTitle(tr("First run")); + msgBox.setIcon(QMessageBox::Question); + msgBox.setStandardButtons(QMessageBox::NoButton); + msgBox.setText(tr("

Welcome to OpenMW!

\ +

It is recommended to run the Installation Wizard.

\ +

The Wizard will let you select an existing Morrowind installation, \ + or install Morrowind for OpenMW to use.

")); + + QAbstractButton *wizardButton = + msgBox.addButton(tr("Run &Installation Wizard"), QMessageBox::AcceptRole); // ActionRole doesn't work?! + QAbstractButton *skipButton = + msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); + + Q_UNUSED(skipButton); // Surpress compiler unused warning + + msgBox.exec(); + + if (msgBox.clickedButton() == wizardButton) + { + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { + return false; + } else { + return true; + } } } - show(); - return true; + return setup(); } bool Launcher::MainDialog::setup() { - if (!setupLauncherSettings()) - return false; - if (!setupGameSettings()) return false; @@ -214,17 +218,7 @@ bool Launcher::MainDialog::setup() loadSettings(); - // Check if we need to run the wizard - if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) - { - if (!showFirstRunDialog()) { - return false; - } else { - return true; - } - } - - show(); // Show ourselves if the wizard is not being run + show(); return true; } @@ -629,6 +623,9 @@ void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exi if (exitCode != 0 || exitStatus == QProcess::CrashExit) return qApp->quit(); + if (!setup()) + return; + reloadSettings(); show(); } diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 7171cb26b..83ea20f5a 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -15,14 +15,16 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(QWidget *parent) : setupUi(this); + // Add a placeholder item to the list of installations + QListWidgetItem *emptyItem = new QListWidgetItem(tr("No existing installations detected")); + emptyItem->setFlags(Qt::NoItemFlags); + + installationsList->insertItem(0, emptyItem); + } void Wizard::ExistingInstallationPage::initializePage() { - QListWidgetItem *emptyItem = new QListWidgetItem(tr("No existing installations detected")); - emptyItem->setFlags(Qt::NoItemFlags); - installationsList->addItem(emptyItem); - // Add the available installation paths QStringList paths(mWizard->mInstallations.keys()); @@ -35,7 +37,9 @@ void Wizard::ExistingInstallationPage::initializePage() foreach (const QString &path, paths) { QListWidgetItem *item = new QListWidgetItem(path); - installationsList->addItem(item); + + if (installationsList->findItems(path, Qt::MatchExactly).isEmpty()) + installationsList->addItem(item); } connect(installationsList, SIGNAL(currentTextChanged(QString)), diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 2469cfa8b..e587d77e5 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -37,9 +37,6 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setWindowIcon(QIcon(QLatin1String(":/images/openmw-wizard.png"))); setMinimumWidth(550); - // This prevents initializePage() being called multiple times - setOption(QWizard::IndependentPages); - // Set the property for comboboxes to the text instead of index setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); @@ -283,9 +280,6 @@ void Wizard::MainWizard::runSettingsImporter() if (!mImporterInvoker->startProcess(QLatin1String("mwiniimport"), arguments, false)) return qApp->quit(); - - // Re-read the game settings - setupGameSettings(); } void Wizard::MainWizard::addInstallation(const QString &path) From 0cc7c062624c7ea9a6ad8adf44ca11eacf803ab0 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 30 May 2014 02:41:09 +0200 Subject: [PATCH 61/69] Ensure the launcher is not shown when the wizard is run --- apps/launcher/main.cpp | 4 +++- apps/launcher/maindialog.cpp | 14 +++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index da319426d..562f5c779 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -55,9 +55,11 @@ int main(int argc, char *argv[]) if (!mainWin.showFirstRunDialog()) return 0; -// if (!mainWin.setup()) +// if (!mainWin.setup()) { // return 0; +// } + mainWin.show(); int returnValue = app.exec(); SDL_Quit(); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index c66de0b03..59e023827 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -51,6 +51,9 @@ Launcher::MainDialog::MainDialog(QWidget *parent) mGameInvoker = new ProcessInvoker(); mWizardInvoker = new ProcessInvoker(); + connect(mWizardInvoker->getProcess(), SIGNAL(started()), + this, SLOT(wizardStarted())); + connect(mWizardInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(wizardFinished(int,QProcess::ExitStatus))); @@ -218,7 +221,6 @@ bool Launcher::MainDialog::setup() loadSettings(); - show(); return true; } @@ -416,7 +418,7 @@ bool Launcher::MainDialog::setupGameSettings() } if (selectedFile.isEmpty()) - return false; // Cancel was clicked; + return false; // Cancel was clicked QFileInfo info(selectedFile); @@ -616,6 +618,7 @@ void Launcher::MainDialog::closeEvent(QCloseEvent *event) void Launcher::MainDialog::wizardStarted() { qDebug() << "wizard started!"; + hide(); } void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) @@ -623,11 +626,8 @@ void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exi if (exitCode != 0 || exitStatus == QProcess::CrashExit) return qApp->quit(); - if (!setup()) - return; - - reloadSettings(); - show(); + if (reloadSettings()) + show(); } void Launcher::MainDialog::play() From fda25f385aa85fc05fe9e9637b709fab7efec849 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 30 May 2014 02:50:17 +0200 Subject: [PATCH 62/69] Fixed segfault when launcher tries to reload settings on pages not yet there --- apps/launcher/maindialog.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 59e023827..41a23e246 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -626,6 +626,9 @@ void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exi if (exitCode != 0 || exitStatus == QProcess::CrashExit) return qApp->quit(); + // HACK: Ensure the pages are created, else segfault + setup(); + if (reloadSettings()) show(); } From 751211351ce75c009d48fb57b3c432571e1ab929 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 18 Nov 2014 17:54:24 +0100 Subject: [PATCH 63/69] Fix multi-line comment warning --- components/process/processinvoker.cpp | 64 ++++++++++++++------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp index 44a11cc4e..4d53084c6 100644 --- a/components/process/processinvoker.cpp +++ b/components/process/processinvoker.cpp @@ -111,37 +111,39 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis } else { mProcess->start(path, arguments); -// if (!mProcess->waitForFinished()) { -// QMessageBox msgBox; -// msgBox.setWindowTitle(tr("Error starting executable")); -// msgBox.setIcon(QMessageBox::Critical); -// msgBox.setStandardButtons(QMessageBox::Ok); -// msgBox.setText(tr("

Could not start %1

\ -//

An error occurred while starting %1.

\ -//

Press \"Show Details...\" for more information.

").arg(info.fileName())); -// msgBox.setDetailedText(mProcess->errorString()); -// msgBox.exec(); - -// return false; -// } - -// if (mProcess->exitCode() != 0 || mProcess->exitStatus() == QProcess::CrashExit) { -// QString error(mProcess->readAllStandardError()); -// error.append(tr("\nArguments:\n")); -// error.append(arguments.join(" ")); - -// QMessageBox msgBox; -// msgBox.setWindowTitle(tr("Error running executable")); -// msgBox.setIcon(QMessageBox::Critical); -// msgBox.setStandardButtons(QMessageBox::Ok); -// msgBox.setText(tr("

Executable %1 returned an error

\ -//

An error occurred while running %1.

\ -//

Press \"Show Details...\" for more information.

").arg(info.fileName())); -// msgBox.setDetailedText(error); -// msgBox.exec(); - -// return false; -// } + /* + if (!mProcess->waitForFinished()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not start %1

\ +

An error occurred while starting %1.

\ +

Press \"Show Details...\" for more information.

").arg(info.fileName())); + msgBox.setDetailedText(mProcess->errorString()); + msgBox.exec(); + + return false; + } + + if (mProcess->exitCode() != 0 || mProcess->exitStatus() == QProcess::CrashExit) { + QString error(mProcess->readAllStandardError()); + error.append(tr("\nArguments:\n")); + error.append(arguments.join(" ")); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error running executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Executable %1 returned an error

\ +

An error occurred while running %1.

\ +

Press \"Show Details...\" for more information.

").arg(info.fileName())); + msgBox.setDetailedText(error); + msgBox.exec(); + + return false; + } + */ } return true; From d4a9cf70757605ca8a7e378cc987665be4f66a6e Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 18 Nov 2014 17:54:37 +0100 Subject: [PATCH 64/69] Fix wizard install location --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 84fc5043a..208bddf33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -435,7 +435,7 @@ IF(NOT WIN32 AND NOT APPLE) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/niftest" DESTINATION "${BINDIR}" ) ENDIF(BUILD_NIFTEST) IF(BUILD_WIZARD) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/wizard" DESTINATION "${BINDIR}" ) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw-wizard" DESTINATION "${BINDIR}" ) ENDIF(BUILD_WIZARD) if(BUILD_MYGUI_PLUGIN) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Plugin_MyGUI_OpenMW_Resources.so" DESTINATION "${LIBDIR}" ) From e5a6049ea7d2955edc4512f9900c0a49a6c476bd Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 18 Nov 2014 18:03:31 +0100 Subject: [PATCH 65/69] Improve clarity of label --- files/ui/wizard/methodselectionpage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/ui/wizard/methodselectionpage.ui b/files/ui/wizard/methodselectionpage.ui index 122ef754e..531d093af 100644 --- a/files/ui/wizard/methodselectionpage.ui +++ b/files/ui/wizard/methodselectionpage.ui @@ -79,7 +79,7 @@ - Install Morrowind to a new location for OpenMW to use. + Install Morrowind from a retail disk to a new location for OpenMW to use. true From c6558fe487b6c53d6be6a470de9061bb11fc0082 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 19 Nov 2014 13:19:02 +0100 Subject: [PATCH 66/69] Fix Ogre being initialised twice --- apps/launcher/graphicspage.cpp | 3 ++- components/ogreinit/ogreinit.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 1c6e69023..ec7f5a04d 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -137,6 +137,7 @@ bool Launcher::GraphicsPage::setupSDL() return false; } + screenComboBox->clear(); for (int i = 0; i < displays; i++) { screenComboBox->addItem(QString(tr("Screen ")) + QString::number(i + 1)); @@ -149,7 +150,7 @@ bool Launcher::GraphicsPage::loadSettings() { if (!setupSDL()) return false; - if (!setupOgre()) + if (!mOgre && !setupOgre()) return false; if (mGraphicsSettings.value(QString("Video/vsync")) == QLatin1String("true")) diff --git a/components/ogreinit/ogreinit.cpp b/components/ogreinit/ogreinit.cpp index 40712e282..574b67f19 100644 --- a/components/ogreinit/ogreinit.cpp +++ b/components/ogreinit/ogreinit.cpp @@ -94,6 +94,8 @@ namespace OgreInit Ogre::Root* OgreInit::init(const std::string &logPath) { + if (mRoot) + throw std::runtime_error("OgreInit was already initialised"); #ifndef ANDROID // Set up logging first From 31fab533fe1f6db2634c2abff5f5697e76f7e2fd Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 20 Nov 2014 18:14:49 +0100 Subject: [PATCH 67/69] OSX build fix --- components/process/processinvoker.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp index 4d53084c6..cc842fd61 100644 --- a/components/process/processinvoker.cpp +++ b/components/process/processinvoker.cpp @@ -7,6 +7,7 @@ #include #include #include +#include Process::ProcessInvoker::ProcessInvoker() { From e11bec1a37956a4f5ef07f84e07629b8da932855 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 20 Nov 2014 22:36:24 +0100 Subject: [PATCH 68/69] Fix the launcher not properly reloading data directories --- components/config/gamesettings.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 5c348b191..1e7f716e2 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -38,9 +38,6 @@ Config::GameSettings::~GameSettings() void Config::GameSettings::validatePaths() { - if (mSettings.isEmpty() || !mDataDirs.isEmpty()) - return; // Don't re-validate paths if they are already parsed - QStringList paths = mSettings.values(QString("data")); Files::PathContainer dataDirs; From 1e06f69d9e0a33f2e92f4451fabae3393ec1d267 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 20 Nov 2014 22:39:21 +0100 Subject: [PATCH 69/69] Remove the "browse to existing installation" button, now replaced by the wizard Clean up some debug spam --- apps/launcher/maindialog.cpp | 28 ++-------------------------- apps/launcher/settingspage.cpp | 4 ---- apps/wizard/inisettings.cpp | 3 --- apps/wizard/installationpage.cpp | 2 -- apps/wizard/mainwizard.cpp | 2 -- 5 files changed, 2 insertions(+), 37 deletions(-) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 41a23e246..975958d7a 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -388,27 +388,14 @@ bool Launcher::MainDialog::setupGameSettings() msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Cancel); msgBox.setText(tr("
Could not find the Data Files location

\ - The directory containing the data files was not found.

\ - Press \"Browse...\" to specify the location manually.
")); - - QAbstractButton *browseButton = - msgBox.addButton(tr("Browse..."), QMessageBox::ActionRole); + The directory containing the data files was not found.")); QAbstractButton *wizardButton = msgBox.addButton(tr("Run &Installation Wizard..."), QMessageBox::ActionRole); msgBox.exec(); - QString selectedFile; - if (msgBox.clickedButton() == browseButton) - { - selectedFile = QFileDialog::getOpenFileName( - this, - tr("Select master file"), - QDir::currentPath(), - tr("Morrowind master file (*.esm)")); - } - else if (msgBox.clickedButton() == wizardButton) + if (msgBox.clickedButton() == wizardButton) { if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { return false; @@ -416,15 +403,6 @@ bool Launcher::MainDialog::setupGameSettings() return true; } } - - if (selectedFile.isEmpty()) - return false; // Cancel was clicked - - QFileInfo info(selectedFile); - - // Add the new dir to the settings file and to the data dir container - mGameSettings.setMultiValue(QString("data"), info.absolutePath()); - mGameSettings.addDataDir(info.absolutePath()); } return true; @@ -610,14 +588,12 @@ bool Launcher::MainDialog::writeSettings() void Launcher::MainDialog::closeEvent(QCloseEvent *event) { - qDebug() << "close event!"; writeSettings(); event->accept(); } void Launcher::MainDialog::wizardStarted() { - qDebug() << "wizard started!"; hide(); } diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 45e0f72a4..5422e7957 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -174,13 +174,11 @@ void Launcher::SettingsPage::wizardStarted() { mMain->hide(); // Hide the launcher - qDebug() << "wizard started!"; wizardButton->setEnabled(false); } void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) { - qDebug() << "wizard finished!"; if (exitCode != 0 || exitStatus == QProcess::CrashExit) return qApp->quit(); @@ -192,13 +190,11 @@ void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus e void Launcher::SettingsPage::importerStarted() { - qDebug() << "importer started!"; importerButton->setEnabled(false); } void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) { - qDebug() << "importer finished!"; if (exitCode != 0 || exitStatus == QProcess::CrashExit) return; diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index f67da0455..3711ba066 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -33,7 +33,6 @@ QStringList Wizard::IniSettings::findKeys(const QString &text) bool Wizard::IniSettings::readFile(QTextStream &stream) { - qDebug() << "readFile called!"; // Look for a square bracket, "'\\[" // that has one or more "not nothing" in it, "([^]]+)" // and is closed with a square bracket, "\\]" @@ -66,7 +65,6 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) if (!currentSection.isEmpty()) key = currentSection + QLatin1Char('/') + key; - //qDebug() << "adding: " << key << value; mSettings[key] = QVariant(value); } } @@ -204,7 +202,6 @@ bool Wizard::IniSettings::parseInx(const QString &path) const QString key(array.mid(section.length() + 3, lenght)); QString value(array.mid(section.length() + key.length() + 6)); - //qDebug() << section << key << value; // Add the value setValue(section + QLatin1Char('/') + key, QVariant(value)); diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index b76aeea22..09e577317 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -58,8 +58,6 @@ Wizard::InstallationPage::InstallationPage(QWidget *parent) : Wizard::InstallationPage::~InstallationPage() { - qDebug() << "stop!"; - if (mThread->isRunning()) { mUnshield->stopWorker(); mThread->wait(); diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index e587d77e5..d539793ec 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -331,12 +331,10 @@ void Wizard::MainWizard::setupPages() void Wizard::MainWizard::importerStarted() { - qDebug() << "importer started!"; } void Wizard::MainWizard::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) { - qDebug() << "importer finished!"; if (exitCode != 0 || exitStatus == QProcess::CrashExit) return;