#include #include #include #include "model/datafilesmodel.hpp" #include "model/esm/esmfile.hpp" #include "settings/gamesettings.hpp" #include "utils/profilescombobox.hpp" #include "utils/lineedit.hpp" #include "utils/naturalsort.hpp" #include "utils/textinputdialog.hpp" #include "datafilespage.hpp" #include /** * Workaround for problems with whitespaces in paths in older versions of Boost library */ #if (BOOST_VERSION <= 104600) namespace boost { template<> inline boost::filesystem::path lexical_cast(const std::string& arg) { return boost::filesystem::path(arg); } } /* namespace boost */ #endif /* (BOOST_VERSION <= 104600) */ using namespace ESM; using namespace std; //sort QModelIndexList ascending bool rowGreaterThan(const QModelIndex &index1, const QModelIndex &index2) { return index1.row() >= index2.row(); } //sort QModelIndexList descending bool rowSmallerThan(const QModelIndex &index1, const QModelIndex &index2) { return index1.row() <= index2.row(); } DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, GameSettings &gameSettings, QWidget *parent) : mCfgMgr(cfg) , mGameSettings(gameSettings) , QWidget(parent) { // Models mMastersModel = new DataFilesModel(this); mPluginsModel = new DataFilesModel(this); mPluginsProxyModel = new QSortFilterProxyModel(); mPluginsProxyModel->setDynamicSortFilter(true); mPluginsProxyModel->setSourceModel(mPluginsModel); // Filter toolbar QLabel *filterLabel = new QLabel(tr("&Filter:"), this); LineEdit *filterLineEdit = new LineEdit(this); filterLabel->setBuddy(filterLineEdit); QToolBar *filterToolBar = new QToolBar(this); filterToolBar->setMovable(false); // Create a container widget and a layout to get the spacer to work QWidget *filterWidget = new QWidget(this); QHBoxLayout *filterLayout = new QHBoxLayout(filterWidget); QSpacerItem *hSpacer1 = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); filterLayout->addItem(hSpacer1); filterLayout->addWidget(filterLabel); filterLayout->addWidget(filterLineEdit); filterToolBar->addWidget(filterWidget); QCheckBox checkBox; unsigned int height = checkBox.sizeHint().height() + 4; mMastersTable = new QTableView(this); mMastersTable->setModel(mMastersModel); mMastersTable->setObjectName("MastersTable"); mMastersTable->setSelectionBehavior(QAbstractItemView::SelectRows); mMastersTable->setSelectionMode(QAbstractItemView::SingleSelection); mMastersTable->setEditTriggers(QAbstractItemView::NoEditTriggers); mMastersTable->setAlternatingRowColors(true); mMastersTable->horizontalHeader()->setStretchLastSection(true); mMastersTable->horizontalHeader()->hide(); // Set the row height to the size of the checkboxes mMastersTable->verticalHeader()->setDefaultSectionSize(height); mMastersTable->verticalHeader()->setResizeMode(QHeaderView::Fixed); mMastersTable->verticalHeader()->hide(); mMastersTable->setColumnHidden(1, true); mMastersTable->setColumnHidden(2, true); mMastersTable->setColumnHidden(3, true); mMastersTable->setColumnHidden(4, true); mMastersTable->setColumnHidden(5, true); mMastersTable->setColumnHidden(6, true); mMastersTable->setColumnHidden(7, true); mMastersTable->setColumnHidden(8, true); mPluginsTable = new QTableView(this); mPluginsTable->setModel(mPluginsProxyModel); mPluginsTable->setObjectName("PluginsTable"); mPluginsTable->setContextMenuPolicy(Qt::CustomContextMenu); mPluginsTable->setSelectionBehavior(QAbstractItemView::SelectRows); mPluginsTable->setSelectionMode(QAbstractItemView::SingleSelection); mPluginsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); mPluginsTable->setAlternatingRowColors(true); mPluginsTable->setVerticalScrollMode(QAbstractItemView::ScrollPerItem); mPluginsTable->horizontalHeader()->setStretchLastSection(true); mPluginsTable->horizontalHeader()->hide(); mPluginsTable->verticalHeader()->setDefaultSectionSize(height); mPluginsTable->verticalHeader()->setResizeMode(QHeaderView::Fixed); mPluginsTable->setColumnHidden(1, true); mPluginsTable->setColumnHidden(2, true); mPluginsTable->setColumnHidden(3, true); mPluginsTable->setColumnHidden(4, true); mPluginsTable->setColumnHidden(5, true); mPluginsTable->setColumnHidden(6, true); mPluginsTable->setColumnHidden(7, true); mPluginsTable->setColumnHidden(8, true); // Add both tables to a splitter QSplitter *splitter = new QSplitter(this); splitter->setOrientation(Qt::Horizontal); splitter->addWidget(mMastersTable); splitter->addWidget(mPluginsTable); // Adjust the default widget widths inside the splitter QList sizeList; sizeList << 175 << 200; splitter->setSizes(sizeList); // Bottom part with profile options QLabel *profileLabel = new QLabel(tr("Current Profile: "), this); mProfilesComboBox = new ProfilesComboBox(this); mProfilesComboBox->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum)); mProfilesComboBox->setInsertPolicy(QComboBox::NoInsert); mProfilesComboBox->setDuplicatesEnabled(false); mProfilesComboBox->setEditEnabled(false); mProfileToolBar = new QToolBar(this); mProfileToolBar->setMovable(false); mProfileToolBar->setIconSize(QSize(16, 16)); mProfileToolBar->addWidget(profileLabel); mProfileToolBar->addWidget(mProfilesComboBox); QVBoxLayout *pageLayout = new QVBoxLayout(this); pageLayout->addWidget(filterToolBar); pageLayout->addWidget(splitter); pageLayout->addWidget(mProfileToolBar); // Create a dialog for the new profile name input mNewProfileDialog = new TextInputDialog(tr("New Profile"), tr("Profile name:"), this); connect(mNewProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(updateOkButton(QString))); connect(mPluginsTable, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(setCheckState(QModelIndex))); connect(mMastersTable, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(setCheckState(QModelIndex))); connect(mMastersModel, SIGNAL(checkedItemsChanged(QStringList,QStringList)), mPluginsModel, SLOT(slotcheckedItemsChanged(QStringList,QStringList))); connect(filterLineEdit, SIGNAL(textChanged(QString)), this, SLOT(filterChanged(QString))); connect(mPluginsTable, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showContextMenu(QPoint))); connect(mProfilesComboBox, SIGNAL(profileRenamed(QString,QString)), this, SLOT(profileRenamed(QString,QString))); connect(mProfilesComboBox, SIGNAL(profileChanged(QString,QString)), this, SLOT(profileChanged(QString,QString))); createActions(); setupConfig(); setupDataFiles(); } void DataFilesPage::createActions() { // Refresh the plugins QAction *refreshAction = new QAction(tr("Refresh"), this); refreshAction->setShortcut(QKeySequence(tr("F5"))); connect(refreshAction, SIGNAL(triggered()), this, SLOT(refresh())); // Profile actions mNewProfileAction = new QAction(QIcon::fromTheme("document-new"), tr("&New Profile"), this); mNewProfileAction->setToolTip(tr("New Profile")); mNewProfileAction->setShortcut(QKeySequence(tr("Ctrl+N"))); connect(mNewProfileAction, SIGNAL(triggered()), this, SLOT(newProfile())); mDeleteProfileAction = new QAction(QIcon::fromTheme("edit-delete"), tr("Delete Profile"), this); mDeleteProfileAction->setToolTip(tr("Delete Profile")); mDeleteProfileAction->setShortcut(QKeySequence(tr("Delete"))); connect(mDeleteProfileAction, SIGNAL(triggered()), this, SLOT(deleteProfile())); // Add the newly created actions to the toolbar mProfileToolBar->addSeparator(); mProfileToolBar->addAction(mNewProfileAction); mProfileToolBar->addAction(mDeleteProfileAction); // Context menu actions mCheckAction = new QAction(tr("Check selected"), this); connect(mCheckAction, SIGNAL(triggered()), this, SLOT(check())); mUncheckAction = new QAction(tr("Uncheck selected"), this); connect(mUncheckAction, SIGNAL(triggered()), this, SLOT(uncheck())); // Context menu for the plugins table mContextMenu = new QMenu(this); mContextMenu->addAction(mCheckAction); mContextMenu->addAction(mUncheckAction); } void DataFilesPage::setupConfig() { // Open our config file QString config = QString::fromStdString((mCfgMgr.getUserPath() / "launcher.cfg").string()); mLauncherConfig = new QSettings(config, QSettings::IniFormat); // Make sure we have no groups open while (!mLauncherConfig->group().isEmpty()) { mLauncherConfig->endGroup(); } mLauncherConfig->beginGroup("Profiles"); QStringList profiles = mLauncherConfig->childGroups(); // Add the profiles to the combobox foreach (const QString &profile, profiles) { if (profile.contains(QRegExp("[^a-zA-Z0-9_]"))) continue; // Profile name contains garbage qDebug() << "adding " << profile; mProfilesComboBox->addItem(profile); } // Add a default profile if (mProfilesComboBox->findText(QString("Default")) == -1) { mProfilesComboBox->addItem(QString("Default")); } QString currentProfile = mLauncherConfig->value("CurrentProfile").toString(); if (currentProfile.isEmpty()) { // No current profile selected currentProfile = "Default"; } const int currentIndex = mProfilesComboBox->findText(currentProfile); if (currentIndex != -1) { // Profile is found mProfilesComboBox->setCurrentIndex(currentIndex); } mLauncherConfig->endGroup(); } void DataFilesPage::readConfig() { // Don't read the config if no masters are found if (mMastersModel->rowCount() < 1) return; QString profile = mProfilesComboBox->currentText(); // Make sure we have no groups open while (!mLauncherConfig->group().isEmpty()) { mLauncherConfig->endGroup(); } mLauncherConfig->beginGroup("Profiles"); mLauncherConfig->beginGroup(profile); QStringList childKeys = mLauncherConfig->childKeys(); QStringList plugins; // Sort the child keys numerical instead of alphabetically // i.e. Plugin1, Plugin2 instead of Plugin1, Plugin10 qSort(childKeys.begin(), childKeys.end(), naturalSortLessThanCI); foreach (const QString &key, childKeys) { const QString keyValue = mLauncherConfig->value(key).toString(); if (key.startsWith("Plugin")) { //QStringList checked = mPluginsModel->checkedItems(); EsmFile *file = mPluginsModel->findItem(keyValue); QModelIndex index = mPluginsModel->indexFromItem(file); mPluginsModel->setCheckState(index, Qt::Checked); // Move the row to the top of te view //mPluginsModel->moveRow(index.row(), checked.count()); plugins << keyValue; } if (key.startsWith("Master")) { EsmFile *file = mMastersModel->findItem(keyValue); mMastersModel->setCheckState(mMastersModel->indexFromItem(file), Qt::Checked); } } qDebug() << plugins; // // Set the checked item positions // const QStringList checked = mPluginsModel->checkedItems(); // for (int i = 0; i < plugins.size(); ++i) { // EsmFile *file = mPluginsModel->findItem(plugins.at(i)); // QModelIndex index = mPluginsModel->indexFromItem(file); // mPluginsModel->moveRow(index.row(), i); // qDebug() << "Moving: " << plugins.at(i) << " from: " << index.row() << " to: " << i << " count: " << checked.count(); // } // Iterate over the plugins to set their checkstate and position // for (int i = 0; i < plugins.size(); ++i) { // const QString plugin = plugins.at(i); // const QList pluginList = mPluginsModel->findItems(plugin); // if (!pluginList.isEmpty()) // { // foreach (const QStandardItem *currentPlugin, pluginList) { // mPluginsModel->setData(currentPlugin->index(), Qt::Checked, Qt::CheckStateRole); // // Move the plugin to the position specified in the config file // mPluginsModel->insertRow(i, mPluginsModel->takeRow(currentPlugin->row())); // } // } // } } void DataFilesPage::setupDataFiles() { // Set the encoding to the one found in openmw.cfg or the default mMastersModel->setEncoding(mGameSettings.value(QString("encoding"), QString("win1252"))); mPluginsModel->setEncoding(mGameSettings.value(QString("encoding"), QString("win1252"))); QStringList paths = mGameSettings.getDataDirs(); foreach (const QString &path, paths) { mMastersModel->addMasters(path); mPluginsModel->addPlugins(path); } QString dataLocal = mGameSettings.getDataLocal(); if (!dataLocal.isEmpty()) { mMastersModel->addMasters(dataLocal); mPluginsModel->addPlugins(dataLocal); } } void DataFilesPage::writeConfig(QString profile) { // // Don't overwrite the config if no masters are found // if (mMastersModel->rowCount() < 1) // return; // QString pathStr = QString::fromStdString(mCfgMgr.getUserPath().string()); // QDir userPath(pathStr); // if (!userPath.exists()) { // if (!userPath.mkpath(pathStr)) { // QMessageBox msgBox; // msgBox.setWindowTitle("Error creating OpenMW configuration directory"); // msgBox.setIcon(QMessageBox::Critical); // msgBox.setStandardButtons(QMessageBox::Ok); // msgBox.setText(tr("
Could not create %0

\ // Please make sure you have the right permissions and try again.
").arg(pathStr)); // msgBox.exec(); // qApp->quit(); // return; // } // } // // Open the OpenMW config as a QFile // QFile file(pathStr.append("openmw.cfg")); // if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { // // File cannot be opened or created // QMessageBox msgBox; // msgBox.setWindowTitle("Error writing OpenMW configuration file"); // msgBox.setIcon(QMessageBox::Critical); // msgBox.setStandardButtons(QMessageBox::Ok); // msgBox.setText(tr("
Could not open or create %0

\ // Please make sure you have the right permissions and try again.
").arg(file.fileName())); // msgBox.exec(); // qApp->quit(); // return; // } // QTextStream in(&file); // QByteArray buffer; // // Remove all previous entries from config // while (!in.atEnd()) { // QString line = in.readLine(); // if (!line.startsWith("master") && // !line.startsWith("plugin") && // !line.startsWith("data") && // !line.startsWith("data-local")) // { // buffer += line += "\n"; // } // } // file.close(); // // Now we write back the other config entries // if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { // QMessageBox msgBox; // msgBox.setWindowTitle("Error writing OpenMW configuration file"); // msgBox.setIcon(QMessageBox::Critical); // msgBox.setStandardButtons(QMessageBox::Ok); // msgBox.setText(tr("
Could not write to %0

\ // Please make sure you have the right permissions and try again.
").arg(file.fileName())); // msgBox.exec(); // qApp->quit(); // return; // } // if (!buffer.isEmpty()) { // file.write(buffer); // } // QTextStream gameConfig(&file); // QString path; // // data= directories // for (Files::PathContainer::iterator it = mDataDirs.begin(); it != mDataDirs.end(); ++it) { // path = QString::fromStdString(it->string()); // path.remove(QChar('\"')); // // Make sure the string is quoted when it contains spaces // if (path.contains(" ")) { // gameConfig << "data=\"" << path << "\"" << endl; // } else { // gameConfig << "data=" << path << endl; // } // } // // data-local directory // if (!mDataLocal.empty()) { // path = QString::fromStdString(mDataLocal.front().string()); // path.remove(QChar('\"')); // if (path.contains(" ")) { // gameConfig << "data-local=\"" << path << "\"" << endl; // } else { // gameConfig << "data-local=" << path << endl; // } // } // if (profile.isEmpty()) // profile = mProfilesComboBox->currentText(); // if (profile.isEmpty()) // return; // // Make sure we have no groups open // while (!mLauncherConfig->group().isEmpty()) { // mLauncherConfig->endGroup(); // } // mLauncherConfig->beginGroup("Profiles"); // mLauncherConfig->setValue("CurrentProfile", profile); // // Open the profile-name subgroup // mLauncherConfig->beginGroup(profile); // mLauncherConfig->remove(""); // Clear the subgroup // // Now write the masters to the configs // const QStringList masters = mMastersModel->checkedItems(); // // We don't use foreach because we need i // for (int i = 0; i < masters.size(); ++i) { // const QString currentMaster = masters.at(i); // mLauncherConfig->setValue(QString("Master%0").arg(i), currentMaster); // gameConfig << "master=" << currentMaster << endl; // } // // And finally write all checked plugins // const QStringList plugins = mPluginsModel->checkedItems(); // for (int i = 0; i < plugins.size(); ++i) { // const QString currentPlugin = plugins.at(i); // mLauncherConfig->setValue(QString("Plugin%1").arg(i), currentPlugin); // gameConfig << "plugin=" << currentPlugin << endl; // } // file.close(); // mLauncherConfig->endGroup(); // mLauncherConfig->endGroup(); // mLauncherConfig->sync(); } void DataFilesPage::newProfile() { if (mNewProfileDialog->exec() == QDialog::Accepted) { const QString text = mNewProfileDialog->lineEdit()->text(); mProfilesComboBox->addItem(text); // Copy the currently checked items to cfg writeConfig(text); mLauncherConfig->sync(); mProfilesComboBox->setCurrentIndex(mProfilesComboBox->findText(text)); } } void DataFilesPage::updateOkButton(const QString &text) { if (text.isEmpty()) { mNewProfileDialog->setOkButtonEnabled(false); return; } (mProfilesComboBox->findText(text) == -1) ? mNewProfileDialog->setOkButtonEnabled(true) : mNewProfileDialog->setOkButtonEnabled(false); } void DataFilesPage::deleteProfile() { QString profile = mProfilesComboBox->currentText(); if (profile.isEmpty()) return; QMessageBox msgBox(this); msgBox.setWindowTitle(tr("Delete Profile")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Cancel); msgBox.setText(tr("Are you sure you want to delete %0?").arg(profile)); QAbstractButton *deleteButton = msgBox.addButton(tr("Delete"), QMessageBox::ActionRole); msgBox.exec(); if (msgBox.clickedButton() == deleteButton) { // Make sure we have no groups open while (!mLauncherConfig->group().isEmpty()) { mLauncherConfig->endGroup(); } mLauncherConfig->beginGroup("Profiles"); // Open the profile-name subgroup mLauncherConfig->beginGroup(profile); mLauncherConfig->remove(""); // Clear the subgroup mLauncherConfig->endGroup(); mLauncherConfig->endGroup(); // Remove the profile from the combobox mProfilesComboBox->removeItem(mProfilesComboBox->findText(profile)); } } void DataFilesPage::check() { // Check the current selection if (!mPluginsTable->selectionModel()->hasSelection()) { return; } QModelIndexList indexes = mPluginsTable->selectionModel()->selectedIndexes(); //sort selection ascending because selectedIndexes returns an unsorted list //qSort(indexes.begin(), indexes.end(), rowSmallerThan); foreach (const QModelIndex &index, indexes) { if (!index.isValid()) return; mPluginsModel->setCheckState(index, Qt::Checked); } } void DataFilesPage::uncheck() { // uncheck the current selection if (!mPluginsTable->selectionModel()->hasSelection()) { return; } QModelIndexList indexes = mPluginsTable->selectionModel()->selectedIndexes(); //sort selection ascending because selectedIndexes returns an unsorted list //qSort(indexes.begin(), indexes.end(), rowSmallerThan); foreach (const QModelIndex &index, indexes) { if (!index.isValid()) return; mPluginsModel->setCheckState(index, Qt::Unchecked); } } void DataFilesPage::refresh() { mPluginsModel->sort(0); // Refresh the plugins table mPluginsTable->scrollToTop(); writeConfig(); readConfig(); } void DataFilesPage::setCheckState(QModelIndex index) { if (!index.isValid()) return; QObject *object = QObject::sender(); // Not a signal-slot call if (!object) return; if (object->objectName() == QLatin1String("PluginsTable")) { QModelIndex sourceIndex = mPluginsProxyModel->mapToSource(index); (mPluginsModel->checkState(sourceIndex) == Qt::Checked) ? mPluginsModel->setCheckState(sourceIndex, Qt::Unchecked) : mPluginsModel->setCheckState(sourceIndex, Qt::Checked); } if (object->objectName() == QLatin1String("MastersTable")) { (mMastersModel->checkState(index) == Qt::Checked) ? mMastersModel->setCheckState(index, Qt::Unchecked) : mMastersModel->setCheckState(index, Qt::Checked); } return; } void DataFilesPage::filterChanged(const QString filter) { QRegExp regExp(filter, Qt::CaseInsensitive, QRegExp::FixedString); mPluginsProxyModel->setFilterRegExp(regExp); } void DataFilesPage::profileChanged(const QString &previous, const QString ¤t) { qDebug() << "Profile is changed from: " << previous << " to " << current; // Prevent the deletion of the default profile if (current == QLatin1String("Default")) { mDeleteProfileAction->setEnabled(false); mProfilesComboBox->setEditEnabled(false); } else { mDeleteProfileAction->setEnabled(true); mProfilesComboBox->setEditEnabled(true); } if (!previous.isEmpty()) { writeConfig(previous); mLauncherConfig->sync(); if (mProfilesComboBox->currentIndex() == -1) return; } else { return; } mMastersModel->uncheckAll(); mPluginsModel->uncheckAll(); readConfig(); } void DataFilesPage::profileRenamed(const QString &previous, const QString ¤t) { if (previous.isEmpty()) return; // Save the new profile name writeConfig(current); // Make sure we have no groups open while (!mLauncherConfig->group().isEmpty()) { mLauncherConfig->endGroup(); } mLauncherConfig->beginGroup("Profiles"); // Open the profile-name subgroup mLauncherConfig->beginGroup(previous); mLauncherConfig->remove(""); // Clear the subgroup mLauncherConfig->endGroup(); mLauncherConfig->endGroup(); mLauncherConfig->sync(); // Remove the profile from the combobox mProfilesComboBox->removeItem(mProfilesComboBox->findText(previous)); mMastersModel->uncheckAll(); mPluginsModel->uncheckAll(); readConfig(); } void DataFilesPage::showContextMenu(const QPoint &point) { // Make sure there are plugins in the view if (!mPluginsTable->selectionModel()->hasSelection()) { return; } QPoint globalPos = mPluginsTable->mapToGlobal(point); QModelIndexList indexes = mPluginsTable->selectionModel()->selectedIndexes(); // Show the check/uncheck actions depending on the state of the selected items mUncheckAction->setEnabled(false); mCheckAction->setEnabled(false); foreach (const QModelIndex &index, indexes) { if (!index.isValid()) return; (mPluginsModel->checkState(index) == Qt::Checked) ? mUncheckAction->setEnabled(true) : mCheckAction->setEnabled(true); } // Show menu mContextMenu->exec(globalPos); }