/*
Copyright (C) 2012 Sebastian Herbord. All rights reserved.
This file is part of Mod Organizer.
Mod Organizer is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Mod Organizer is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Mod Organizer. If not, see .
*/
#include "editexecutablesdialog.h"
#include "ui_editexecutablesdialog.h"
#include "filedialogmemory.h"
#include "stackdata.h"
#include "modlist.h"
#include "forcedloaddialog.h"
#include "organizercore.h"
#include
#include
#include
#include
using namespace MOBase;
using namespace MOShared;
EditExecutablesDialog::EditExecutablesDialog(OrganizerCore& oc, QWidget* parent)
: TutorableDialog("EditExecutables", parent)
, ui(new Ui::EditExecutablesDialog)
, m_organizerCore(oc)
, m_originalExecutables(*oc.executablesList())
, m_executablesList(*oc.executablesList())
, m_settingUI(false)
{
ui->setupUi(this);
ui->splitter->setSizes({200, 1});
ui->splitter->setStretchFactor(0, 0);
ui->splitter->setStretchFactor(1, 1);
m_customOverwrites.load(m_organizerCore.currentProfile(), m_executablesList);
m_forcedLibraries.load(m_organizerCore.currentProfile(), m_executablesList);
fillExecutableList();
ui->mods->addItems(m_organizerCore.modList()->allMods());
// some widgets need to do more than just save() and have their own handler
connect(ui->binary, &QLineEdit::textChanged, [&]{ save(); });
connect(ui->workingDirectory, &QLineEdit::textChanged, [&]{ save(); });
connect(ui->arguments, &QLineEdit::textChanged, [&]{ save(); });
connect(ui->steamAppID, &QLineEdit::textChanged, [&]{ save(); });
connect(ui->mods, &QComboBox::currentTextChanged, [&]{ save(); });
connect(ui->useApplicationIcon, &QCheckBox::toggled, [&]{ save(); });
// select the first one in the list, if any
if (ui->list->count() > 0) {
ui->list->item(0)->setSelected(true);
} else {
updateUI(nullptr);
}
}
EditExecutablesDialog::~EditExecutablesDialog() = default;
ExecutablesList EditExecutablesDialog::getExecutablesList() const
{
ExecutablesList newList;
// make sure the executables are in the same order as in the list
for (int i = 0; i < ui->list->count(); ++i) {
const auto& title = ui->list->item(i)->text();
auto itor = m_executablesList.find(title);
if (itor == m_executablesList.end()) {
qWarning().nospace()
<< "getExecutablesList(): executable '" << title << "' not found";
continue;
}
newList.setExecutable(*itor);
}
return newList;
}
const CustomOverwrites& EditExecutablesDialog::getCustomOverwrites() const
{
return m_customOverwrites;
}
const ForcedLibraries& EditExecutablesDialog::getForcedLibraries() const
{
return m_forcedLibraries;
}
void EditExecutablesDialog::commitChanges()
{
const auto newExecutables = getExecutablesList();
auto* profile = m_organizerCore.currentProfile();
// remove all the custom overwrites and forced libraries
for (const auto& e : m_originalExecutables) {
profile->removeSetting("custom_overwrites", e.title());
profile->removeForcedLibraries(e.title());
}
// set the new custom overwrites and forced libraries
for (const auto& e : newExecutables) {
if (auto info=m_customOverwrites.find(e.title())) {
if (info && info->enabled) {
profile->storeSetting("custom_overwrites", e.title(), info->modName);
}
}
if (auto info=m_forcedLibraries.find(e.title())) {
if (info && info->enabled && !info->list.empty()) {
profile->setForcedLibrariesEnabled(e.title(), true);
profile->storeForcedLibraries(e.title(), info->list);
}
}
}
// set the new executables list
m_organizerCore.setExecutablesList(newExecutables);
}
QListWidgetItem* EditExecutablesDialog::selectedItem()
{
const auto selection = ui->list->selectedItems();
if (selection.empty()) {
return nullptr;
}
return selection[0];
}
Executable* EditExecutablesDialog::selectedExe()
{
auto* item = selectedItem();
if (!item) {
return nullptr;
}
const auto& title = item->text();
auto itor = m_executablesList.find(title);
if (itor == m_executablesList.end()) {
return nullptr;
}
return &*itor;
}
void EditExecutablesDialog::fillExecutableList()
{
ui->list->clear();
for(const auto& exe : m_executablesList) {
ui->list->addItem(createListItem(exe));
}
}
QListWidgetItem* EditExecutablesDialog::createListItem(const Executable& exe)
{
QListWidgetItem *newItem = new QListWidgetItem(exe.title());
if (!exe.isCustom()) {
auto f = newItem->font();
f.setItalic(true);
newItem->setFont(f);
}
return newItem;
}
void EditExecutablesDialog::updateUI(const Executable* e)
{
// the ui is currently being set, ignore changes
m_settingUI = true;
if (e) {
setEdits(*e);
ui->remove->setEnabled(e->isCustom());
} else {
clearEdits();
ui->remove->setEnabled(false);
}
// any changes from now on are from the user
m_settingUI = false;
}
void EditExecutablesDialog::clearEdits()
{
ui->title->clear();
ui->title->setEnabled(false);
ui->binary->clear();
ui->binary->setEnabled(false);
ui->browseBinary->setEnabled(false);
ui->workingDirectory->clear();
ui->workingDirectory->setEnabled(false);
ui->browseWorkingDirectory->setEnabled(false);
ui->arguments->clear();
ui->arguments->setEnabled(false);
ui->overwriteSteamAppID->setEnabled(false);
ui->overwriteSteamAppID->setChecked(false);
ui->steamAppID->setEnabled(false);
ui->steamAppID->clear();
ui->createFilesInMod->setEnabled(false);
ui->createFilesInMod->setChecked(false);
ui->mods->setEnabled(false);
ui->mods->setCurrentIndex(-1);
ui->forceLoadLibraries->setEnabled(false);
ui->forceLoadLibraries->setChecked(false);
ui->configureLibraries->setEnabled(false);
ui->useApplicationIcon->setEnabled(false);
ui->useApplicationIcon->setChecked(false);
ui->pluginProvidedLabel->setVisible(false);
}
void EditExecutablesDialog::setEdits(const Executable& e)
{
ui->title->setText(e.title());
ui->binary->setText(QDir::toNativeSeparators(e.binaryInfo().absoluteFilePath()));
ui->workingDirectory->setText(QDir::toNativeSeparators(e.workingDirectory()));
ui->arguments->setText(e.arguments());
ui->overwriteSteamAppID->setChecked(!e.steamAppID().isEmpty());
ui->steamAppID->setEnabled(!e.steamAppID().isEmpty());
ui->steamAppID->setText(e.steamAppID());
ui->useApplicationIcon->setChecked(e.usesOwnIcon());
{
int modIndex = -1;
const auto info = m_customOverwrites.find(e.title());
if (info && !info->modName.isEmpty()) {
modIndex = ui->mods->findText(info->modName);
if (modIndex == -1) {
qWarning().nospace()
<< "executable '" << e.title() << "' uses mod '" << info->modName << "' "
<< "as a custom overwrite, but that mod doesn't exist";
}
}
const bool hasCustomOverwrites = (info && info->enabled);
ui->createFilesInMod->setChecked(hasCustomOverwrites);
ui->mods->setEnabled(hasCustomOverwrites);
ui->mods->setCurrentIndex(modIndex);
}
{
const auto info = m_forcedLibraries.find(e.title());
const bool hasForcedLibraries = (info && info->enabled);
ui->forceLoadLibraries->setChecked(hasForcedLibraries);
ui->configureLibraries->setEnabled(hasForcedLibraries);
}
ui->pluginProvidedLabel->setVisible(!e.isCustom());
// only enabled for custom executables
ui->title->setEnabled(e.isCustom());
ui->binary->setEnabled(e.isCustom());
ui->browseBinary->setEnabled(e.isCustom());
ui->workingDirectory->setEnabled(e.isCustom());
ui->browseWorkingDirectory->setEnabled(e.isCustom());
ui->arguments->setEnabled(e.isCustom());
ui->overwriteSteamAppID->setEnabled(e.isCustom());
ui->useApplicationIcon->setEnabled(e.isCustom());
// always enabled
ui->createFilesInMod->setEnabled(true);
ui->forceLoadLibraries->setEnabled(true);
}
void EditExecutablesDialog::save()
{
if (m_settingUI) {
return;
}
auto* e = selectedExe();
if (!e) {
qWarning("trying to save but nothing is selected");
return;
}
qDebug().nospace() << "saving '" << e->title() << "'";
// title may have changed, start with the stuff using it
// custom overwrites
if (ui->createFilesInMod->isChecked()) {
m_customOverwrites.setEnabled(e->title(), true);
m_customOverwrites.setMod(e->title(), ui->mods->currentText());
} else {
m_customOverwrites.setEnabled(e->title(), false);
}
// forced libraries
m_forcedLibraries.setEnabled(e->title(), ui->forceLoadLibraries->isChecked());
// get the new title, but ignore it if it's conflicting with an already
// existing executable
QString newTitle = ui->title->text();
if (isTitleConflicting(newTitle)) {
newTitle = e->title();
}
if (e->title() != newTitle) {
// now rename both the custom overwrites and forced libraries if the title
// is being changed
m_customOverwrites.rename(e->title(), newTitle);
m_forcedLibraries.rename(e->title(), newTitle);
// save the new title
e->title(newTitle);
}
e->binaryInfo(ui->binary->text());
e->workingDirectory(ui->workingDirectory->text());
e->arguments(ui->arguments->text());
if (ui->overwriteSteamAppID->isChecked()) {
e->steamAppID(ui->steamAppID->text());
} else {
e->steamAppID("");
}
if (ui->useApplicationIcon->isChecked()) {
e->flags(e->flags() | Executable::UseApplicationIcon);
} else {
e->flags(e->flags() & (~Executable::UseApplicationIcon));
}
}
void EditExecutablesDialog::on_list_itemSelectionChanged()
{
updateUI(selectedExe());
}
void EditExecutablesDialog::on_add_clicked()
{
auto title = makeNonConflictingTitle(tr("New Executable"));
if (!title) {
return;
}
auto e = Executable()
.title(*title)
.flags(Executable::CustomExecutable);
m_executablesList.setExecutable(e);
auto* item = createListItem(e);
ui->list->addItem(item);
item->setSelected(true);
}
void EditExecutablesDialog::on_remove_clicked()
{
auto* item = selectedItem();
if (!item) {
qWarning("trying to remove entry but nothing is selected");
return;
}
auto* exe = selectedExe();
if (!exe) {
qWarning("trying to remove entry but nothing is selected");
return;
}
const int currentRow = ui->list->row(item);
delete item;
m_customOverwrites.remove(exe->title());
m_forcedLibraries.remove(exe->title());
// removing from main list, must be done last because it invalidates the
// exe pointer
m_executablesList.remove(exe->title());
// reselecting the same row as before, or the last one
if (currentRow >= ui->list->count()) {
// that was the last item, select the new list item, if any
if (ui->list->count() > 0) {
ui->list->item(ui->list->count() - 1)->setSelected(true);
}
} else {
ui->list->item(currentRow)->setSelected(true);
}
}
bool EditExecutablesDialog::isTitleConflicting(const QString& s)
{
for (const auto& exe : m_executablesList) {
if (exe.title() == s) {
if (&exe != selectedExe()) {
// found an executable that's not the current one with the same title
return true;
}
}
}
return false;
}
void EditExecutablesDialog::on_title_textChanged(const QString& s)
{
if (m_settingUI) {
return;
}
// don't allow changing the title to something that already exists
if (isTitleConflicting(s)) {
return;
}
// must save before modifying the item in the list widget because saving
// relies on the item's text being the same as an item in m_executablesList
save();
// once the executable is saved, the list item must be changed to match the
// new name
if (auto* i=selectedItem()) {
i->setText(s);
}
}
void EditExecutablesDialog::on_overwriteSteamAppID_toggled(bool checked)
{
if (m_settingUI) {
return;
}
ui->steamAppID->setEnabled(checked);
save();
}
void EditExecutablesDialog::on_createFilesInMod_toggled(bool checked)
{
if (m_settingUI) {
return;
}
ui->mods->setEnabled(checked);
save();
}
void EditExecutablesDialog::on_forceLoadLibraries_toggled(bool checked)
{
if (m_settingUI) {
return;
}
ui->configureLibraries->setEnabled(ui->forceLoadLibraries->isChecked());
save();
}
void EditExecutablesDialog::on_browseBinary_clicked()
{
const QString binaryName = FileDialogMemory::getOpenFileName(
"editExecutableBinary", this, tr("Select a binary"), ui->binary->text(),
tr("Executable (%1)").arg("*.exe *.bat *.jar"));
if (binaryName.isNull()) {
// canceled
return;
}
if (binaryName.endsWith(".jar", Qt::CaseInsensitive)) {
setJarBinary(binaryName);
} else {
ui->binary->setText(QDir::toNativeSeparators(binaryName));
}
if (ui->title->text().isEmpty()) {
const auto prefix = QFileInfo(binaryName).baseName();
const auto newTitle = makeNonConflictingTitle(prefix);
if (newTitle) {
ui->title->setText(*newTitle);
}
}
save();
}
void EditExecutablesDialog::on_browseWorkingDirectory_clicked()
{
QString dirName = FileDialogMemory::getExistingDirectory(
"editExecutableDirectory", this, tr("Select a directory"),
ui->workingDirectory->text());
if (dirName.isNull()) {
// canceled
return;
}
ui->workingDirectory->setText(dirName);
}
void EditExecutablesDialog::on_configureLibraries_clicked()
{
auto* e = selectedExe();
if (!e) {
qWarning("trying to configure libraries but nothing is selected");
return;
}
ForcedLoadDialog dialog(m_organizerCore.managedGame(), this);
if (auto info=m_forcedLibraries.find(e->title())) {
dialog.setValues(info->list);
}
if (dialog.exec() == QDialog::Accepted) {
m_forcedLibraries.setList(e->title(), dialog.values());
save();
}
}
void EditExecutablesDialog::on_buttons_accepted()
{
commitChanges();
accept();
}
void EditExecutablesDialog::on_buttons_rejected()
{
reject();
}
void EditExecutablesDialog::setJarBinary(const QString& binaryName)
{
auto java = OrganizerCore::findJavaInstallation(binaryName);
if (java.isEmpty()) {
QMessageBox::information(
this, tr("Java (32-bit) required"),
tr("MO requires 32-bit java to run this application. If you already "
"have it installed, select javaw.exe from that installation as "
"the binary."));
}
// only save once
m_settingUI = true;
ui->binary->setText(java);
ui->workingDirectory->setText(QDir::toNativeSeparators(QFileInfo(binaryName).absolutePath()));
ui->arguments->setText("-jar \"" + QDir::toNativeSeparators(binaryName) + "\"");
m_settingUI = false;
save();
}
std::optional EditExecutablesDialog::makeNonConflictingTitle(
const QString& prefix)
{
QString title = prefix;
for (int i=1; i<100; ++i) {
if (!m_executablesList.titleExists(title)) {
return title;
}
title = prefix + QString(" (%1)").arg(i);
}
qCritical().nospace()
<< "ran out of executable titles for prefix '" << prefix << "'";
return {};
}
void CustomOverwrites::load(Profile* p, const ExecutablesList& exes)
{
for (const auto& e : exes) {
const auto s = p->setting("custom_overwrites", e.title()).toString();
if (!s.isEmpty()) {
m_map[e.title()] = {true, s};
}
}
}
std::optional CustomOverwrites::find(
const QString& title) const
{
auto itor = m_map.find(title);
if (itor == m_map.end()) {
return {};
}
return itor->second;
}
void CustomOverwrites::setEnabled(const QString& title, bool b)
{
auto itor = m_map.find(title);
if (itor == m_map.end()) {
m_map[title] = {b, {}};
} else {
itor->second.enabled = b;
}
}
void CustomOverwrites::setMod(const QString& title, const QString& mod)
{
auto itor = m_map.find(title);
if (itor == m_map.end()) {
m_map[title] = {true, mod};
} else {
itor->second.modName = mod;
}
}
void CustomOverwrites::rename(const QString& oldTitle, const QString& newTitle)
{
auto itor = m_map.find(oldTitle);
if (itor == m_map.end()) {
return;
}
// copy to new title, erase old
m_map[newTitle] = itor->second;
m_map.erase(itor);
}
void CustomOverwrites::remove(const QString& title)
{
auto itor = m_map.find(title);
if (itor != m_map.end()) {
m_map.erase(itor);
}
}
void ForcedLibraries::load(Profile* p, const ExecutablesList& exes)
{
for (const auto& e : exes) {
if (p->forcedLibrariesEnabled(e.title())) {
m_map[e.title()] = {true, p->determineForcedLibraries(e.title())};
}
}
}
std::optional ForcedLibraries::find(
const QString& title) const
{
auto itor = m_map.find(title);
if (itor == m_map.end()) {
return {};
}
return itor->second;
}
void ForcedLibraries::setEnabled(const QString& title, bool b)
{
auto itor = m_map.find(title);
if (itor == m_map.end()) {
m_map[title] = {b, {}};
} else {
itor->second.enabled = b;
}
}
void ForcedLibraries::setList(const QString& title, const list_type& list)
{
auto itor = m_map.find(title);
if (itor == m_map.end()) {
m_map[title] = {true, list};
} else {
itor->second.list = list;
}
}
void ForcedLibraries::rename(const QString& oldTitle, const QString& newTitle)
{
auto itor = m_map.find(oldTitle);
if (itor == m_map.end()) {
return;
}
// copy to new title, erase old
m_map[newTitle] = itor->second;
m_map.erase(itor);
}
void ForcedLibraries::remove(const QString& title)
{
auto itor = m_map.find(title);
if (itor != m_map.end()) {
m_map.erase(itor);
}
}