@ -7,6 +7,9 @@
# include <QMessageBox>
# include <QMenu>
# include <QSortFilterProxyModel>
# include <QFileDialog>
# include <QTreeView>
# include <qnamespace.h>
# include <thread>
# include <mutex>
# include <algorithm>
@ -20,12 +23,34 @@
# include <components/config/gamesettings.hpp>
# include <components/config/launchersettings.hpp>
# include <components/bsa/compressedbsafile.hpp>
# include <components/navmeshtool/protocol.hpp>
# include <components/vfs/bsaarchive.hpp>
# include "utils/textinputdialog.hpp"
# include "utils/profilescombobox.hpp"
# include "ui_directorypicker.h"
const char * Launcher : : DataFilesPage : : mDefaultContentListName = " Default " ;
namespace
{
void contentSubdirs ( const QString & path , QStringList & dirs )
{
QStringList fileFilter { " *.esm " , " *.esp " , " *.omwaddon " , " *.bsa " } ;
QStringList dirFilter { " bookart " , " icons " , " meshes " , " music " , " sound " , " textures " } ;
QDir currentDir ( path ) ;
if ( ! currentDir . entryInfoList ( fileFilter , QDir : : Files ) . empty ( )
| | ! currentDir . entryInfoList ( dirFilter , QDir : : Dirs | QDir : : NoDotAndDotDot ) . empty ( ) )
dirs . push_back ( currentDir . absolutePath ( ) ) ;
for ( const auto & subdir : currentDir . entryInfoList ( QDir : : Dirs | QDir : : NoDotAndDotDot ) )
contentSubdirs ( subdir . absoluteFilePath ( ) , dirs ) ;
}
}
namespace Launcher
{
namespace
@ -114,6 +139,14 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config:
this , SLOT ( updateNewProfileOkButton ( QString ) ) ) ;
connect ( mCloneProfileDialog - > lineEdit ( ) , SIGNAL ( textChanged ( QString ) ) ,
this , SLOT ( updateCloneProfileOkButton ( QString ) ) ) ;
connect ( ui . directoryAddSubdirsButton , & QPushButton : : released , this , [ = ] ( ) { this - > addSubdirectories ( true ) ; } ) ;
connect ( ui . directoryInsertButton , & QPushButton : : released , this , [ = ] ( ) { this - > addSubdirectories ( false ) ; } ) ;
connect ( ui . directoryUpButton , & QPushButton : : released , this , [ = ] ( ) { this - > moveDirectory ( - 1 ) ; } ) ;
connect ( ui . directoryDownButton , & QPushButton : : released , this , [ = ] ( ) { this - > moveDirectory ( 1 ) ; } ) ;
connect ( ui . directoryRemoveButton , & QPushButton : : released , this , [ = ] ( ) { this - > removeDirectory ( ) ; } ) ;
connect ( ui . archiveUpButton , & QPushButton : : released , this , [ = ] ( ) { this - > moveArchive ( - 1 ) ; } ) ;
connect ( ui . archiveDownButton , & QPushButton : : released , this , [ = ] ( ) { this - > moveArchive ( 1 ) ; } ) ;
connect ( ui . directoryListWidget - > model ( ) , & QAbstractItemModel : : rowsMoved , this , [ = ] ( ) { this - > sortDirectories ( ) ; } ) ;
buildView ( ) ;
loadSettings ( ) ;
@ -128,13 +161,12 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config:
void Launcher : : DataFilesPage : : buildView ( )
{
QToolButton * refreshButton = mSelector - > refreshButton ( ) ;
QToolButton * refreshButton = mSelector - > refreshButton ( ) ;
//tool buttons
ui . newProfileButton - > setToolTip ( " Create a new Content List " ) ;
ui . cloneProfileButton - > setToolTip ( " Clone the current Content List " ) ;
ui . deleteProfileButton - > setToolTip ( " Delete an existing Content List " ) ;
refreshButton - > setToolTip ( " Refresh Data Files " ) ;
//combo box
ui . profilesComboBox - > addItem ( mDefaultContentListName ) ;
@ -188,20 +220,94 @@ bool Launcher::DataFilesPage::loadSettings()
void Launcher : : DataFilesPage : : populateFileViews ( const QString & contentModelName )
{
QStringList paths = mGameSettings . getDataDirs ( ) ;
mSelector - > clearFiles ( ) ;
ui . archiveListWidget - > clear ( ) ;
ui . directoryListWidget - > clear ( ) ;
mDataLocal = mGameSettings . getDataLocal ( ) ;
QStringList directories = mLauncherSettings . getDataDirectoryList ( contentModelName ) ;
if ( directories . isEmpty ( ) )
directories = mGameSettings . getDataDirs ( ) ;
mDataLocal = mGameSettings . getDataLocal ( ) ;
if ( ! mDataLocal . isEmpty ( ) )
path s. insert ( 0 , mDataLocal ) ;
directorie s. insert ( 0 , mDataLocal ) ;
mSelector - > clearFiles ( ) ;
const auto globalDataDir = QString ( mGameSettings . getGlobalDataDir ( ) . c_str ( ) ) ;
if ( ! globalDataDir . isEmpty ( ) )
directories . insert ( 0 , globalDataDir ) ;
// add directories, archives and content files
directories . removeDuplicates ( ) ;
for ( const auto & currentDir : directories )
{
// add new achives files presents in current directory
addArchivesFromDir ( currentDir ) ;
// Display new content with green background
QColor background ;
QString tooltip ;
if ( mNewDataDirs . contains ( currentDir ) )
{
tooltip + = " Will be added to the current profile \n " ;
background = Qt : : green ;
}
else
background = Qt : : white ;
// add content files presents in current directory
mSelector - > addFiles ( currentDir , mNewDataDirs . contains ( currentDir ) ) ;
// add current directory to list
ui . directoryListWidget - > addItem ( currentDir ) ;
auto row = ui . directoryListWidget - > count ( ) - 1 ;
auto * item = ui . directoryListWidget - > item ( row ) ;
item - > setBackground ( background ) ;
// deactivate data-local and global data directory: they are always included
if ( currentDir = = mDataLocal | | currentDir = = globalDataDir )
{
auto flags = item - > flags ( ) ;
item - > setFlags ( flags & ~ ( Qt : : ItemIsDragEnabled | Qt : : ItemIsDropEnabled | Qt : : ItemIsEnabled ) ) ;
}
for ( const QString & path : paths )
mSelector - > addFiles ( path ) ;
// Add a "data file" icon if the directory contains a content file
if ( mSelector - > containsDataFiles ( currentDir ) )
{
item - > setIcon ( QIcon ( " :/images/openmw-plugin.png " ) ) ;
tooltip + = " Contains content file(s) " ;
}
else
{
// Pad to correct vertical alignment
QPixmap pixmap ( QSize ( 200 , 200 ) ) ; // Arbitrary big number, will be scaled down to widget size
pixmap . fill ( background ) ;
auto emptyIcon = QIcon ( pixmap ) ;
item - > setIcon ( emptyIcon ) ;
}
item - > setToolTip ( tooltip ) ;
}
mSelector - > sortFiles ( ) ;
PathIterator pathIterator ( paths ) ;
QStringList selectedArchives = mLauncherSettings . getArchiveList ( contentModelName ) ;
if ( selectedArchives . isEmpty ( ) )
selectedArchives = mGameSettings . getArchiveList ( ) ;
// sort and tick BSA according to profile
int row = 0 ;
for ( const auto & archive : selectedArchives )
{
const auto match = ui . archiveListWidget - > findItems ( archive , Qt : : MatchExactly ) ;
if ( match . isEmpty ( ) )
continue ;
const auto name = match [ 0 ] - > text ( ) ;
const auto oldrow = ui . archiveListWidget - > row ( match [ 0 ] ) ;
ui . archiveListWidget - > takeItem ( oldrow ) ;
ui . archiveListWidget - > insertItem ( row , name ) ;
ui . archiveListWidget - > item ( row ) - > setCheckState ( Qt : : Checked ) ;
row + + ;
}
PathIterator pathIterator ( directories ) ;
mSelector - > setProfileContent ( filesInProfile ( contentModelName , pathIterator ) ) ;
}
@ -232,6 +338,9 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile)
if ( profileName . isEmpty ( ) )
profileName = ui . profilesComboBox - > currentText ( ) ;
//retrieve the data paths
auto dirList = selectedDirectoriesPaths ( ) ;
//retrieve the files selected for the profile
ContentSelectorModel : : ContentFileList items = mSelector - > selectedFiles ( ) ;
@ -243,11 +352,36 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile)
{
fileNames . append ( item - > fileName ( ) ) ;
}
mLauncherSettings . setContentList ( profileName , fileNames) ;
mGameSettings . setContentList ( fileNames) ;
mLauncherSettings . setContentList ( profileName , dirList, selectedArchivePaths ( ) , fileNames) ;
mGameSettings . setContentList ( dirList, selectedArchivePaths ( ) , fileNames) ;
}
QStringList Launcher : : DataFilesPage : : selectedFilePaths ( )
QStringList Launcher : : DataFilesPage : : selectedDirectoriesPaths ( ) const
{
QStringList dirList ;
for ( int i = 0 ; i < ui . directoryListWidget - > count ( ) ; + + i )
{
if ( ui . directoryListWidget - > item ( i ) - > background ( ) ! = Qt : : gray )
dirList . append ( ui . directoryListWidget - > item ( i ) - > text ( ) ) ;
}
return dirList ;
}
QStringList Launcher : : DataFilesPage : : selectedArchivePaths ( bool all ) const
{
QStringList archiveList ;
for ( int i = 0 ; i < ui . archiveListWidget - > count ( ) ; + + i )
{
const auto * item = ui . archiveListWidget - > item ( i ) ;
const auto archive = ui . archiveListWidget - > item ( i ) - > text ( ) ;
if ( all | | item - > checkState ( ) = = Qt : : Checked )
archiveList . append ( item - > text ( ) ) ;
}
return archiveList ;
}
QStringList Launcher : : DataFilesPage : : selectedFilePaths ( ) const
{
//retrieve the files selected for the profile
ContentSelectorModel : : ContentFileList items = mSelector - > selectedFiles ( ) ;
@ -255,15 +389,8 @@ QStringList Launcher::DataFilesPage::selectedFilePaths()
for ( const ContentSelectorModel : : EsmFile * item : items )
{
QFile file ( item - > filePath ( ) ) ;
if ( file . exists ( ) )
{
filePaths . append ( item - > filePath ( ) ) ;
}
else
{
slotRefreshButtonClicked ( ) ;
}
}
return filePaths ;
}
@ -307,8 +434,18 @@ void Launcher::DataFilesPage::setProfile (const QString &previous, const QString
ui . profilesComboBox - > setCurrentProfile ( ui . profilesComboBox - > findText ( current ) ) ;
mNewDataDirs . clear ( ) ;
mKnownArchives . clear ( ) ;
populateFileViews ( current ) ;
// save list of "old" bsa to be able to display "new" bsa in a different colour
for ( int i = 0 ; i < ui . archiveListWidget - > count ( ) ; + + i )
{
auto * item = ui . archiveListWidget - > item ( i ) ;
mKnownArchives . push_back ( item - > text ( ) ) ;
item - > setBackground ( Qt : : white ) ;
}
checkForDefaultProfile ( ) ;
}
@ -397,7 +534,7 @@ void Launcher::DataFilesPage::on_cloneProfileAction_triggered()
if ( profile . isEmpty ( ) )
return ;
mLauncherSettings . setContentList ( profile , selected FilePaths( ) ) ;
mLauncherSettings . setContentList ( profile , selected DirectoriesPaths( ) , selectedArchivePaths ( ) , selected FilePaths( ) ) ;
addProfile ( profile , true ) ;
}
@ -435,6 +572,155 @@ void Launcher::DataFilesPage::updateCloneProfileOkButton(const QString &text)
mCloneProfileDialog - > setOkButtonEnabled ( ! text . isEmpty ( ) & & ui . profilesComboBox - > findText ( text ) = = - 1 ) ;
}
QString Launcher : : DataFilesPage : : selectDirectory ( )
{
QFileDialog fileDialog ( this ) ;
fileDialog . setFileMode ( QFileDialog : : Directory ) ;
fileDialog . setOptions ( QFileDialog : : Option : : ShowDirsOnly | QFileDialog : : Option : : ReadOnly ) ;
if ( fileDialog . exec ( ) = = QDialog : : Rejected )
return { } ;
return fileDialog . selectedFiles ( ) [ 0 ] ;
}
void Launcher : : DataFilesPage : : addSubdirectories ( bool append )
{
int selectedRow = append ? ui . directoryListWidget - > count ( ) : ui . directoryListWidget - > currentRow ( ) ;
if ( selectedRow = = - 1 )
return ;
const auto rootDir = selectDirectory ( ) ;
if ( rootDir . isEmpty ( ) )
return ;
QStringList subdirs ;
contentSubdirs ( rootDir , subdirs ) ;
if ( subdirs . empty ( ) )
{
// we didn't find anything that looks like a content directory, add directory selected by user
if ( ui . directoryListWidget - > findItems ( rootDir , Qt : : MatchFixedString ) . isEmpty ( ) )
{
ui . directoryListWidget - > addItem ( rootDir ) ;
mNewDataDirs . push_back ( rootDir ) ;
refreshDataFilesView ( ) ;
}
return ;
}
QDialog dialog ;
Ui : : SelectSubdirs select ;
select . setupUi ( & dialog ) ;
for ( const auto & dir : subdirs )
{
if ( ! ui . directoryListWidget - > findItems ( dir , Qt : : MatchFixedString ) . isEmpty ( ) )
continue ;
const auto lastRow = select . dirListWidget - > count ( ) ;
select . dirListWidget - > addItem ( dir ) ;
select . dirListWidget - > item ( lastRow ) - > setCheckState ( Qt : : Unchecked ) ;
}
dialog . show ( ) ;
if ( dialog . exec ( ) = = QDialog : : Rejected )
return ;
for ( int i = 0 ; i < select . dirListWidget - > count ( ) ; + + i )
{
const auto * dir = select . dirListWidget - > item ( i ) ;
if ( dir - > checkState ( ) = = Qt : : Checked )
{
ui . directoryListWidget - > insertItem ( selectedRow + + , dir - > text ( ) ) ;
mNewDataDirs . push_back ( dir - > text ( ) ) ;
}
}
refreshDataFilesView ( ) ;
}
void Launcher : : DataFilesPage : : sortDirectories ( )
{
// Ensure disabled entries (aka default directories) are always at the top.
for ( auto i = 1 ; i < ui . directoryListWidget - > count ( ) ; + + i )
{
if ( ! ( ui . directoryListWidget - > item ( i ) - > flags ( ) & Qt : : ItemIsEnabled ) & &
( ui . directoryListWidget - > item ( i - 1 ) - > flags ( ) & Qt : : ItemIsEnabled ) )
{
const auto item = ui . directoryListWidget - > takeItem ( i ) ;
ui . directoryListWidget - > insertItem ( i - 1 , item ) ;
ui . directoryListWidget - > setCurrentRow ( i ) ;
}
}
}
void Launcher : : DataFilesPage : : moveDirectory ( int step )
{
int selectedRow = ui . directoryListWidget - > currentRow ( ) ;
int newRow = selectedRow + step ;
if ( selectedRow = = - 1 | | newRow < 0 | | newRow > ui . directoryListWidget - > count ( ) - 1 )
return ;
if ( ! ( ui . directoryListWidget - > item ( newRow ) - > flags ( ) & Qt : : ItemIsEnabled ) )
return ;
const auto item = ui . directoryListWidget - > takeItem ( selectedRow ) ;
ui . directoryListWidget - > insertItem ( newRow , item ) ;
ui . directoryListWidget - > setCurrentRow ( newRow ) ;
}
void Launcher : : DataFilesPage : : removeDirectory ( )
{
for ( const auto & path : ui . directoryListWidget - > selectedItems ( ) )
ui . directoryListWidget - > takeItem ( ui . directoryListWidget - > row ( path ) ) ;
refreshDataFilesView ( ) ;
}
void Launcher : : DataFilesPage : : moveArchive ( int step )
{
int selectedRow = ui . archiveListWidget - > currentRow ( ) ;
int newRow = selectedRow + step ;
if ( selectedRow = = - 1 | | newRow < 0 | | newRow > ui . archiveListWidget - > count ( ) - 1 )
return ;
const auto * item = ui . archiveListWidget - > takeItem ( selectedRow ) ;
addArchive ( item - > text ( ) , item - > checkState ( ) , newRow ) ;
ui . archiveListWidget - > setCurrentRow ( newRow ) ;
}
void Launcher : : DataFilesPage : : addArchive ( const QString & name , Qt : : CheckState selected , int row )
{
if ( row = = - 1 )
row = ui . archiveListWidget - > count ( ) ;
ui . archiveListWidget - > insertItem ( row , name ) ;
ui . archiveListWidget - > item ( row ) - > setCheckState ( selected ) ;
if ( mKnownArchives . filter ( name ) . isEmpty ( ) ) // XXX why contains doesn't work here ???
ui . archiveListWidget - > item ( row ) - > setBackground ( Qt : : green ) ;
}
void Launcher : : DataFilesPage : : addArchivesFromDir ( const QString & path )
{
QDir dir ( path , " *.bsa " ) ;
for ( const auto & fileinfo : dir . entryInfoList ( ) )
{
const auto absPath = fileinfo . absoluteFilePath ( ) ;
if ( Bsa : : CompressedBSAFile : : detectVersion ( absPath . toStdString ( ) ) = = Bsa : : BSAVER_UNKNOWN )
continue ;
const auto fileName = fileinfo . fileName ( ) ;
const auto currentList = selectedArchivePaths ( true ) ;
if ( ! currentList . contains ( fileName , Qt : : CaseInsensitive ) )
addArchive ( fileName , Qt : : Unchecked ) ;
}
}
void Launcher : : DataFilesPage : : checkForDefaultProfile ( )
{
//don't allow deleting "Default" profile