diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e2d00e1b..e6e9bb80b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -399,7 +399,7 @@ endif (CMAKE_COMPILER_IS_GNUCC) # Apple bundling # TODO REWRITE! if (APPLE) - set(MISC_FILES + set(MISC_FILES ${APP_BUNDLE_DIR}/Contents/MacOS/openmw.cfg ${APP_BUNDLE_DIR}/Contents/MacOS/plugins.cfg) @@ -484,6 +484,8 @@ if (BUILD_ESMTOOL) add_subdirectory( apps/esmtool ) endif() +add_subdirectory( apps/launcher ) + if (WIN32) if (MSVC) if (USE_DEBUG_CONSOLE) diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt new file mode 100644 index 000000000..5090c167d --- /dev/null +++ b/apps/launcher/CMakeLists.txt @@ -0,0 +1,79 @@ +set(LAUNCHER + datafilespage.cpp + graphicspage.cpp + lineedit.cpp + main.cpp + maindialog.cpp + naturalsort.cpp + playpage.cpp + pluginsmodel.cpp + pluginsview.cpp +) + +set(LAUNCHER_HEADER + combobox.hpp + datafilespage.hpp + graphicspage.hpp + lineedit.hpp + maindialog.hpp + naturalsort.hpp + playpage.hpp + pluginsmodel.hpp + pluginsview.hpp +) + +# Headers that must be pre-processed +set(LAUNCHER_HEADER_MOC + combobox.hpp + datafilespage.hpp + graphicspage.hpp + lineedit.hpp + maindialog.hpp + playpage.hpp + pluginsmodel.hpp + pluginsview.hpp +) + +source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER} ${LAUNCHER_HEADER_MOC}) + +find_package(Qt4 REQUIRED) +set(QT_USE_QTGUI 1) + +find_package(PNG REQUIRED) +include_directories(${PNG_INCLUDE_DIR}) + +QT4_ADD_RESOURCES(RCC_SRCS resources.qrc) +QT4_WRAP_CPP(MOC_SRCS ${LAUNCHER_HEADER_MOC}) + +include(${QT_USE_FILE}) + +# Main executable +add_executable(omwlauncher + ${LAUNCHER} + ${MISC} ${MISC_HEADER} + ${FILES} ${FILES_HEADER} + ${TO_UTF8} + ${ESM} + ${RCC_SRCS} + ${MOC_SRCS} +) + +target_link_libraries(omwlauncher + ${Boost_LIBRARIES} + ${OGRE_LIBRARIES} + ${QT_LIBRARIES} + ${PNG_LIBRARY} +) + +if (APPLE) + configure_file(${CMAKE_SOURCE_DIR}/files/launcher.qss + "${APP_BUNDLE_DIR}/../launcher.qss") + configure_file(${CMAKE_SOURCE_DIR}/files/launcher.qss + "${APP_BUNDLE_DIR}/../launcher.cfg") +else() + configure_file(${CMAKE_SOURCE_DIR}/files/launcher.qss + "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/launcher.qss") + + configure_file(${CMAKE_SOURCE_DIR}/files/launcher.cfg + "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/launcher.cfg") +endif() diff --git a/apps/launcher/combobox.hpp b/apps/launcher/combobox.hpp new file mode 100644 index 000000000..bc99575d7 --- /dev/null +++ b/apps/launcher/combobox.hpp @@ -0,0 +1,28 @@ +#ifndef COMBOBOX_H +#define COMBOBOX_H + +#include + +class ComboBox : public QComboBox +{ + Q_OBJECT +private: + QString oldText; +public: + ComboBox(QWidget *parent=0) : QComboBox(parent), oldText() + { + connect(this,SIGNAL(editTextChanged(const QString&)), this, + SLOT(textChangedSlot(const QString&))); + connect(this,SIGNAL(currentIndexChanged(const QString&)), this, + SLOT(textChangedSlot(const QString&))); + } +private slots: + void textChangedSlot(const QString &newText) + { + emit textChanged(oldText, newText); + oldText = newText; + } +signals: + void textChanged(const QString &oldText, const QString &newText); +}; +#endif \ No newline at end of file diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp new file mode 100644 index 000000000..24cb1b3cf --- /dev/null +++ b/apps/launcher/datafilespage.cpp @@ -0,0 +1,978 @@ +#include + +#include +#include +#include +#include + +#include "datafilespage.hpp" +#include "lineedit.hpp" +#include "naturalsort.hpp" +#include "pluginsmodel.hpp" +#include "pluginsview.hpp" + +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(QWidget *parent) : QWidget(parent) +{ + mDataFilesModel = new QStandardItemModel(); // Contains all plugins with masters + mPluginsModel = new PluginsModel(); // Contains selectable plugins + + mPluginsProxyModel = new QSortFilterProxyModel(); + mPluginsProxyModel->setDynamicSortFilter(true); + mPluginsProxyModel->setSourceModel(mPluginsModel); + + 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); + + mMastersWidget = new QTableWidget(this); // Contains the available masters + mMastersWidget->setObjectName("MastersWidget"); + mMastersWidget->setSelectionBehavior(QAbstractItemView::SelectRows); + mMastersWidget->setSelectionMode(QAbstractItemView::MultiSelection); + mMastersWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); + mMastersWidget->setAlternatingRowColors(true); + mMastersWidget->horizontalHeader()->setStretchLastSection(true); + mMastersWidget->horizontalHeader()->hide(); + mMastersWidget->verticalHeader()->hide(); + mMastersWidget->insertColumn(0); + + mPluginsTable = new PluginsView(this); + mPluginsTable->setModel(mPluginsProxyModel); + mPluginsTable->setVerticalScrollMode(QAbstractItemView::ScrollPerItem); + + mPluginsTable->horizontalHeader()->setStretchLastSection(true); + mPluginsTable->horizontalHeader()->hide(); + + // Set the row height to the size of the checkboxes + QCheckBox checkBox; + unsigned int height = checkBox.sizeHint().height() + 4; + + mPluginsTable->verticalHeader()->setDefaultSectionSize(height); + + // Add both tables to a splitter + QSplitter *splitter = new QSplitter(this); + splitter->setOrientation(Qt::Horizontal); + + splitter->addWidget(mMastersWidget); + splitter->addWidget(mPluginsTable); + + // Adjust the default widget widths inside the splitter + QList sizeList; + sizeList << 100 << 300; + splitter->setSizes(sizeList); + + // Bottom part with profile options + QLabel *profileLabel = new QLabel(tr("Current Profile: "), this); + + mProfilesComboBox = new ComboBox(this); + mProfilesComboBox->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum)); + mProfilesComboBox->setInsertPolicy(QComboBox::InsertAtBottom); + + 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); + + connect(mMastersWidget->selectionModel(), + SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), + this, SLOT(masterSelectionChanged(const QItemSelection&, const QItemSelection&))); + + connect(filterLineEdit, SIGNAL(textChanged(QString)), this, SLOT(filterChanged(const QString))); + + connect(mPluginsTable, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(setCheckState(QModelIndex))); + connect(mPluginsTable, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(showContextMenu(const QPoint&))); + + + + setupConfig(); + createActions(); +} + +void DataFilesPage::setupDataFiles(const QStringList &paths, bool strict) +{ + // Put the paths in a boost::filesystem vector to use with Files::Collections + std::vector dataDirs; + + foreach (const QString ¤tPath, paths) { + dataDirs.push_back(boost::filesystem::path(currentPath.toStdString())); + } + + // Create a file collection for the dataDirs + Files::Collections mFileCollections(dataDirs, strict); + + // First we add all the master files + const Files::MultiDirCollection &esm = mFileCollections.getCollection(".esm"); + unsigned int i = 0; // Row number + + for (Files::MultiDirCollection::TIter iter(esm.begin()); iter!=esm.end(); ++iter) + { + std::string filename = boost::filesystem::path (iter->second.filename()).string(); + QString currentMaster = QString::fromStdString(filename); + + const QList itemList = mMastersWidget->findItems(currentMaster, Qt::MatchExactly); + + if (itemList.isEmpty()) // Master is not yet in the widget + { + mMastersWidget->insertRow(i); + QTableWidgetItem *item = new QTableWidgetItem(currentMaster); + mMastersWidget->setItem(i, 0, item); + ++i; + } + } + + // Now on to the plugins + const Files::MultiDirCollection &esp = mFileCollections.getCollection(".esp"); + + for (Files::MultiDirCollection::TIter iter(esp.begin()); iter!=esp.end(); ++iter) + { + ESMReader fileReader; + QStringList availableMasters; // Will contain all found masters + + fileReader.open(iter->second.string()); + + // First we fill the availableMasters and the mMastersWidget + ESMReader::MasterList mlist = fileReader.getMasters(); + + for (unsigned int i = 0; i < mlist.size(); ++i) { + const QString currentMaster = QString::fromStdString(mlist[i].name); + availableMasters.append(currentMaster); + + const QList itemList = mMastersWidget->findItems(currentMaster, Qt::MatchExactly); + + if (itemList.isEmpty()) // Master is not yet in the widget + { + // TODO: Show warning, missing master + mMastersWidget->insertRow(i); + QTableWidgetItem *item = new QTableWidgetItem(currentMaster); + mMastersWidget->setItem(i, 0, item); + } + } + + availableMasters.sort(); // Sort the masters alphabetically + + // Now we put the current plugin in the mDataFilesModel under its masters + QStandardItem *parent = new QStandardItem(availableMasters.join(",")); + + std::string filename = boost::filesystem::path (iter->second.filename()).string(); + QStandardItem *child = new QStandardItem(QString::fromStdString(std::string(filename))); + + const QList masterList = mDataFilesModel->findItems(availableMasters.join(",")); + + if (masterList.isEmpty()) { // Masters node not yet in the mDataFilesModel + parent->appendRow(child); + mDataFilesModel->appendRow(parent); + } else { + // Masters node exists, append current plugin + foreach (QStandardItem *currentItem, masterList) { + currentItem->appendRow(child); + } + } + } + + readConfig(); +} + +void DataFilesPage::setupConfig() +{ + QString config = "./launcher.cfg"; + QFile file(config); + + if (!file.exists()) { + config = QString::fromStdString(Files::getPath(Files::Path_ConfigUser, + "openmw", "launcher.cfg")); + } + + file.setFileName(config); // Just for displaying information + + // Open our config file + mLauncherConfig = new QSettings(config, QSettings::IniFormat); + mLauncherConfig->sync(); + + + // Make sure we have no groups open + while (!mLauncherConfig->group().isEmpty()) { + mLauncherConfig->endGroup(); + } + + mLauncherConfig->beginGroup("Profiles"); + QStringList profiles = mLauncherConfig->childGroups(); + + if (profiles.isEmpty()) { + // Add a default profile + profiles.append("Default"); + } + + mProfilesComboBox->addItems(profiles); + + QString currentProfile = mLauncherConfig->value("CurrentProfile").toString(); + + if (currentProfile.isEmpty()) { + // No current profile selected + currentProfile = "Default"; + } + mProfilesComboBox->setCurrentIndex(mProfilesComboBox->findText(currentProfile)); + + mLauncherConfig->endGroup(); + + // Now we connect the combobox to do something if the profile changes + // This prevents strange behaviour while reading and appending the profiles + connect(mProfilesComboBox, SIGNAL(textChanged(const QString&, const QString&)), this, SLOT(profileChanged(const QString&, const QString&))); +} + +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())); + + + mCopyProfileAction = new QAction(QIcon::fromTheme("edit-copy"), tr("&Copy Profile"), this); + mCopyProfileAction->setToolTip(tr("Copy Profile")); + mCopyProfileAction->setShortcut(QKeySequence(tr("Ctrl+C"))); + connect(mCopyProfileAction, SIGNAL(triggered()), this, SLOT(copyProfile())); + + 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(mCopyProfileAction); + mProfileToolBar->addAction(mDeleteProfileAction); + + // Context menu actions + mMoveUpAction = new QAction(QIcon::fromTheme("go-up"), tr("Move &Up"), this); + mMoveUpAction->setShortcut(QKeySequence(tr("Ctrl+U"))); + connect(mMoveUpAction, SIGNAL(triggered()), this, SLOT(moveUp())); + + mMoveDownAction = new QAction(QIcon::fromTheme("go-down"), tr("&Move Down"), this); + mMoveDownAction->setShortcut(QKeySequence(tr("Ctrl+M"))); + connect(mMoveDownAction, SIGNAL(triggered()), this, SLOT(moveDown())); + + mMoveTopAction = new QAction(QIcon::fromTheme("go-top"), tr("Move to Top"), this); + mMoveTopAction->setShortcut(QKeySequence(tr("Ctrl+Shift+U"))); + connect(mMoveTopAction, SIGNAL(triggered()), this, SLOT(moveTop())); + + mMoveBottomAction = new QAction(QIcon::fromTheme("go-bottom"), tr("Move to Bottom"), this); + mMoveBottomAction->setShortcut(QKeySequence(tr("Ctrl+Shift+M"))); + connect(mMoveBottomAction, SIGNAL(triggered()), this, SLOT(moveBottom())); + + 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())); + + // Makes shortcuts work even if the context menu is hidden + this->addAction(refreshAction); + this->addAction(mMoveUpAction); + this->addAction(mMoveDownAction); + this->addAction(mMoveTopAction); + this->addAction(mMoveBottomAction); + + // Context menu for the plugins table + mContextMenu = new QMenu(this); + + mContextMenu->addAction(mMoveUpAction); + mContextMenu->addAction(mMoveDownAction); + mContextMenu->addSeparator(); + mContextMenu->addAction(mMoveTopAction); + mContextMenu->addAction(mMoveBottomAction); + mContextMenu->addSeparator(); + mContextMenu->addAction(mCheckAction); + mContextMenu->addAction(mUncheckAction); + +} + +void DataFilesPage::newProfile() +{ + bool ok; + QString text = QInputDialog::getText(this, tr("New Profile"), + tr("Profile Name:"), QLineEdit::Normal, + tr("New Profile"), &ok); + if (ok && !text.isEmpty()) { + if (mProfilesComboBox->findText(text) != -1) { + QMessageBox::warning(this, tr("Profile already exists"), + tr("the profile %0 already exists.").arg(text), + QMessageBox::Ok); + } else { + // Add the new profile to the combobox + mProfilesComboBox->addItem(text); + mProfilesComboBox->setCurrentIndex(mProfilesComboBox->findText(text)); + + } + + } + +} + +void DataFilesPage::copyProfile() +{ + QString profile = mProfilesComboBox->currentText(); + bool ok; + + QString text = QInputDialog::getText(this, tr("Copy Profile"), + tr("Profile Name:"), QLineEdit::Normal, + tr("%0 Copy").arg(profile), &ok); + if (ok && !text.isEmpty()) { + if (mProfilesComboBox->findText(text) != -1) { + QMessageBox::warning(this, tr("Profile already exists"), + tr("the profile %0 already exists.").arg(text), + QMessageBox::Ok); + } else { + // Add the new profile to the combobox + mProfilesComboBox->addItem(text); + + // First write the current profile as the new profile + writeConfig(text); + mProfilesComboBox->setCurrentIndex(mProfilesComboBox->findText(text)); + + } + + } + +} + +void DataFilesPage::deleteProfile() +{ + QString profile = mProfilesComboBox->currentText(); + + + if (profile.isEmpty()) { + return; + } + + QMessageBox deleteMessageBox(this); + deleteMessageBox.setWindowTitle(tr("Delete Profile")); + deleteMessageBox.setText(tr("Are you sure you want to delete %0?").arg(profile)); + deleteMessageBox.setIcon(QMessageBox::Warning); + QAbstractButton *deleteButton = + deleteMessageBox.addButton(tr("Delete"), QMessageBox::ActionRole); + + deleteMessageBox.addButton(QMessageBox::Cancel); + + deleteMessageBox.exec(); + + if (deleteMessageBox.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::moveUp() +{ + // Shift the selected plugins up one row + + if (!mPluginsTable->selectionModel()->hasSelection()) { + return; + } + + QModelIndexList selectedIndexes = mPluginsTable->selectionModel()->selectedIndexes(); + + //sort selection ascending because selectedIndexes returns an unsorted list + qSort(selectedIndexes.begin(), selectedIndexes.end(), rowSmallerThan); + + QModelIndex firstIndex = mPluginsProxyModel->mapToSource(selectedIndexes.first()); + + // Check if the first selected plugin is the top one + if (firstIndex.row() == 0) { + return; + } + + foreach (const QModelIndex ¤tIndex, selectedIndexes) { + const QModelIndex sourceModelIndex = mPluginsProxyModel->mapToSource(currentIndex); + int currentRow = sourceModelIndex.row(); + + if (sourceModelIndex.isValid() && currentRow > 0) { + mPluginsModel->insertRow((currentRow - 1), mPluginsModel->takeRow(currentRow)); + + const QModelIndex targetIndex = mPluginsModel->index((currentRow - 1), 0, QModelIndex()); + + mPluginsTable->selectionModel()->select(targetIndex, QItemSelectionModel::Select | QItemSelectionModel::Rows); + scrollToSelection(); + } + } +} + +void DataFilesPage::moveDown() +{ + // Shift the selected plugins down one row + + if (!mPluginsTable->selectionModel()->hasSelection()) { + return; + } + + QModelIndexList selectedIndexes = mPluginsTable->selectionModel()->selectedIndexes(); + + //sort selection descending because selectedIndexes returns an unsorted list + qSort(selectedIndexes.begin(), selectedIndexes.end(), rowGreaterThan); + + const QModelIndex lastIndex = mPluginsProxyModel->mapToSource(selectedIndexes.first()); + + // Check if last selected plugin is bottom one + if ((lastIndex.row() + 1) == mPluginsModel->rowCount()) { + return; + } + + foreach (const QModelIndex ¤tIndex, selectedIndexes) { + const QModelIndex sourceModelIndex = mPluginsProxyModel->mapToSource(currentIndex); + int currentRow = sourceModelIndex.row(); + + if (sourceModelIndex.isValid() && (currentRow + 1) < mPluginsModel->rowCount()) { + mPluginsModel->insertRow((currentRow + 1), mPluginsModel->takeRow(currentRow)); + + const QModelIndex targetIndex = mPluginsModel->index((currentRow + 1), 0, QModelIndex()); + + mPluginsTable->selectionModel()->select(targetIndex, QItemSelectionModel::Select | QItemSelectionModel::Rows); + scrollToSelection(); + } + } +} + +void DataFilesPage::moveTop() +{ + // Move the selection to the top of the table + + if (!mPluginsTable->selectionModel()->hasSelection()) { + return; + } + + QModelIndexList selectedIndexes = mPluginsTable->selectionModel()->selectedIndexes(); + + //sort selection ascending because selectedIndexes returns an unsorted list + qSort(selectedIndexes.begin(), selectedIndexes.end(), rowSmallerThan); + + QModelIndex firstIndex = mPluginsProxyModel->mapToSource(selectedIndexes.first()); + + // Check if the first selected plugin is the top one + if (firstIndex.row() == 0) { + return; + } + + for (int i=0; i < selectedIndexes.count(); ++i) { + + const QModelIndex sourceModelIndex = mPluginsProxyModel->mapToSource(selectedIndexes.at(i)); + + int currentRow = sourceModelIndex.row(); + + if (sourceModelIndex.isValid() && currentRow > 0) { + + mPluginsModel->insertRow(i, mPluginsModel->takeRow(currentRow)); + mPluginsTable->selectionModel()->select(mPluginsModel->index(i, 0, QModelIndex()), QItemSelectionModel::Select | QItemSelectionModel::Rows); + mPluginsTable->scrollToTop(); + } + } +} + +void DataFilesPage::moveBottom() +{ + // Move the selection to the bottom of the table + + if (!mPluginsTable->selectionModel()->hasSelection()) { + return; + } + + QModelIndexList selectedIndexes = mPluginsTable->selectionModel()->selectedIndexes(); + + //sort selection descending because selectedIndexes returns an unsorted list + qSort(selectedIndexes.begin(), selectedIndexes.end(), rowSmallerThan); + + const QModelIndex lastIndex = mPluginsProxyModel->mapToSource(selectedIndexes.last()); + + // Check if last selected plugin is bottom one + if ((lastIndex.row() + 1) == mPluginsModel->rowCount()) { + return; + } + + for (int i=0; i < selectedIndexes.count(); ++i) { + + const QModelIndex sourceModelIndex = mPluginsProxyModel->mapToSource(selectedIndexes.at(i)); + + // Subtract iterations because takeRow shifts the rows below the taken row up + int currentRow = sourceModelIndex.row() - i; + + if (sourceModelIndex.isValid() && (currentRow + 1) < mPluginsModel->rowCount()) { + mPluginsModel->appendRow(mPluginsModel->takeRow(currentRow)); + + // Rowcount starts with 1, row numbers start with 0 + const QModelIndex lastRow = mPluginsModel->index((mPluginsModel->rowCount() -1), 0, QModelIndex()); + + mPluginsTable->selectionModel()->select(lastRow, QItemSelectionModel::Select | QItemSelectionModel::Rows); + mPluginsTable->scrollToBottom(); + } + } +} + +void DataFilesPage::check() +{ + // Check the current selection + + if (!mPluginsTable->selectionModel()->hasSelection()) { + return; + } + + QModelIndexList selectedIndexes = mPluginsTable->selectionModel()->selectedIndexes(); + + //sort selection ascending because selectedIndexes returns an unsorted list + qSort(selectedIndexes.begin(), selectedIndexes.end(), rowSmallerThan); + + foreach (const QModelIndex ¤tIndex, selectedIndexes) { + QModelIndex sourceModelIndex = mPluginsProxyModel->mapToSource(currentIndex); + + if (sourceModelIndex.isValid()) { + mPluginsModel->setData(sourceModelIndex, Qt::Checked, Qt::CheckStateRole); + } + } +} + +void DataFilesPage::uncheck() +{ + // Uncheck the current selection + + if (!mPluginsTable->selectionModel()->hasSelection()) { + return; + } + + QModelIndexList selectedIndexes = mPluginsTable->selectionModel()->selectedIndexes(); + + //sort selection ascending because selectedIndexes returns an unsorted list + qSort(selectedIndexes.begin(), selectedIndexes.end(), rowSmallerThan); + + foreach (const QModelIndex ¤tIndex, selectedIndexes) { + QModelIndex sourceModelIndex = mPluginsProxyModel->mapToSource(currentIndex); + + if (sourceModelIndex.isValid()) { + mPluginsModel->setData(sourceModelIndex, Qt::Unchecked, Qt::CheckStateRole); + } + } +} + +void DataFilesPage::refresh() +{ + // Refresh the plugins table + + writeConfig(); + readConfig(); +} + +void DataFilesPage::scrollToSelection() +{ + // Scroll to the selected plugins + + if (!mPluginsTable->selectionModel()->hasSelection()) { + return; + } + + // Get the selected indexes visible by determining the middle index + QModelIndexList selectedIndexes = mPluginsTable->selectionModel()->selectedIndexes(); + qSort(selectedIndexes.begin(), selectedIndexes.end(), rowSmallerThan); + + // The selected rows including non-selected inside selection + unsigned int selectedRows = selectedIndexes.last().row() - selectedIndexes.first().row(); + + // Determine the row which is roughly in the middle of the selection + unsigned int middleRow = selectedIndexes.first().row() + (int)(selectedRows / 2) + 1; + + + const QModelIndex middleIndex = mPluginsProxyModel->mapFromSource(mPluginsModel->index(middleRow, 0, QModelIndex())); + + // Make sure the whole selection is visible + mPluginsTable->scrollTo(selectedIndexes.first()); + mPluginsTable->scrollTo(selectedIndexes.last()); + mPluginsTable->scrollTo(middleIndex); +} + +void DataFilesPage::showContextMenu(const QPoint &point) +{ + + QPoint globalPos = mPluginsTable->mapToGlobal(point); + + QModelIndexList selectedIndexes = 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 ¤tIndex, selectedIndexes) { + if (currentIndex.isValid()) { + + const QModelIndex sourceIndex = mPluginsProxyModel->mapToSource(currentIndex); + + if (!sourceIndex.isValid()) { + return; + } + + const QStandardItem *currentItem = mPluginsModel->itemFromIndex(sourceIndex); + + if (currentItem->checkState() == Qt::Checked) { + mUncheckAction->setEnabled(true); + } else { + mCheckAction->setEnabled(true); + } + } + + } + + // Make sure these are enabled because they might still be disabled + mMoveUpAction->setEnabled(true); + mMoveTopAction->setEnabled(true); + mMoveDownAction->setEnabled(true); + mMoveBottomAction->setEnabled(true); + + QModelIndex firstIndex = mPluginsProxyModel->mapToSource(selectedIndexes.first()); + QModelIndex lastIndex = mPluginsProxyModel->mapToSource(selectedIndexes.last()); + + // Check if selected first item is top row in model + if (firstIndex.row() == 0) { + mMoveUpAction->setEnabled(false); + mMoveTopAction->setEnabled(false); + } + + // Check if last row is bottom row in model + if ((lastIndex.row() + 1) == mPluginsModel->rowCount()) { + mMoveDownAction->setEnabled(false); + mMoveBottomAction->setEnabled(false); + } + + // Show menu + mContextMenu->exec(globalPos); +} + +void DataFilesPage::masterSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + if (mMastersWidget->selectionModel()->hasSelection()) { + + QStringList masters; + QString masterstr; + + // Create a QStringList containing all the masters + const QStringList masterList = selectedMasters(); + + foreach (const QString ¤tMaster, masterList) { + masters.append(currentMaster); + } + + masters.sort(); + masterstr = masters.join(","); // Make a comma-separated QString + + // Iterate over all masters in the datafilesmodel to see if they are selected + for (int r=0; rrowCount(); ++r) { + QModelIndex currentIndex = mDataFilesModel->index(r, 0); + QString master = currentIndex.data().toString(); + + if (currentIndex.isValid()) { + // See if the current master is in the string with selected masters + if (masterstr.contains(master)) + { + // Append the plugins from the current master to pluginsmodel + addPlugins(currentIndex); + } + } + } + } + + // See what plugins to remove + QModelIndexList deselectedIndexes = deselected.indexes(); + + if (!deselectedIndexes.isEmpty()) { + foreach (const QModelIndex ¤tIndex, deselectedIndexes) { + + QString master = currentIndex.data().toString(); + master.prepend("*"); + master.append("*"); + const QList itemList = mDataFilesModel->findItems(master, Qt::MatchWildcard); + + foreach (const QStandardItem *currentItem, itemList) { + QModelIndex index = currentItem->index(); + removePlugins(index); + } + } + } + +} + +void DataFilesPage::addPlugins(const QModelIndex &index) +{ + // Find the plugins in the datafilesmodel and append them to the pluginsmodel + if (!index.isValid()) + return; + + for (int r=0; rrowCount(index); ++r ) { + QModelIndex childIndex = index.child(r, 0); + + if (childIndex.isValid()) { + // Now we see if the pluginsmodel already contains this plugin + const QString childIndexData = QVariant(mDataFilesModel->data(childIndex)).toString(); + + const QList itemList = mPluginsModel->findItems(childIndexData); + + if (itemList.isEmpty()) + { + // Plugin not yet in the pluginsmodel, add it + QStandardItem *item = new QStandardItem(childIndexData); + item->setFlags(item->flags() & ~(Qt::ItemIsDropEnabled)); + item->setCheckable(true); + + mPluginsModel->appendRow(item); + } + } + + } + +} + +void DataFilesPage::removePlugins(const QModelIndex &index) +{ + + if (!index.isValid()) + return; + + for (int r=0; rrowCount(index); ++r) { + QModelIndex childIndex = index.child(r, 0); + + const QList itemList = mPluginsModel->findItems(QVariant(childIndex.data()).toString()); + + if (!itemList.isEmpty()) { + foreach (const QStandardItem *currentItem, itemList) { + mPluginsModel->removeRow(currentItem->row()); + } + } + } + +} + +void DataFilesPage::setCheckState(QModelIndex index) +{ + if (!index.isValid()) + return; + + QModelIndex sourceModelIndex = mPluginsProxyModel->mapToSource(index); + + if (mPluginsModel->data(sourceModelIndex, Qt::CheckStateRole) == Qt::Checked) { + // Selected row is checked, uncheck it + mPluginsModel->setData(sourceModelIndex, Qt::Unchecked, Qt::CheckStateRole); + } else { + mPluginsModel->setData(sourceModelIndex, Qt::Checked, Qt::CheckStateRole); + } +} + +const QStringList DataFilesPage::selectedMasters() +{ + QStringList masters; + const QList selectedMasters = mMastersWidget->selectedItems(); + + foreach (const QTableWidgetItem *item, selectedMasters) { + masters.append(item->data(Qt::DisplayRole).toString()); + } + + return masters; +} + +const QStringList DataFilesPage::checkedPlugins() +{ + QStringList checkedItems; + + for (int r=0; rrowCount(); ++r ) { + QModelIndex index = mPluginsModel->index(r, 0); + + if (index.isValid()) { + // See if the current item is checked + if (mPluginsModel->data(index, Qt::CheckStateRole) == Qt::Checked) { + checkedItems.append(index.data().toString()); + } + } + } + return checkedItems; +} + +void DataFilesPage::uncheckPlugins() +{ + for (int r=0; rrowCount(); ++r ) { + QModelIndex index = mPluginsModel->index(r, 0); + + if (index.isValid()) { + // See if the current item is checked + if (mPluginsModel->data(index, Qt::CheckStateRole) == Qt::Checked) { + mPluginsModel->setData(index, Qt::Unchecked, Qt::CheckStateRole); + } + } + } +} + +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) +{ + if (!previous.isEmpty()) { + writeConfig(previous); + mLauncherConfig->sync(); + } + + uncheckPlugins(); + // Deselect the masters + mMastersWidget->selectionModel()->clearSelection(); + readConfig(); +} + +void DataFilesPage::readConfig() +{ + 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")) { + plugins.append(keyValue); + continue; + } + + if (key.startsWith("Master")) { + const QList masterList = mMastersWidget->findItems(keyValue, Qt::MatchFixedString); + + if (!masterList.isEmpty()) { + foreach (QTableWidgetItem *currentMaster, masterList) { + mMastersWidget->selectionModel()->select(mMastersWidget->model()->index(currentMaster->row(), 0), QItemSelectionModel::Select); + } + } + } + } + + // 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::writeConfig(QString profile) +{ + 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 + + // First write the masters to the config + const QStringList masterList = selectedMasters(); + + // We don't use foreach because we need i + for (int i = 0; i < masterList.size(); ++i) { + const QString master = masterList.at(i); + mLauncherConfig->setValue(QString("Master%0").arg(i), master); + } + + // Now write all checked plugins + const QStringList plugins = checkedPlugins(); + + for (int i = 0; i < plugins.size(); ++i) + { + mLauncherConfig->setValue(QString("Plugin%1").arg(i), plugins.at(i)); + } + + mLauncherConfig->endGroup(); + mLauncherConfig->endGroup(); + +} diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp new file mode 100644 index 000000000..2d0a385a7 --- /dev/null +++ b/apps/launcher/datafilespage.hpp @@ -0,0 +1,93 @@ +#ifndef DATAFILESPAGE_H +#define DATAFILESPAGE_H + +#include +#include +#include "combobox.hpp" + +class QTableWidget; +class QStandardItemModel; +class QItemSelection; +class QItemSelectionModel; +class QSortFilterProxyModel; +class QStringListModel; +class QSettings; +class QAction; +class QToolBar; +class QMenu; +class PluginsModel; +class PluginsView; +class ComboBox; + +class DataFilesPage : public QWidget +{ + Q_OBJECT + +public: + DataFilesPage(QWidget *parent = 0); + + ComboBox *mProfilesComboBox; + QSettings *mLauncherConfig; + + const QStringList checkedPlugins(); + const QStringList selectedMasters(); + void setupConfig(); + void readConfig(); + void writeConfig(QString profile = QString()); + + void setupDataFiles(const QStringList &paths, bool strict); + +public slots: + void masterSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + void setCheckState(QModelIndex index); + + void filterChanged(const QString filter); + void showContextMenu(const QPoint &point); + void profileChanged(const QString &previous, const QString ¤t); + + // Action slots + void newProfile(); + void copyProfile(); + void deleteProfile(); + void moveUp(); + void moveDown(); + void moveTop(); + void moveBottom(); + void check(); + void uncheck(); + void refresh(); + +private: + QTableWidget *mMastersWidget; + PluginsView *mPluginsTable; + + QStandardItemModel *mDataFilesModel; + PluginsModel *mPluginsModel; + + QSortFilterProxyModel *mPluginsProxyModel; + QItemSelectionModel *mPluginsSelectModel; + + QToolBar *mProfileToolBar; + QMenu *mContextMenu; + + QAction *mNewProfileAction; + QAction *mCopyProfileAction; + QAction *mDeleteProfileAction; + + QAction *mMoveUpAction; + QAction *mMoveDownAction; + QAction *mMoveTopAction; + QAction *mMoveBottomAction; + QAction *mCheckAction; + QAction *mUncheckAction; + + void addPlugins(const QModelIndex &index); + void removePlugins(const QModelIndex &index); + void uncheckPlugins(); + void createActions(); + + void scrollToSelection(); + +}; + +#endif diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp new file mode 100644 index 000000000..0074346e7 --- /dev/null +++ b/apps/launcher/graphicspage.cpp @@ -0,0 +1,473 @@ +#include + +#include + +#include "graphicspage.hpp" + +GraphicsPage::GraphicsPage(QWidget *parent) : QWidget(parent) +{ + QGroupBox *rendererGroup = new QGroupBox(tr("Renderer"), this); + + QLabel *rendererLabel = new QLabel(tr("Rendering Subsystem:"), rendererGroup); + mRendererComboBox = new QComboBox(rendererGroup); + + // Layout for the combobox and label + QGridLayout *renderSystemLayout = new QGridLayout(); + renderSystemLayout->addWidget(rendererLabel, 0, 0, 1, 1); + renderSystemLayout->addWidget(mRendererComboBox, 0, 1, 1, 1); + + mRendererStackedWidget = new QStackedWidget(rendererGroup); + + QVBoxLayout *rendererGroupLayout = new QVBoxLayout(rendererGroup); + + rendererGroupLayout->addLayout(renderSystemLayout); + rendererGroupLayout->addWidget(mRendererStackedWidget); + + // Display + QGroupBox *displayGroup = new QGroupBox(tr("Display"), this); + + mDisplayStackedWidget = new QStackedWidget(displayGroup); + + QVBoxLayout *displayGroupLayout = new QVBoxLayout(displayGroup); + QSpacerItem *vSpacer3 = new QSpacerItem(20, 10, QSizePolicy::Minimum, QSizePolicy::Expanding); + + displayGroupLayout->addWidget(mDisplayStackedWidget); + displayGroupLayout->addItem(vSpacer3); + + // Layout for the whole page + QVBoxLayout *pageLayout = new QVBoxLayout(this); + + pageLayout->addWidget(rendererGroup); + pageLayout->addWidget(displayGroup); + + connect(mRendererComboBox, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(rendererChanged(const QString&))); + + createPages(); + setupConfig(); + setupOgre(); + + readConfig(); +} + +void GraphicsPage::createPages() +{ + // OpenGL rendering settings + QWidget *mOGLRendererPage = new QWidget(); + + QLabel *OGLRTTLabel = new QLabel(tr("Preferred RTT Mode:"), mOGLRendererPage); + mOGLRTTComboBox = new QComboBox(mOGLRendererPage); + + QLabel *OGLAntiAliasingLabel = new QLabel(tr("Antialiasing:"), mOGLRendererPage); + mOGLAntiAliasingComboBox = new QComboBox(mOGLRendererPage); + + QGridLayout *OGLRendererLayout = new QGridLayout(mOGLRendererPage); + QSpacerItem *vSpacer1 = new QSpacerItem(20, 10, QSizePolicy::Minimum, QSizePolicy::Expanding); + + OGLRendererLayout->addWidget(OGLRTTLabel, 0, 0, 1, 1); + OGLRendererLayout->addWidget(mOGLRTTComboBox, 0, 1, 1, 1); + OGLRendererLayout->addWidget(OGLAntiAliasingLabel, 1, 0, 1, 1); + OGLRendererLayout->addWidget(mOGLAntiAliasingComboBox, 1, 1, 1, 1); + OGLRendererLayout->addItem(vSpacer1, 2, 1, 1, 1); + + // OpenGL display settings + QWidget *mOGLDisplayPage = new QWidget(); + + QLabel *OGLResolutionLabel = new QLabel(tr("Resolution:"), mOGLDisplayPage); + mOGLResolutionComboBox = new QComboBox(mOGLDisplayPage); + + QLabel *OGLFrequencyLabel = new QLabel(tr("Display Frequency:"), mOGLDisplayPage); + mOGLFrequencyComboBox = new QComboBox(mOGLDisplayPage); + + mOGLVSyncCheckBox = new QCheckBox(tr("Vertical Sync"), mOGLDisplayPage); + mOGLFullScreenCheckBox = new QCheckBox(tr("Full Screen"), mOGLDisplayPage); + + QGridLayout *OGLDisplayLayout = new QGridLayout(mOGLDisplayPage); + QSpacerItem *vSpacer2 = new QSpacerItem(20, 10, QSizePolicy::Minimum, QSizePolicy::Minimum); + + OGLDisplayLayout->addWidget(OGLResolutionLabel, 0, 0, 1, 1); + OGLDisplayLayout->addWidget(mOGLResolutionComboBox, 0, 1, 1, 1); + OGLDisplayLayout->addWidget(OGLFrequencyLabel, 1, 0, 1, 1); + OGLDisplayLayout->addWidget(mOGLFrequencyComboBox, 1, 1, 1, 1); + + OGLDisplayLayout->addItem(vSpacer2, 2, 1, 1, 1); + OGLDisplayLayout->addWidget(mOGLVSyncCheckBox, 3, 0, 1, 1); + OGLDisplayLayout->addWidget(mOGLFullScreenCheckBox, 6, 0, 1, 1); + + // Direct3D rendering settings + QWidget *mD3DRendererPage = new QWidget(); + + QLabel *D3DRenderDeviceLabel = new QLabel(tr("Rendering Device:"), mD3DRendererPage); + mD3DRenderDeviceComboBox = new QComboBox(mD3DRendererPage); + + QLabel *D3DAntiAliasingLabel = new QLabel(tr("Antialiasing:"), mD3DRendererPage); + mD3DAntiAliasingComboBox = new QComboBox(mD3DRendererPage); + + QLabel *D3DFloatingPointLabel = new QLabel(tr("Floating-point Mode:"), mD3DRendererPage); + mD3DFloatingPointComboBox = new QComboBox(mD3DRendererPage); + + mD3DNvPerfCheckBox = new QCheckBox(tr("Allow NVPerfHUD"), mD3DRendererPage); + + QGridLayout *D3DRendererLayout = new QGridLayout(mD3DRendererPage); + QSpacerItem *vSpacer3 = new QSpacerItem(20, 10, QSizePolicy::Minimum, QSizePolicy::Minimum); + QSpacerItem *vSpacer4 = new QSpacerItem(20, 10, QSizePolicy::Minimum, QSizePolicy::Expanding); + + D3DRendererLayout->addWidget(D3DRenderDeviceLabel, 0, 0, 1, 1); + D3DRendererLayout->addWidget(mD3DRenderDeviceComboBox, 0, 1, 1, 1); + D3DRendererLayout->addWidget(D3DAntiAliasingLabel, 1, 0, 1, 1); + D3DRendererLayout->addWidget(mD3DAntiAliasingComboBox, 1, 1, 1, 1); + D3DRendererLayout->addWidget(D3DFloatingPointLabel, 2, 0, 1, 1); + D3DRendererLayout->addWidget(mD3DFloatingPointComboBox, 2, 1, 1, 1); + D3DRendererLayout->addItem(vSpacer3, 3, 1, 1, 1); + D3DRendererLayout->addWidget(mD3DNvPerfCheckBox, 4, 0, 1, 1); + D3DRendererLayout->addItem(vSpacer4, 5, 1, 1, 1); + + // Direct3D display settings + QWidget *mD3DDisplayPage = new QWidget(); + + QLabel *D3DResolutionLabel = new QLabel(tr("Resolution:"), mD3DDisplayPage); + mD3DResolutionComboBox = new QComboBox(mD3DDisplayPage); + + mD3DVSyncCheckBox = new QCheckBox(tr("Vertical Sync"), mD3DDisplayPage); + mD3DFullScreenCheckBox = new QCheckBox(tr("Full Screen"), mD3DDisplayPage); + + QGridLayout *mD3DDisplayLayout = new QGridLayout(mD3DDisplayPage); + QSpacerItem *vSpacer5 = new QSpacerItem(20, 10, QSizePolicy::Minimum, QSizePolicy::Minimum); + + mD3DDisplayLayout->addWidget(D3DResolutionLabel, 0, 0, 1, 1); + mD3DDisplayLayout->addWidget(mD3DResolutionComboBox, 0, 1, 1, 1); + mD3DDisplayLayout->addItem(vSpacer5, 1, 1, 1, 1); + mD3DDisplayLayout->addWidget(mD3DVSyncCheckBox, 2, 0, 1, 1); + mD3DDisplayLayout->addWidget(mD3DFullScreenCheckBox, 5, 0, 1, 1); + + // Add the created pages + mRendererStackedWidget->addWidget(mOGLRendererPage); + mRendererStackedWidget->addWidget(mD3DRendererPage); + + mDisplayStackedWidget->addWidget(mOGLDisplayPage); + mDisplayStackedWidget->addWidget(mD3DDisplayPage); +} + +void GraphicsPage::setupConfig() +{ + QString ogreCfg = "./ogre.cfg"; + + QFile file(ogreCfg); + + if (!file.exists()) { + ogreCfg = QString::fromStdString(Files::getPath(Files::Path_ConfigUser, + "openmw", "ogre.cfg")); + } + + mOgreConfig = new QSettings(ogreCfg, QSettings::IniFormat); + +} + +void GraphicsPage::setupOgre() +{ + QString pluginCfg = "./plugins.cfg"; + QFile file(pluginCfg); + + if (!file.exists()) { + pluginCfg = QString::fromStdString(Files::getPath(Files::Path_ConfigUser, + "openmw", "plugins.cfg")); + } + + // Reopen the file from user directory + file.setFileName(pluginCfg); + + if (!file.exists()) { + // There's no plugins.cfg in the user directory, use global directory + pluginCfg = QString::fromStdString(Files::getPath(Files::Path_ConfigGlobal, + "openmw", "plugins.cfg")); + } + + // Create a log manager so we can surpress debug text to stdout/stderr + Ogre::LogManager* logMgr = OGRE_NEW Ogre::LogManager; + logMgr->createLog("launcherOgre.log", true, false, false); + + try + { + mOgre = new Ogre::Root(pluginCfg.toStdString()); + } + catch(Ogre::Exception &ex) + { + QString ogreError = QString::fromStdString(ex.getFullDescription().c_str()); + QMessageBox msgBox; + msgBox.setWindowTitle("Error creating Ogre::Root"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("
Failed to create the Ogre::Root object

\ + Make sure the plugins.cfg is present and valid.

\ + Press \"Show Details...\" for more information.
")); + msgBox.setDetailedText(ogreError); + msgBox.exec(); + + qCritical("Error creating Ogre::Root, the error reported was:\n %s", qPrintable(ogreError)); + + std::exit(1); + } + + // Get the available renderers and put them in the combobox + const Ogre::RenderSystemList &renderers = mOgre->getAvailableRenderers(); + + for (Ogre::RenderSystemList::const_iterator r = renderers.begin(); r != renderers.end(); ++r) { + mSelectedRenderSystem = *r; + mRendererComboBox->addItem((*r)->getName().c_str()); + } + + int index = mRendererComboBox->findText(mOgreConfig->value("Render System").toString()); + + if ( index != -1) { + mRendererComboBox->setCurrentIndex(index); + } + + // Create separate rendersystems + QString openGLName = mRendererComboBox->itemText(mRendererComboBox->findText(QString("OpenGL"), Qt::MatchStartsWith)); + QString direct3DName = mRendererComboBox->itemText(mRendererComboBox->findText(QString("Direct3D"), Qt::MatchStartsWith)); + + mOpenGLRenderSystem = mOgre->getRenderSystemByName(openGLName.toStdString()); + mDirect3DRenderSystem = mOgre->getRenderSystemByName(direct3DName.toStdString()); + + if (!mOpenGLRenderSystem && !mDirect3DRenderSystem) { + QMessageBox msgBox; + msgBox.setWindowTitle("Error creating renderer"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("
Could not select a valid render system

\ + Please make sure the plugins.cfg file exists and contains a valid rendering plugin.
")); + msgBox.exec(); + + std::exit(1); + } + + // Now fill the GUI elements + // OpenGL + if (mOpenGLRenderSystem) { + mOGLRTTComboBox->addItems(getAvailableOptions(QString("RTT Preferred Mode"), mOpenGLRenderSystem)); + mOGLAntiAliasingComboBox->addItems(getAvailableOptions(QString("FSAA"), mOpenGLRenderSystem)); + + QStringList videoModes = getAvailableOptions(QString("Video Mode"), mOpenGLRenderSystem); + // Remove extraneous spaces + videoModes.replaceInStrings(QRegExp("\\s{2,}"), QString(" ")); + videoModes.replaceInStrings(QRegExp("^\\s"), QString()); + + mOGLResolutionComboBox->addItems(videoModes); + mOGLFrequencyComboBox->addItems(getAvailableOptions(QString("Display Frequency"), mOpenGLRenderSystem)); + } + + // Direct3D + if (mDirect3DRenderSystem) { + mD3DRenderDeviceComboBox->addItems(getAvailableOptions(QString("Rendering Device"), mDirect3DRenderSystem)); + mD3DAntiAliasingComboBox->addItems(getAvailableOptions(QString("Anti aliasing"), mDirect3DRenderSystem)); + mD3DFloatingPointComboBox->addItems(getAvailableOptions(QString("Floating-point mode"), mDirect3DRenderSystem)); + + QStringList videoModes = getAvailableOptions(QString("Video Mode"), mDirect3DRenderSystem); + // Remove extraneous spaces + videoModes.replaceInStrings(QRegExp("\\s{2,}"), QString(" ")); + videoModes.replaceInStrings(QRegExp("^\\s"), QString()); + mD3DResolutionComboBox->addItems(videoModes); + } +} + +void GraphicsPage::readConfig() +{ + // Read the config file settings + if (mOpenGLRenderSystem) { + + int index = mOGLRTTComboBox->findText(getConfigValue("RTT Preferred Mode", mOpenGLRenderSystem)); + if ( index != -1) { + mOGLRTTComboBox->setCurrentIndex(index); + } + + index = mOGLAntiAliasingComboBox->findText(getConfigValue("FSAA", mOpenGLRenderSystem)); + if ( index != -1){ + mOGLAntiAliasingComboBox->setCurrentIndex(index); + } + + index = mOGLResolutionComboBox->findText(getConfigValue("Video Mode", mOpenGLRenderSystem)); + if ( index != -1) { + mOGLResolutionComboBox->setCurrentIndex(index); + } + + index = mOGLFrequencyComboBox->findText(getConfigValue("Display Frequency", mOpenGLRenderSystem)); + if ( index != -1) { + mOGLFrequencyComboBox->setCurrentIndex(index); + } + + // Now we do the same for the checkboxes + if (getConfigValue("VSync", mOpenGLRenderSystem) == QLatin1String("Yes")) { + mOGLVSyncCheckBox->setCheckState(Qt::Checked); + } + + if (getConfigValue("Full Screen", mOpenGLRenderSystem) == QLatin1String("Yes")) { + mOGLFullScreenCheckBox->setCheckState(Qt::Checked); + } + } + + if (mDirect3DRenderSystem) { + + int index = mD3DRenderDeviceComboBox->findText(getConfigValue("Rendering Device", mDirect3DRenderSystem)); + if ( index != -1) { + mD3DRenderDeviceComboBox->setCurrentIndex(index); + } + + index = mD3DAntiAliasingComboBox->findText(getConfigValue("Anti aliasing", mDirect3DRenderSystem)); + if ( index != -1) { + mD3DAntiAliasingComboBox->setCurrentIndex(index); + } + + index = mD3DFloatingPointComboBox->findText(getConfigValue("Floating-point mode", mDirect3DRenderSystem)); + if ( index != -1) { + mD3DFloatingPointComboBox->setCurrentIndex(index); + } + + index = mD3DResolutionComboBox->findText(getConfigValue("Video Mode", mDirect3DRenderSystem)); + if ( index != -1) { + mD3DResolutionComboBox->setCurrentIndex(index); + } + + if (getConfigValue("Allow NVPerfHUD", mDirect3DRenderSystem) == QLatin1String("Yes")) { + mD3DNvPerfCheckBox->setCheckState(Qt::Checked); + } + + if (getConfigValue("VSync", mDirect3DRenderSystem) == QLatin1String("Yes")) { + mD3DVSyncCheckBox->setCheckState(Qt::Checked); + } + + if (getConfigValue("Full Screen", mDirect3DRenderSystem) == QLatin1String("Yes")) { + mD3DFullScreenCheckBox->setCheckState(Qt::Checked); + } + } +} + +void GraphicsPage::writeConfig() +{ + // Write the config file settings + + // Custom write method: We cannot use QSettings because it does not accept spaces + QFile file(mOgreConfig->fileName()); + + if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { + // File could not be opened, + QMessageBox msgBox; + msgBox.setWindowTitle("Error opening Ogre configuration file"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("
Could not open %0

\ + Please make sure you have the right permissions and try again.
").arg(file.fileName())); + msgBox.exec(); + return; + } + + QTextStream out(&file); + + out << "Render System=" << mSelectedRenderSystem->getName().c_str() << endl << endl; + + if (mOpenGLRenderSystem) { + QString openGLName = mOpenGLRenderSystem->getName().c_str(); + openGLName.prepend("["); + openGLName.append("]"); + out << openGLName << endl; + + out << "RTT Preferred Mode=" << mOGLRTTComboBox->currentText() << endl; + out << "FSAA=" << mOGLAntiAliasingComboBox->currentText() << endl; + out << "Video Mode=" << mOGLResolutionComboBox->currentText() << endl; + out << "Display Frequency=" << mOGLFrequencyComboBox->currentText() << endl; + + if (mOGLVSyncCheckBox->checkState() == Qt::Checked) { + out << "VSync=Yes" << endl; + } else { + out << "VSync=No" << endl; + } + + if (mOGLFullScreenCheckBox->checkState() == Qt::Checked) { + out << "Full Screen=Yes" << endl; + } else { + out << "Full Screen=No" << endl; + } + + } + + if (mDirect3DRenderSystem) { + QString direct3DName = mDirect3DRenderSystem->getName().c_str(); + direct3DName.prepend("["); + direct3DName.append("]"); + out << direct3DName << endl; + + out << "Rendering Device=" << mD3DRenderDeviceComboBox->currentText() << endl; + out << "Anti aliasing=" << mD3DAntiAliasingComboBox->currentText() << endl; + out << "Floating-point mode=" << mD3DFloatingPointComboBox->currentText() << endl; + out << "Video Mode=" << mD3DResolutionComboBox->currentText() << endl; + + if (mD3DNvPerfCheckBox->checkState() == Qt::Checked) { + out << "Allow NVPerfHUD=Yes" << endl; + } else { + out << "Allow NVPerfHUD=No" << endl; + } + + if (mD3DVSyncCheckBox->checkState() == Qt::Checked) { + out << "VSync=Yes" << endl; + } else { + out << "VSync=No" << endl; + } + + if (mD3DFullScreenCheckBox->checkState() == Qt::Checked) { + out << "Full Screen=Yes" << endl; + } else { + out << "Full Screen=No" << endl; + } + + } + + file.close(); + +} + +QString GraphicsPage::getConfigValue(const QString &key, Ogre::RenderSystem *renderer) +{ + QString result; + + mOgreConfig->beginGroup(renderer->getName().c_str()); + result = mOgreConfig->value(key).toString(); + mOgreConfig->endGroup(); + + return result; +} + +QStringList GraphicsPage::getAvailableOptions(const QString &key, Ogre::RenderSystem *renderer) +{ + QStringList result; + + uint row = 0; + Ogre::ConfigOptionMap options = renderer->getConfigOptions(); + + for (Ogre::ConfigOptionMap::iterator i = options.begin (); i != options.end (); i++, row++) + { + Ogre::StringVector::iterator opt_it; + uint idx = 0; + for (opt_it = i->second.possibleValues.begin (); + opt_it != i->second.possibleValues.end (); opt_it++, idx++) + { + + if (strcmp (key.toStdString().c_str(), i->first.c_str()) == 0) + result << (*opt_it).c_str(); + } + + } + + return result; +} + +void GraphicsPage::rendererChanged(const QString &renderer) +{ + if (renderer.contains("Direct3D")) { + mRendererStackedWidget->setCurrentIndex(1); + mDisplayStackedWidget->setCurrentIndex(1); + } + + if (renderer.contains("OpenGL")) { + mRendererStackedWidget->setCurrentIndex(0); + mDisplayStackedWidget->setCurrentIndex(0); + } + +} \ No newline at end of file diff --git a/apps/launcher/graphicspage.hpp b/apps/launcher/graphicspage.hpp new file mode 100644 index 000000000..49ed92ac0 --- /dev/null +++ b/apps/launcher/graphicspage.hpp @@ -0,0 +1,69 @@ +#ifndef GRAPHICSPAGE_H +#define GRAPHICSPAGE_H + +#include + +#include +#include +#include +#include + +class QComboBox; +class QCheckBox; +class QStackedWidget; +class QSettings; + +class GraphicsPage : public QWidget +{ + Q_OBJECT + +public: + GraphicsPage(QWidget *parent = 0); + + QSettings *mOgreConfig; + + void writeConfig(); + +public slots: + void rendererChanged(const QString &renderer); + +private: + Ogre::Root *mOgre; + Ogre::RenderSystem *mSelectedRenderSystem; + Ogre::RenderSystem *mOpenGLRenderSystem; + Ogre::RenderSystem *mDirect3DRenderSystem; + + QComboBox *mRendererComboBox; + + QStackedWidget *mRendererStackedWidget; + QStackedWidget *mDisplayStackedWidget; + + // OpenGL + QComboBox *mOGLRTTComboBox; + QComboBox *mOGLAntiAliasingComboBox; + QComboBox *mOGLResolutionComboBox; + QComboBox *mOGLFrequencyComboBox; + + QCheckBox *mOGLVSyncCheckBox; + QCheckBox *mOGLFullScreenCheckBox; + + // Direct3D + QComboBox *mD3DRenderDeviceComboBox; + QComboBox *mD3DAntiAliasingComboBox; + QComboBox *mD3DFloatingPointComboBox; + QComboBox *mD3DResolutionComboBox; + + QCheckBox *mD3DNvPerfCheckBox; + QCheckBox *mD3DVSyncCheckBox; + QCheckBox *mD3DFullScreenCheckBox; + + QString getConfigValue(const QString &key, Ogre::RenderSystem *renderer); + QStringList getAvailableOptions(const QString &key, Ogre::RenderSystem *renderer); + + void createPages(); + void setupConfig(); + void setupOgre(); + void readConfig(); +}; + +#endif diff --git a/apps/launcher/launcher.pro b/apps/launcher/launcher.pro new file mode 100644 index 000000000..0b4e4618d --- /dev/null +++ b/apps/launcher/launcher.pro @@ -0,0 +1,30 @@ +###################################################################### +# Automatically generated by qmake (2.01a) Fri Jun 24 21:14:15 2011 +###################################################################### + +TEMPLATE = app +TARGET = +DEPENDPATH += . +INCLUDEPATH += . + +# Input +HEADERS += combobox.hpp \ + datafilespage.hpp \ + graphicspage.hpp \ + lineedit.hpp \ + maindialog.hpp \ + naturalsort.hpp \ + playpage.hpp \ + pluginsmodel.hpp \ + pluginsview.hpp +SOURCES += datafilespage.cpp \ + graphicspage.cpp \ + lineedit.cpp \ + main.cpp \ + maindialog.cpp \ + naturalsort.cpp \ + playpage.cpp \ + pluginsmodel.cpp \ + pluginsview.cpp +RESOURCES += resources.qrc +win32:RC_FILE = launcher.rc diff --git a/apps/launcher/launcher.rc b/apps/launcher/launcher.rc new file mode 100644 index 000000000..efe86e4da --- /dev/null +++ b/apps/launcher/launcher.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "resources/images/openmw.ico" diff --git a/apps/launcher/lineedit.cpp b/apps/launcher/lineedit.cpp new file mode 100644 index 000000000..254c09fce --- /dev/null +++ b/apps/launcher/lineedit.cpp @@ -0,0 +1,46 @@ +/**************************************************************************** +** +** Copyright (c) 2007 Trolltech ASA +** +** Use, modification and distribution is allowed without limitation, +** warranty, liability or support of any kind. +** +****************************************************************************/ + +#include "lineedit.hpp" +#include +#include + +LineEdit::LineEdit(QWidget *parent) + : QLineEdit(parent) +{ + clearButton = new QToolButton(this); + QPixmap pixmap(":images/clear.png"); + clearButton->setIcon(QIcon(pixmap)); + clearButton->setIconSize(pixmap.size()); + clearButton->setCursor(Qt::ArrowCursor); + clearButton->setStyleSheet("QToolButton { border: none; padding: 0px; }"); + clearButton->hide(); + connect(clearButton, SIGNAL(clicked()), this, SLOT(clear())); + connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(updateCloseButton(const QString&))); + int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + setStyleSheet(QString("QLineEdit { padding-right: %1px; } ").arg(clearButton->sizeHint().width() + frameWidth + 1)); + QSize msz = minimumSizeHint(); + setMinimumSize(qMax(msz.width(), clearButton->sizeHint().height() + frameWidth * 2 + 2), + qMax(msz.height(), clearButton->sizeHint().height() + frameWidth * 2 + 2)); +} + +void LineEdit::resizeEvent(QResizeEvent *) +{ + QSize sz = clearButton->sizeHint(); + int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + clearButton->move(rect().right() - frameWidth - sz.width(), + (rect().bottom() + 1 - sz.height())/2); +} + +void LineEdit::updateCloseButton(const QString& text) +{ + clearButton->setVisible(!text.isEmpty()); +} + + diff --git a/apps/launcher/lineedit.hpp b/apps/launcher/lineedit.hpp new file mode 100644 index 000000000..7a96a523c --- /dev/null +++ b/apps/launcher/lineedit.hpp @@ -0,0 +1,35 @@ +/**************************************************************************** +** +** Copyright (c) 2007 Trolltech ASA +** +** Use, modification and distribution is allowed without limitation, +** warranty, liability or support of any kind. +** +****************************************************************************/ + +#ifndef LINEEDIT_H +#define LINEEDIT_H + +#include + +class QToolButton; + +class LineEdit : public QLineEdit +{ + Q_OBJECT + +public: + LineEdit(QWidget *parent = 0); + +protected: + void resizeEvent(QResizeEvent *); + +private slots: + void updateCloseButton(const QString &text); + +private: + QToolButton *clearButton; +}; + +#endif // LIENEDIT_H + diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp new file mode 100644 index 000000000..4e438a4db --- /dev/null +++ b/apps/launcher/main.cpp @@ -0,0 +1,35 @@ +#include +#include +#include + +#include "maindialog.hpp" + +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()); + + #if defined(Q_OS_MAC) + if (dir.dirName() == "MacOS") { + dir.cdUp(); + dir.cdUp(); + dir.cdUp(); + } + #endif + + QDir::setCurrent(dir.absolutePath()); + + // Load the stylesheet + QFile file("./launcher.qss"); + + file.open(QFile::ReadOnly); + QString styleSheet = QLatin1String(file.readAll()); + app.setStyleSheet(styleSheet); + + MainDialog dialog; + return dialog.exec(); + +} + diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp new file mode 100644 index 000000000..f51b3b549 --- /dev/null +++ b/apps/launcher/maindialog.cpp @@ -0,0 +1,355 @@ +#include + +#include + +#include "maindialog.hpp" +#include "playpage.hpp" +#include "graphicspage.hpp" +#include "datafilespage.hpp" + +MainDialog::MainDialog() +{ + mIconWidget = new QListWidget; + mIconWidget->setObjectName("IconWidget"); + mIconWidget->setViewMode(QListView::IconMode); + mIconWidget->setWrapping(false); + mIconWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Just to be sure + mIconWidget->setIconSize(QSize(48, 48)); + mIconWidget->setMovement(QListView::Static); + + mIconWidget->setMinimumWidth(400); + mIconWidget->setFixedHeight(80); + mIconWidget->setSpacing(4); + mIconWidget->setCurrentRow(0); + mIconWidget->setFlow(QListView::LeftToRight); + + QGroupBox *groupBox = new QGroupBox(this); + QVBoxLayout *groupLayout = new QVBoxLayout(groupBox); + + mPagesWidget = new QStackedWidget(groupBox); + groupLayout->addWidget(mPagesWidget); + + QPushButton *playButton = new QPushButton(tr("Play")); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(this); + buttonBox->setStandardButtons(QDialogButtonBox::Close); + buttonBox->addButton(playButton, QDialogButtonBox::AcceptRole); + + QVBoxLayout *dialogLayout = new QVBoxLayout(this); + dialogLayout->addWidget(mIconWidget); + dialogLayout->addWidget(groupBox); + dialogLayout->addWidget(buttonBox); + + + setWindowTitle(tr("OpenMW Launcher")); + setWindowIcon(QIcon(":/images/openmw.png")); + // Remove what's this? button + setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); + setMinimumSize(QSize(575, 575)); + + connect(buttonBox, SIGNAL(rejected()), this, SLOT(close())); + connect(buttonBox, SIGNAL(accepted()), this, SLOT(play())); + + setupConfig(); + createIcons(); + createPages(); +} + +void 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(mIconWidget); + playButton->setIcon(QIcon(":/images/openmw.png")); + playButton->setText(tr("Play")); + playButton->setTextAlignment(Qt::AlignCenter); + playButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + + QListWidgetItem *graphicsButton = new QListWidgetItem(mIconWidget); + 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(mIconWidget); + dataFilesButton->setIcon(QIcon(":/images/openmw-plugin.png")); + dataFilesButton->setText(tr("Data Files")); + dataFilesButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); + dataFilesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + + connect(mIconWidget, + SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), + this, SLOT(changePage(QListWidgetItem*,QListWidgetItem*))); + +} + +void MainDialog::createPages() +{ + // Various pages + mPlayPage = new PlayPage(this); + mGraphicsPage = new GraphicsPage(this); + mDataFilesPage = new DataFilesPage(this); + + // First we retrieve all data= keys from the config + // We can't use QSettings directly because it + // does not support multiple keys with the same name + QFile file(mGameConfig->fileName()); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle("Error opening OpenMW configuration file"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("
Could not open %0

\ + Please make sure you have the right permissions and try again.
").arg(file.fileName())); + msgBox.exec(); + + QApplication::exit(); // File cannot be opened or created + } + + QTextStream in(&file); + + QStringList dataDirs; + + // Add each data= value + while (!in.atEnd()) { + QString line = in.readLine(); + + if (line.startsWith("data=")) { + dataDirs.append(line.remove("data=")); + } + } + + // Add the data-local= key + QString dataLocal = mGameConfig->value("data-local").toString(); + if (!dataLocal.isEmpty()) { + dataDirs.append(dataLocal); + } + + if (!dataDirs.isEmpty()) { + // Now pass the datadirs on to the DataFilesPage + mDataFilesPage->setupDataFiles(dataDirs, mGameConfig->value("fs-strict").toBool()); + } else { + QMessageBox msgBox; + msgBox.setWindowTitle("Error reading OpenMW configuration file"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("
Could not read the location of the data files

\ + Please make sure OpenMW is correctly configured and try again.
")); + msgBox.exec(); + + QApplication::exit(); // No data or data-local entries in openmw.cfg + } + + // Set the combobox of the play page to imitate the comobox on the datafilespage + mPlayPage->mProfilesComboBox->setModel(mDataFilesPage->mProfilesComboBox->model()); + mPlayPage->mProfilesComboBox->setCurrentIndex(mDataFilesPage->mProfilesComboBox->currentIndex()); + + // Add the pages to the stacked widget + mPagesWidget->addWidget(mPlayPage); + mPagesWidget->addWidget(mGraphicsPage); + mPagesWidget->addWidget(mDataFilesPage); + + // Select the first page + mIconWidget->setCurrentItem(mIconWidget->item(0), QItemSelectionModel::Select); + + connect(mPlayPage->mPlayButton, SIGNAL(clicked()), this, SLOT(play())); + + connect(mPlayPage->mProfilesComboBox, + SIGNAL(currentIndexChanged(int)), + this, SLOT(profileChanged(int))); + + connect(mDataFilesPage->mProfilesComboBox, + SIGNAL(currentIndexChanged(int)), + this, SLOT(profileChanged(int))); + +} + +void MainDialog::profileChanged(int index) +{ + // Just to be sure, should always have a selection + if (!mIconWidget->selectionModel()->hasSelection()) { + return; + } + + QString currentPage = mIconWidget->currentItem()->data(Qt::DisplayRole).toString(); + if (currentPage == QLatin1String("Play")) { + mDataFilesPage->mProfilesComboBox->setCurrentIndex(index); + } + + if (currentPage == QLatin1String("Data Files")) { + mPlayPage->mProfilesComboBox->setCurrentIndex(index); + } +} + +void MainDialog::changePage(QListWidgetItem *current, QListWidgetItem *previous) +{ + if (!current) + current = previous; + + mPagesWidget->setCurrentIndex(mIconWidget->row(current)); +} + +void MainDialog::closeEvent(QCloseEvent *event) +{ + // Now write all config files + writeConfig(); + event->accept(); +} + +void MainDialog::play() +{ + // First do a write of all the configs, just to be sure + writeConfig(); + +#ifdef Q_WS_WIN + QString game = "./openmw.exe"; + QFile file(game); +#elif defined(Q_WS_MAC) + QDir dir(QCoreApplication::applicationDirPath()); + QString game = dir.absoluteFilePath("openmw"); + QFile file(game); +#else + QString game = "./openmw"; + QFile file(game); +#endif + + QProcess process; + QFileInfo info(file); + + if (!file.exists()) { + QMessageBox msgBox; + msgBox.setWindowTitle("Error starting OpenMW"); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("
Could not find OpenMW

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

\ + The OpenMW application is not executable.
\ + Please make sure you have the right permissions and try again.
")); + msgBox.exec(); + + return; + } + + // Start the game + if (!process.startDetached(game)) { + QMessageBox msgBox; + msgBox.setWindowTitle("Error starting OpenMW"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("
Could not start OpenMW

\ + An error occurred while starting OpenMW.

\ + Press \"Show Details...\" for more information.
")); + msgBox.setDetailedText(process.errorString()); + msgBox.exec(); + + return; + } else { + close(); + } +} + +void MainDialog::setupConfig() +{ + // First we read the OpenMW config + QString config = "./openmw.cfg"; + QFile file(config); + + if (!file.exists()) { + config = QString::fromStdString(Files::getPath(Files::Path_ConfigUser, + "openmw", "openmw.cfg")); + } + + file.close(); + + // Open our config file + mGameConfig = new QSettings(config, QSettings::IniFormat); +} + +void MainDialog::writeConfig() +{ + // Write the profiles + mDataFilesPage->writeConfig(); + mDataFilesPage->mLauncherConfig->sync(); + + // Write the graphics settings + mGraphicsPage->writeConfig(); + mGraphicsPage->mOgreConfig->sync(); + + QStringList dataFiles = mDataFilesPage->selectedMasters(); + dataFiles.append(mDataFilesPage->checkedPlugins()); + + // Open the config as a QFile + QFile file(mGameConfig->fileName()); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + // File cannot be opened or created + QMessageBox msgBox; + msgBox.setWindowTitle("Error opening OpenMW configuration file"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("
Could not open %0

\ + Please make sure you have the right permissions and try again.
").arg(file.fileName())); + msgBox.exec(); + + return; + } + + QTextStream in(&file); + QByteArray buffer; + + // Remove all previous master/plugin entries from config + while (!in.atEnd()) { + QString line = in.readLine(); + if (!line.contains("master") && !line.contains("plugin")) { + 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(); + + return; + } + + file.write(buffer); + QTextStream out(&file); + + // Write the list of game files to the config + foreach (const QString ¤tFile, dataFiles) { + + if (currentFile.endsWith(QString(".esm"), Qt::CaseInsensitive)) { + out << "master=" << currentFile << endl; + } else if (currentFile.endsWith(QString(".esp"), Qt::CaseInsensitive)) { + out << "plugin=" << currentFile << endl; + } + } + + file.close(); +} diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp new file mode 100644 index 000000000..c6f22e336 --- /dev/null +++ b/apps/launcher/maindialog.hpp @@ -0,0 +1,46 @@ +#ifndef MAINDIALOG_H +#define MAINDIALOG_H + +#include + +class QListWidget; +class QListWidgetItem; +class QStackedWidget; +class QStringListModel; +class QSettings; + +class PlayPage; +class GraphicsPage; +class DataFilesPage; + +class MainDialog : public QDialog +{ + Q_OBJECT + +public: + MainDialog(); + +public slots: + void changePage(QListWidgetItem *current, QListWidgetItem *previous); + void play(); + void profileChanged(int index); + + +private: + void createIcons(); + void createPages(); + void setupConfig(); + void writeConfig(); + void closeEvent(QCloseEvent *event); + + QListWidget *mIconWidget; + QStackedWidget *mPagesWidget; + + PlayPage *mPlayPage; + GraphicsPage *mGraphicsPage; + DataFilesPage *mDataFilesPage; + + QSettings *mGameConfig; +}; + +#endif diff --git a/apps/launcher/naturalsort.cpp b/apps/launcher/naturalsort.cpp new file mode 100644 index 000000000..648038ed7 --- /dev/null +++ b/apps/launcher/naturalsort.cpp @@ -0,0 +1,95 @@ +/* + * This file contains code found in the QtGui module of the Qt Toolkit. + * See Qt's qfilesystemmodel source files for more information + */ + +#include "naturalsort.hpp" + +static inline QChar getNextChar(const QString &s, int location) +{ + return (location < s.length()) ? s.at(location) : QChar(); +} + +/*! + * Natural number sort, skips spaces. + * + * Examples: + * 1, 2, 10, 55, 100 + * 01.jpg, 2.jpg, 10.jpg + * + * Note on the algorithm: + * Only as many characters as necessary are looked at and at most they all + * are looked at once. + * + * Slower then QString::compare() (of course) + */ +int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs) +{ + for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2) { + // skip spaces, tabs and 0's + QChar c1 = getNextChar(s1, l1); + while (c1.isSpace()) + c1 = getNextChar(s1, ++l1); + QChar c2 = getNextChar(s2, l2); + while (c2.isSpace()) + c2 = getNextChar(s2, ++l2); + + if (c1.isDigit() && c2.isDigit()) { + while (c1.digitValue() == 0) + c1 = getNextChar(s1, ++l1); + while (c2.digitValue() == 0) + c2 = getNextChar(s2, ++l2); + + int lookAheadLocation1 = l1; + int lookAheadLocation2 = l2; + int currentReturnValue = 0; + // find the last digit, setting currentReturnValue as we go if it isn't equal + for ( + QChar lookAhead1 = c1, lookAhead2 = c2; + (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); + lookAhead1 = getNextChar(s1, ++lookAheadLocation1), + lookAhead2 = getNextChar(s2, ++lookAheadLocation2) + ) { + bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit(); + bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit(); + if (!is1ADigit && !is2ADigit) + break; + if (!is1ADigit) + return -1; + if (!is2ADigit) + return 1; + if (currentReturnValue == 0) { + if (lookAhead1 < lookAhead2) { + currentReturnValue = -1; + } else if (lookAhead1 > lookAhead2) { + currentReturnValue = 1; + } + } + } + if (currentReturnValue != 0) + return currentReturnValue; + } + + if (cs == Qt::CaseInsensitive) { + if (!c1.isLower()) c1 = c1.toLower(); + if (!c2.isLower()) c2 = c2.toLower(); + } + int r = QString::localeAwareCompare(c1, c2); + if (r < 0) + return -1; + if (r > 0) + return 1; + } + // The two strings are the same (02 == 2) so fall back to the normal sort + return QString::compare(s1, s2, cs); +} + +bool naturalSortLessThanCS( const QString &left, const QString &right ) +{ + return (naturalCompare( left, right, Qt::CaseSensitive ) < 0); +} + +bool naturalSortLessThanCI( const QString &left, const QString &right ) +{ + return (naturalCompare( left, right, Qt::CaseInsensitive ) < 0); +} diff --git a/apps/launcher/naturalsort.hpp b/apps/launcher/naturalsort.hpp new file mode 100644 index 000000000..2d314396f --- /dev/null +++ b/apps/launcher/naturalsort.hpp @@ -0,0 +1,9 @@ +#ifndef NATURALSORT_H +#define NATURALSORT_H + +#include + +bool naturalSortLessThanCS( const QString &left, const QString &right ); +bool naturalSortLessThanCI( const QString &left, const QString &right ); + +#endif \ No newline at end of file diff --git a/apps/launcher/playpage.cpp b/apps/launcher/playpage.cpp new file mode 100644 index 000000000..cb993a8fa --- /dev/null +++ b/apps/launcher/playpage.cpp @@ -0,0 +1,43 @@ +#include + +#include "playpage.hpp" + +PlayPage::PlayPage(QWidget *parent) : QWidget(parent) +{ + QWidget *playWidget = new QWidget(this); + playWidget->setObjectName("PlayGroup"); + playWidget->setFixedSize(QSize(425, 375)); + + mPlayButton = new QPushButton(tr("Play"), playWidget); + mPlayButton->setObjectName("PlayButton"); + mPlayButton->setMinimumSize(QSize(200, 50)); + + QLabel *profileLabel = new QLabel(tr("Current Profile:"), playWidget); + profileLabel->setObjectName("ProfileLabel"); + + QPlastiqueStyle *style = new QPlastiqueStyle; + mProfilesComboBox = new QComboBox(playWidget); + mProfilesComboBox->setObjectName("ProfilesComboBox"); + mProfilesComboBox->setStyle(style); + + QGridLayout *playLayout = new QGridLayout(playWidget); + + QSpacerItem *hSpacer1 = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); + QSpacerItem *hSpacer2 = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); + + QSpacerItem *vSpacer1 = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding); + QSpacerItem *vSpacer2 = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding); + + playLayout->addWidget(mPlayButton, 1, 1, 1, 1); + playLayout->addWidget(profileLabel, 2, 1, 1, 1); + playLayout->addWidget(mProfilesComboBox, 3, 1, 1, 1); + playLayout->addItem(hSpacer1, 2, 0, 1, 1); + playLayout->addItem(hSpacer2, 2, 2, 1, 1); + playLayout->addItem(vSpacer1, 0, 1, 1, 1); + playLayout->addItem(vSpacer2, 4, 1, 1, 1); + + QHBoxLayout *pageLayout = new QHBoxLayout(this); + + pageLayout->addWidget(playWidget); + +} \ No newline at end of file diff --git a/apps/launcher/playpage.hpp b/apps/launcher/playpage.hpp new file mode 100644 index 000000000..efec6f2b3 --- /dev/null +++ b/apps/launcher/playpage.hpp @@ -0,0 +1,21 @@ +#ifndef PLAYPAGE_H +#define PLAYPAGE_H + +#include + +class QComboBox; +class QPushButton; + +class PlayPage : public QWidget +{ + Q_OBJECT + +public: + PlayPage(QWidget *parent = 0); + + QComboBox *mProfilesComboBox; + QPushButton *mPlayButton; + +}; + +#endif \ No newline at end of file diff --git a/apps/launcher/pluginsmodel.cpp b/apps/launcher/pluginsmodel.cpp new file mode 100644 index 000000000..86bd53027 --- /dev/null +++ b/apps/launcher/pluginsmodel.cpp @@ -0,0 +1,149 @@ +#include +#include + +#include + +#include "pluginsmodel.hpp" + +PluginsModel::PluginsModel(QObject *parent) : QStandardItemModel(parent) +{ + +} + +void decodeDataRecursive(QDataStream &stream, QStandardItem *item) +{ + int colCount, childCount; + stream >> *item; + stream >> colCount >> childCount; + item->setColumnCount(colCount); + + int childPos = childCount; + + while(childPos > 0) { + childPos--; + QStandardItem *child = new QStandardItem(); + decodeDataRecursive(stream, child); + item->setChild( childPos / colCount, childPos % colCount, child); + } +} + +bool PluginsModel::dropMimeData(const QMimeData *data, Qt::DropAction action, + int row, int column, const QModelIndex &parent) +{ + // Code largely based on QStandardItemModel::dropMimeData + + // check if the action is supported + if (!data || !(action == Qt::CopyAction || action == Qt::MoveAction)) + return false; + + // check if the format is supported + QString format = QLatin1String("application/x-qstandarditemmodeldatalist"); + if (!data->hasFormat(format)) + return QAbstractItemModel::dropMimeData(data, action, row, column, parent); + + if (row > rowCount(parent)) + row = rowCount(parent); + if (row == -1) + row = rowCount(parent); + if (column == -1) + column = 0; + + // decode and insert + QByteArray encoded = data->data(format); + QDataStream stream(&encoded, QIODevice::ReadOnly); + + + //code based on QAbstractItemModel::decodeData + // adapted to work with QStandardItem + int top = std::numeric_limits::max(); + int left = std::numeric_limits::max(); + int bottom = 0; + int right = 0; + QVector rows, columns; + QVector items; + + while (!stream.atEnd()) { + int r, c; + QStandardItem *item = new QStandardItem(); + stream >> r >> c; + decodeDataRecursive(stream, item); + + rows.append(r); + columns.append(c); + items.append(item); + top = qMin(r, top); + left = qMin(c, left); + bottom = qMax(r, bottom); + right = qMax(c, right); + } + + // insert the dragged items into the table, use a bit array to avoid overwriting items, + // since items from different tables can have the same row and column + int dragRowCount = 0; + int dragColumnCount = right - left + 1; + + // Compute the number of continuous rows upon insertion and modify the rows to match + QVector rowsToInsert(bottom + 1); + for (int i = 0; i < rows.count(); ++i) + rowsToInsert[rows.at(i)] = 1; + for (int i = 0; i < rowsToInsert.count(); ++i) { + if (rowsToInsert[i] == 1){ + rowsToInsert[i] = dragRowCount; + ++dragRowCount; + } + } + for (int i = 0; i < rows.count(); ++i) + rows[i] = top + rowsToInsert[rows[i]]; + + QBitArray isWrittenTo(dragRowCount * dragColumnCount); + + // make space in the table for the dropped data + int colCount = columnCount(parent); + if (colCount < dragColumnCount + column) { + insertColumns(colCount, dragColumnCount + column - colCount, parent); + colCount = columnCount(parent); + } + insertRows(row, dragRowCount, parent); + + row = qMax(0, row); + column = qMax(0, column); + + QStandardItem *parentItem = itemFromIndex (parent); + if (!parentItem) + parentItem = invisibleRootItem(); + + QVector newIndexes(items.size()); + // set the data in the table + for (int j = 0; j < items.size(); ++j) { + int relativeRow = rows.at(j) - top; + int relativeColumn = columns.at(j) - left; + int destinationRow = relativeRow + row; + int destinationColumn = relativeColumn + column; + int flat = (relativeRow * dragColumnCount) + relativeColumn; + // if the item was already written to, or we just can't fit it in the table, create a new row + if (destinationColumn >= colCount || isWrittenTo.testBit(flat)) { + destinationColumn = qBound(column, destinationColumn, colCount - 1); + destinationRow = row + dragRowCount; + insertRows(row + dragRowCount, 1, parent); + flat = (dragRowCount * dragColumnCount) + relativeColumn; + isWrittenTo.resize(++dragRowCount * dragColumnCount); + } + if (!isWrittenTo.testBit(flat)) { + newIndexes[j] = index(destinationRow, destinationColumn, parentItem->index()); + isWrittenTo.setBit(flat); + } + } + + for(int k = 0; k < newIndexes.size(); k++) { + if (newIndexes.at(k).isValid()) { + parentItem->setChild(newIndexes.at(k).row(), newIndexes.at(k).column(), items.at(k)); + } else { + delete items.at(k); + } + } + + // The important part, tell the view what is dropped + emit indexesDropped(newIndexes); + + return true; +} diff --git a/apps/launcher/pluginsmodel.hpp b/apps/launcher/pluginsmodel.hpp new file mode 100644 index 000000000..41df499b5 --- /dev/null +++ b/apps/launcher/pluginsmodel.hpp @@ -0,0 +1,21 @@ +#ifndef PLUGINSMODEL_H +#define PLUGINSMODEL_H + +#include + +class PluginsModel : public QStandardItemModel +{ + Q_OBJECT + +public: + PluginsModel(QObject *parent = 0); + ~PluginsModel() {}; + + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); + +signals: + void indexesDropped(QVector indexes); + +}; + +#endif \ No newline at end of file diff --git a/apps/launcher/pluginsview.cpp b/apps/launcher/pluginsview.cpp new file mode 100644 index 000000000..27af45c56 --- /dev/null +++ b/apps/launcher/pluginsview.cpp @@ -0,0 +1,42 @@ +#include +#include + +#include "pluginsview.hpp" + +PluginsView::PluginsView(QWidget *parent) : QTableView(parent) +{ + setSelectionBehavior(QAbstractItemView::SelectRows); + setSelectionMode(QAbstractItemView::ExtendedSelection); + setEditTriggers(QAbstractItemView::NoEditTriggers); + setAlternatingRowColors(true); + setDragEnabled(true); + setDragDropMode(QAbstractItemView::InternalMove); + setDropIndicatorShown(true); + setDragDropOverwriteMode(false); + setContextMenuPolicy(Qt::CustomContextMenu); + +} + +void PluginsView::startDrag(Qt::DropActions supportedActions) +{ + selectionModel()->select( selectionModel()->selection(), + QItemSelectionModel::Select | QItemSelectionModel::Rows ); + QAbstractItemView::startDrag( supportedActions ); +} + +void PluginsView::setModel(QSortFilterProxyModel *model) +{ + QTableView::setModel(model); + + qRegisterMetaType< QVector >(); + + connect(model->sourceModel(), SIGNAL(indexesDropped(QVector)), + this, SLOT(selectIndexes(QVector)), Qt::QueuedConnection); +} + +void PluginsView::selectIndexes( QVector aIndexes ) +{ + selectionModel()->clearSelection(); + foreach( QPersistentModelIndex pIndex, aIndexes ) + selectionModel()->select( pIndex, QItemSelectionModel::Select | QItemSelectionModel::Rows ); +} diff --git a/apps/launcher/pluginsview.hpp b/apps/launcher/pluginsview.hpp new file mode 100644 index 000000000..b3dfb79ff --- /dev/null +++ b/apps/launcher/pluginsview.hpp @@ -0,0 +1,29 @@ +#ifndef PLUGINSVIEW_H +#define PLUGINSVIEW_H + +#include + +#include "pluginsmodel.hpp" + +class QSortFilterProxyModel; + +class PluginsView : public QTableView +{ + Q_OBJECT +public: + PluginsView(QWidget *parent = 0); + + PluginsModel* model() const + { return qobject_cast(QAbstractItemView::model()); } + + void startDrag(Qt::DropActions supportedActions); + void setModel(QSortFilterProxyModel *model); + +public slots: + void selectIndexes(QVector aIndexes); + +}; + +Q_DECLARE_METATYPE(QVector); + +#endif \ No newline at end of file diff --git a/apps/launcher/resources.qrc b/apps/launcher/resources.qrc new file mode 100644 index 000000000..2b56f80fd --- /dev/null +++ b/apps/launcher/resources.qrc @@ -0,0 +1,21 @@ + + + resources/images/clear.png + resources/images/down.png + resources/images/openmw.png + resources/images/openmw-plugin.png + resources/images/openmw-header.png + resources/images/playpage-background.png + + + resources/icons/tango/index.theme + resources/icons/tango/video-display.png + resources/icons/tango/document-new.png + resources/icons/tango/edit-copy.png + resources/icons/tango/edit-delete.png + resources/icons/tango/go-bottom.png + resources/icons/tango/go-down.png + resources/icons/tango/go-top.png + resources/icons/tango/go-up.png + + diff --git a/apps/launcher/resources/icons/tango/document-new.png b/apps/launcher/resources/icons/tango/document-new.png new file mode 100644 index 000000000..4c3efdd6f Binary files /dev/null and b/apps/launcher/resources/icons/tango/document-new.png differ diff --git a/apps/launcher/resources/icons/tango/edit-copy.png b/apps/launcher/resources/icons/tango/edit-copy.png new file mode 100644 index 000000000..8dd48c494 Binary files /dev/null and b/apps/launcher/resources/icons/tango/edit-copy.png differ diff --git a/apps/launcher/resources/icons/tango/edit-delete.png b/apps/launcher/resources/icons/tango/edit-delete.png new file mode 100644 index 000000000..d7667c36b Binary files /dev/null and b/apps/launcher/resources/icons/tango/edit-delete.png differ diff --git a/apps/launcher/resources/icons/tango/go-bottom.png b/apps/launcher/resources/icons/tango/go-bottom.png new file mode 100644 index 000000000..2c5a80385 Binary files /dev/null and b/apps/launcher/resources/icons/tango/go-bottom.png differ diff --git a/apps/launcher/resources/icons/tango/go-down.png b/apps/launcher/resources/icons/tango/go-down.png new file mode 100644 index 000000000..3dd7fccdf Binary files /dev/null and b/apps/launcher/resources/icons/tango/go-down.png differ diff --git a/apps/launcher/resources/icons/tango/go-top.png b/apps/launcher/resources/icons/tango/go-top.png new file mode 100644 index 000000000..70f2c996c Binary files /dev/null and b/apps/launcher/resources/icons/tango/go-top.png differ diff --git a/apps/launcher/resources/icons/tango/go-up.png b/apps/launcher/resources/icons/tango/go-up.png new file mode 100644 index 000000000..fa9a7d71b Binary files /dev/null and b/apps/launcher/resources/icons/tango/go-up.png differ diff --git a/apps/launcher/resources/icons/tango/index.theme b/apps/launcher/resources/icons/tango/index.theme new file mode 100644 index 000000000..1f54489eb --- /dev/null +++ b/apps/launcher/resources/icons/tango/index.theme @@ -0,0 +1,8 @@ +[Icon Theme] +Name=Tango +Comment=Tango Theme +Inherits=default +Directories=16x16 + +[16x16] +Size=16 \ No newline at end of file diff --git a/apps/launcher/resources/icons/tango/video-display.png b/apps/launcher/resources/icons/tango/video-display.png new file mode 100644 index 000000000..133143684 Binary files /dev/null and b/apps/launcher/resources/icons/tango/video-display.png differ diff --git a/apps/launcher/resources/images/clear.png b/apps/launcher/resources/images/clear.png new file mode 100644 index 000000000..6c4b83b7a Binary files /dev/null and b/apps/launcher/resources/images/clear.png differ diff --git a/apps/launcher/resources/images/down.png b/apps/launcher/resources/images/down.png new file mode 100644 index 000000000..f529cda7d Binary files /dev/null and b/apps/launcher/resources/images/down.png differ diff --git a/apps/launcher/resources/images/openmw-header.png b/apps/launcher/resources/images/openmw-header.png new file mode 100644 index 000000000..a168d4d2a Binary files /dev/null and b/apps/launcher/resources/images/openmw-header.png differ diff --git a/apps/launcher/resources/images/openmw-plugin.png b/apps/launcher/resources/images/openmw-plugin.png new file mode 100644 index 000000000..c34cba554 Binary files /dev/null and b/apps/launcher/resources/images/openmw-plugin.png differ diff --git a/apps/launcher/resources/images/openmw.ico b/apps/launcher/resources/images/openmw.ico new file mode 100644 index 000000000..c04fc3d9c Binary files /dev/null and b/apps/launcher/resources/images/openmw.ico differ diff --git a/apps/launcher/resources/images/openmw.png b/apps/launcher/resources/images/openmw.png new file mode 100644 index 000000000..7a5393821 Binary files /dev/null and b/apps/launcher/resources/images/openmw.png differ diff --git a/apps/launcher/resources/images/playpage-background.png b/apps/launcher/resources/images/playpage-background.png new file mode 100644 index 000000000..0116adc0f Binary files /dev/null and b/apps/launcher/resources/images/playpage-background.png differ diff --git a/files/launcher.cfg b/files/launcher.cfg new file mode 100644 index 000000000..bc0e2b7fb --- /dev/null +++ b/files/launcher.cfg @@ -0,0 +1,5 @@ +[Profiles] +CurrentProfile=Default +Default\Master0=Morrowind.esm +Default\Master1=Tribunal.esm +Default\Master2=Bloodmoon.esm diff --git a/files/launcher.qss b/files/launcher.qss new file mode 100644 index 000000000..dad87022c --- /dev/null +++ b/files/launcher.qss @@ -0,0 +1,120 @@ +#PlayGroup { + background-image: url(":/images/playpage-background.png"); + background-repeat: no-repeat; + background-position: top; + padding-left: 30px; + padding-right: 30px; +} + +#MastersWidget { + selection-background-color: palette(highlight); +} + +#PlayButton { + height: 50px; + margin-bottom: 30px; + + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, + stop:0 rgba(255, 255, 255, 200), + stop:0.1 rgba(255, 255, 255, 15), + stop:0.49 rgba(255, 255, 255, 75), + stop:0.5 rgba(0, 0, 0, 0), + stop:0.9 rgba(0, 0, 0, 55), + stop:1 rgba(0, 0, 0, 100)); + + font: 24pt "Trebuchet MS"; + color: black; + + border-right: 1px solid rgba(0, 0, 0, 155); + border-left: 1px solid rgba(0, 0, 0, 55); + border-top: 1px solid rgba(0, 0, 0, 55); + border-bottom: 1px solid rgba(0, 0, 0, 155); + + border-radius: 5px; +} + +#PlayButton:hover { + border-bottom: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(164, 192, 228, 255), stop:1 rgba(255, 255, 255, 0)); + border-top: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(164, 192, 228, 255), stop:1 rgba(255, 255, 255, 0)); + border-right: qlineargradient(spread:pad, x1:1, y1:0, x2:0, y2:0, stop:0 rgba(164, 192, 228, 255), stop:1 rgba(255, 255, 255, 0)); + border-left: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 rgba(164, 192, 228, 255), stop:1 rgba(255, 255, 255, 0)); + border-width: 2px; + border-style: solid; +} + +#PlayButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 rgba(0, 0, 0, 75), + stop:0.1 rgba(0, 0, 0, 15), + stop:0.2 rgba(255, 255, 255, 55) + stop:0.95 rgba(255, 255, 255, 55), + stop:1 rgba(255, 255, 255, 155)); + + border: 1px solid rgba(0, 0, 0, 55); +} + +#ProfileLabel { + font: 14pt "Trebuchet MS"; +} + +#ProfilesComboBox { + padding: 1px 18px 1px 3px; + + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 white, stop:0.2 rgba(0, 0, 0, 25), stop:1 white); + border-width: 1px; + border-color: rgba(0, 0, 0, 125); + border-style: solid; + border-radius: 2px; +} + +/*QComboBox gets the "on" state when the popup is open */ +#ProfilesComboBox:!editable:on, #ProfilesComboBox::drop-down:editable:on { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 rgba(0, 0, 0, 75), + stop:0.1 rgba(0, 0, 0, 15), + stop:0.2 rgba(255, 255, 255, 55)); + + border: 1px solid rgba(0, 0, 0, 55); +} + + +#ProfilesComboBox { /* shift the text when the popup opens */ + padding-top: 3px; + padding-left: 4px; + + font: 11pt "Trebuchet MS"; +} + +#ProfilesComboBox::drop-down { + subcontrol-origin: padding; + subcontrol-position: top right; + + border-width: 1px; + border-left-width: 1px; + border-left-color: darkgray; + border-left-style: solid; /* just a single line */ + border-top-right-radius: 3px; /* same radius as the QComboBox */ + border-bottom-right-radius: 3px; +} + +#ProfilesComboBox::down-arrow { + image: url(":/images/down.png"); +} + +#ProfilesComboBox::down-arrow:on { /* shift the arrow when popup is open */ + top: 1px; + left: 1px; +} + +#ProfilesComboBox QAbstractItemView { + border: 2px solid lightgray; + border-radius: 5px; +} + +#IconWidget { + background-image: url(":/images/openmw-header.png"); + background-color: white; + background-repeat: no-repeat; + background-attachment: scroll; + background-position: right; +} diff --git a/files/mac/Info.plist b/files/mac/Info.plist index 6b350d250..1872425e7 100644 --- a/files/mac/Info.plist +++ b/files/mac/Info.plist @@ -7,7 +7,7 @@ CFBundleDevelopmentRegion English CFBundleExecutable - openmw + omwlauncher CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString diff --git a/files/openmw.desktop b/files/openmw.desktop new file mode 100644 index 000000000..8643d4b13 --- /dev/null +++ b/files/openmw.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=0.11 +Type=Application +Name=OpenMW Launcher +GenericName=Role Playing Game +Comment=An engine replacement for The Elder Scrolls III: Morrowind +TryExec=omwlauncher +Exec=omwlauncher +Icon=openmw +Categories=Game;RolePlaying; diff --git a/readme.txt b/readme.txt new file mode 100644 index 000000000..66e7c12ec --- /dev/null +++ b/readme.txt @@ -0,0 +1,51 @@ +OpenMW: A reimplementation of The Elder Scrolls III: Morrowind + +OpenMW is an attempt at recreating the engine for the popular role-playing game +Morrowind by Bethesda Softworks. You need to own and install the original game for OpenMW to work. + +Version: 0.11 +License: GPL (see GPL3.txt for more information) +Website: www.openmw.com + + +THIS IS A WORK IN PROGRESS + +INSTALLATION + +Windows: +TODO add description for Windows + +Linux: +Ubuntu +TODO add description for Ubuntu + +Arch Linux +There's an OpenMW package available in the AUR Repository: +http://aur.archlinux.org/packages.php?ID=21419 + +OS X: +TODO add description for OS X + +BUILD FROM SOURCE + +TODO add description here + +COMMAND LINE OPTIONS +TODO add description of command line options + +CREDITS + +Developers: +TODO add list of developers + +OpenMW: +Thanks to DokterDume for kindly providing us with the Moon and Star logo +used as the application icon and project logo. + +Launcher: +Thanks to Kevin Ryan for kindly providing us with the icon used for the Data Files tab. + + +CHANGELOG + +TODO add changelog (take pre 0.11.0 changelog from wiki when it is up again; take 0.11.0 and later changelog from tracker)