diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..8fb9bf6 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +1d3cd89fc817c47142dc3f6370a37801b4ea3f90 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f36fa7d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,16 @@ +name: Build Installer Manual Plugin + +on: + push: + branches: master + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Installer Manual Plugin + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: cmake_common uibase diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 0000000..1b58829 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Installer Manual Plugin + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 62a550d..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,40 +0,0 @@ -version: 1.0.{build} -skip_branch_with_pr: true -image: Visual Studio 2019 -environment: - WEBHOOK_URL: - secure: gOKbXaZM9ImtMD5XrYITvdyZUW/az082G9OIN1EC1Vbg57wBaeLhi49uGjxPw5GVujHku6kxN6ab89zhbS5GVeluR76GM83IbKV4Sh7udXzoYZZdg6YudtYHzdhCgUeiedpswbuczTq9ceIkkfSEWZuh/lMAAVVwvcGsJAnoPFw= -build_script: -- pwsh: >- - $ErrorActionPreference = 'Stop' - - git clone --depth=1 --no-single-branch https://github.com/ModOrganizer2/modorganizer-umbrella.git c:\projects\modorganizer-umbrella - - New-Item -ItemType Directory -Path c:\projects\modorganizer-build - - cd c:\projects\modorganizer-umbrella - - ($env:APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH -eq $null) ? ($branch = $env:APPVEYOR_REPO_BRANCH) : ($branch = $env:APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH) - - git checkout $(git show-ref --verify --quiet refs/remotes/origin/${branch} || echo '-b') ${branch} - - C:\Python37-x64\python.exe unimake.py -d c:\projects\modorganizer-build -s Appveyor_Build=True ${env:APPVEYOR_PROJECT_NAME} - - if($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode ) } -artifacts: -- path: vsbuild\src\RelWithDebInfo\installer_manual.dll - name: installer_manual_dll -- path: vsbuild\src\RelWithDebInfo\installer_manual.pdb - name: installer_manual_pdb -- path: vsbuild\src\RelWithDebInfo\installer_manual.lib - name: installer_manual_lib -on_success: - - ps: Set-Location -Path $env:APPVEYOR_BUILD_FOLDER - - ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1 - - ps: ./send.ps1 success $env:WEBHOOK_URL -on_failure: - - ps: Set-Location -Path $env:APPVEYOR_BUILD_FOLDER - - ps: Push-AppveyorArtifact ${env:APPVEYOR_BUILD_FOLDER}\stdout.log - - ps: Push-AppveyorArtifact ${env:APPVEYOR_BUILD_FOLDER}\stderr.log - - ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1 - - ps: ./send.ps1 failure $env:WEBHOOK_URL \ No newline at end of file diff --git a/src/archivetree.cpp b/src/archivetree.cpp index f20cd59..cccd79a 100644 --- a/src/archivetree.cpp +++ b/src/archivetree.cpp @@ -1,468 +1,484 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "archivetree.h" - -#include -#include -#include - -#include -#include -#include - -using namespace MOBase; - -// Implementation details for the ArchiveTree widget: -// -// The ArchiveTreeWidget presents to the user the underlying IFileTree, but in order -// to increase performance, the tree is populated dynamically when required. Populating -// the tree is currently required: -// 1) when a branch of the tree widget is expanded, -// 2) when an item is moved to a tree, -// 3) when a directory is created, -// 4) when a directory is "set as data root". -// -// Case 1 is handled automatically in the setExpanded method of ArchiveTreeWidget. Cases 2 -// and 3 could be dealt with differently, but populating the tree before inserting an item -// makes everything else easier (not that populating the widget is different from populating -// the IFileTree which is done automatically). Case 4 is handled manually in setDataRoot. -// -// Another specificity of the implementation is the treeCheckStateChanged() signal emitted -// by the ArchiveTreeWidget. This signal is used to avoid having to connect to the itemChanged() -// signal or overriding the dataChanged() method which are called much more often than those. -// The treeCheckStateChanged() signal is send only for the item that has actually been changed -// by the user. While the interface is automatically updated by Qt, we need to update the -// underlying tree manually. This is done by doing the following things: -// 1) When an item is unchecked: -// - We detach the corresponding entry from its parent, and recursively detach the empty -// parents (or the ones that become empty). -// - If the entry is a directory and the item has been populated, we recursively detach -// all the child entries for all the child items that have been populated (no need to -// do it for non-populated items)> -// 2) When an item is checked, we do the same process but we re-attach parents and re-insert -// children. -// -// Detaching or re-attaching parents is also done when a directory is created (if the directory -// is created in an empty directory, we need to re-attach), or when an item is moved (if the -// directory the item comes from is now empty or if the target directory was empty). -// - -ArchiveTreeWidgetItem::ArchiveTreeWidgetItem(QString dataName) - : QTreeWidgetItem(QStringList(dataName)), m_Entry(nullptr) { - setFlags(flags() & ~Qt::ItemIsUserCheckable); - setExpanded(true); - m_Populated = true; -} - -ArchiveTreeWidgetItem::ArchiveTreeWidgetItem(std::shared_ptr entry) - : QTreeWidgetItem(QStringList(entry->name())), m_Entry(entry) -{ - if (entry->isDir()) { - setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); - setFlags(flags() | Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate); - } - else { - setFlags(flags() | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren); - } - setCheckState(0, Qt::Checked); - setToolTip(0, entry->path()); -} - -void ArchiveTreeWidgetItem::setData(int column, int role, const QVariant& value) -{ - ArchiveTreeWidget* tree = static_cast(treeWidget()); - if (tree != nullptr && tree->m_Emitter == nullptr) { - tree->m_Emitter = this; - } - QTreeWidgetItem::setData(column, role, value); - if (tree != nullptr && tree->m_Emitter == this) { - tree->m_Emitter = nullptr; - if (role == Qt::CheckStateRole) { - tree->onTreeCheckStateChanged(this); - } - } -} - -void ArchiveTreeWidgetItem::populate(bool force) { - - // Only populates once: - if (isPopulated() && !force) { - return; - } - - // Should never happen: - if (entry()->isFile()) { - return; - } - - // We go in reverse of the tree because we want to insert the original - // entries at the beginning (the item can only contains children if a - // directory has been created under it or if entries has been moved under - // it): - for (auto &entry: *entry()->astree()) { - auto newItem = new ArchiveTreeWidgetItem(entry); - newItem->setCheckState(0, flags().testFlag(Qt::ItemIsUserCheckable) ? checkState(0) : Qt::Checked); - addChild(newItem); - } - - // If the item is unchecked, we need to clear it because it has not been cleared - // before: - if (flags().testFlag(Qt::ItemIsUserCheckable) && checkState(0) == Qt::Unchecked) { - entry()->astree()->clear(); - } - - m_Populated = true; -} - -ArchiveTreeWidget::ArchiveTreeWidget(QWidget *parent) : QTreeWidget(parent) -{ - setAutoExpandDelay(1000); - setDragDropOverwriteMode(true); - connect(this, &ArchiveTreeWidget::itemExpanded, this, &ArchiveTreeWidget::populateItem); -} - -void ArchiveTreeWidget::setup(QString dataFolderName) -{ - m_ViewRoot = new ArchiveTreeWidgetItem("<" + dataFolderName + ">"); - m_DataRoot = nullptr; - addTopLevelItem(m_ViewRoot); -} - -void ArchiveTreeWidget::populateItem(QTreeWidgetItem* item) -{ - static_cast(item)->populate(); -} - -void ArchiveTreeWidget::setDataRoot(ArchiveTreeWidgetItem* const root) -{ - if (root != m_DataRoot) { - if (m_DataRoot != nullptr) { - m_DataRoot->addChildren(m_ViewRoot->takeChildren()); - } - - // Force populate: - root->populate(); - - m_DataRoot = root; - m_ViewRoot->setEntry(m_DataRoot->entry()); - m_ViewRoot->addChildren(m_DataRoot->takeChildren()); - m_ViewRoot->setExpanded(true); - } - - emit treeChanged(); -} - -void ArchiveTreeWidget::detachParents(ArchiveTreeWidgetItem* item) { - auto entry = item->entry(); - auto parent = entry->parent(); - entry->detach(); - while (parent != nullptr && parent->empty()) { - auto tmp = parent->parent(); - parent->detach(); - parent = tmp; - } - -} - -void ArchiveTreeWidget::attachParents(ArchiveTreeWidgetItem* item) { - while (item->parent() != nullptr) { - auto parent = static_cast(item->parent()); - auto parentEntry = parent->entry(); - if (parentEntry != nullptr) { - parentEntry->astree()->insert(item->entry()); - } - item = parent; - } -} - - -void ArchiveTreeWidget::recursiveInsert(ArchiveTreeWidgetItem* item) { - if (item->isPopulated()) { - auto tree = item->entry()->astree(); - for (int i = 0; i < item->childCount(); ++i) { - auto child = static_cast(item->child(i)); - tree->insert(child->entry()); - if (child->entry()->isDir()) { - recursiveInsert(child); - } - } - } -} - -void ArchiveTreeWidget::recursiveDetach(ArchiveTreeWidgetItem* item) { - if (item->isPopulated()) { - for (int i = 0; i < item->childCount(); ++i) { - auto child = static_cast(item->child(i)); - if (child->entry()->isDir()) { - recursiveDetach(child); - } - } - item->entry()->astree()->clear(); - } -} - -ArchiveTreeWidgetItem* ArchiveTreeWidget::addDirectory(ArchiveTreeWidgetItem* item, QString name) -{ - auto tree = item->entry()->astree(); - auto* newItem = new ArchiveTreeWidgetItem(tree->addDirectory(name)); - - // find the insert position - auto it = std::find_if(tree->begin(), tree->end(), [name](auto&& entry) { - return entry->compare(name) == 0; - }); - int index = it - tree->begin(); - MOBase::log::debug("insert at: {}", index); - item->insertChild(index, newItem); - - newItem->setCheckState(0, Qt::Checked); - attachParents(item); - emit treeChanged(); - - return newItem; -} - -void ArchiveTreeWidget::moveItem(ArchiveTreeWidgetItem* source, ArchiveTreeWidgetItem* target) { - // just insert the source in the target. - auto tree = target->entry()->astree(); - - detachParents(source); - - // check if an entry exists with the same name, we check - // in the tree widget to find unchecked items - for (int i = 0; i < target->childCount(); ++i) { - auto* child = target->child(i); - if (child->entry()->compare(source->entry()->name()) == 0) { - // remove existing file and force check existing directory - if (child->entry()->isFile()) { - target->removeChild(child); - } - else { - child->setCheckState(0, Qt::Checked); - } - break; - } - } - - tree->insert(source->entry(), IFileTree::InsertPolicy::MERGE); - - attachParents(target); - - emit treeChanged(); -} - -void ArchiveTreeWidget::onTreeCheckStateChanged(ArchiveTreeWidgetItem* item) { - - auto entry = item->entry(); - - // If the entry is a directory, we need to either detach or re-attach all the - // children. It is not possible to only detach the directory because if the - // user uncheck a directory and then check a file under it, the other files would - // still be attached. - // - // The two recursive methods only go down to the expanded (based on isPopulated() tree, for - // two reasons: - // 1. If a tree item has not been populated, then detaching an entry from its parent will - // delete it since there would be no remaining shared pointers. - // 2. If the tree has not been populated yet, all the entries under it are still attached, - // so there is no need to process them differently. Detaching a non-expanded item can - // be done by simply detaching the tree, no need to detach all the children. - if (entry->isDir()) { - if (item->checkState(0) == Qt::Checked && item->isPopulated()) { - recursiveInsert(item); - } - else if (item->checkState(0) == Qt::Unchecked && item->isPopulated()) { - recursiveDetach(item); - } - } - - // Unchecked: we go up the parent chain removing all trees that are now empty: - if (item->checkState(0) == Qt::Unchecked) { - detachParents(item); - } - // Otherwize, we need to-reattach the parent: - else { - attachParents(item); - } - - emit treeChanged(); -} - -bool ArchiveTreeWidget::testMovePossible(ArchiveTreeWidgetItem* source, ArchiveTreeWidgetItem* target) -{ - if (target == nullptr || source == nullptr) { - return false; - } - - if (target->flags().testFlag(Qt::ItemNeverHasChildren)) { - return false; - } - - if (source == target || source->parent() == target) { - return false; - } - - return true; -} - -void ArchiveTreeWidget::dragEnterEvent(QDragEnterEvent *event) -{ - QTreeWidgetItem *source = this->currentItem(); - if ((source == nullptr) || (source->parent() == nullptr)) { - // can't change top level - event->ignore(); - return; - } else { - QTreeWidget::dragEnterEvent(event); - } - -} - -void ArchiveTreeWidget::dragMoveEvent(QDragMoveEvent *event) -{ - if (!testMovePossible( - static_cast(currentItem()), - static_cast(itemAt(event->pos())))) { - event->ignore(); - } else { - QTreeWidget::dragMoveEvent(event); - } -} - -static bool isAncestor(const QTreeWidgetItem *ancestor, const QTreeWidgetItem *item) -{ - QTreeWidgetItem *iter = item->parent(); - while (iter != nullptr) { - if (iter == ancestor) { - return true; - } - iter = iter->parent(); - } - return false; -} - -void ArchiveTreeWidget::refreshItem(ArchiveTreeWidgetItem* item) -{ - if (!item->isPopulated() || item->flags().testFlag(Qt::ItemNeverHasChildren)) { - return; - } - - // at this point, all child items are checked for we only remember the ones - // that were expanded to re-expand them - std::map expanded; - while (item->childCount() > 0) { - auto* child = item->child(0); - expanded[child->entry()->name()] = child->isExpanded(); - item->removeChild(child); - } - - item->populate(true); - - for (int i = 0; i < item->childCount(); ++i) { - auto* child = item->child(i); - if (expanded[child->entry()->name()]) { - child->setExpanded(true); - } - } -} - -void ArchiveTreeWidget::dropEvent(QDropEvent *event) -{ - event->ignore(); - - // target widget (should be a directory) - auto *target = static_cast(itemAt(event->pos())); - - // this should not really happen because it is prevent by dragMoveEvent - if (target->flags().testFlag(Qt::ItemNeverHasChildren)) { - - // this should really not happen, how should a file get to the top level? - if (target->parent() == nullptr) { - return; - } - - target = target->parent(); - } - - // populate target if required - target->populate(); - - auto sourceItems = this->selectedItems(); - - // check the selected items - we do not want to move only - // some items so we check everything first and then move - for (auto* source : sourceItems) { - - auto* aSource = static_cast(source); - - // do not allow element to be dropped into one of its - // own child - if (isAncestor(source, target)) { - event->accept(); - QMessageBox::warning(parentWidget(), - tr("Cannot drop"), - tr("Cannot drop '%1' into one of its subfolder.").arg(aSource->entry()->name())); - return; - } - - auto sourceEntry = aSource->entry(); - auto targetEntry = target->entry()->astree()->find(sourceEntry->name()); - if (targetEntry && targetEntry->fileType() != sourceEntry->fileType()) { - event->accept(); - QMessageBox::warning(parentWidget(), - tr("Cannot drop"), - targetEntry->isFile() ? - tr("A file '%1' already exists in folder '%2'.").arg(sourceEntry->name()).arg(target->entry()->name()) - : tr("A folder '%1' already exists in folder '%2'.").arg(sourceEntry->name()).arg(target->entry()->name())); - return; - } - - } - - for (auto* source : sourceItems) { - - auto* aSource = static_cast(source); - - // this only check dropping an item on itself or dropping an item in - // its parent so it is ok, it just does not do anything - if (source->parent() == nullptr || !testMovePossible(aSource, target)) { - continue; - } - - // force expand item that are going to be merged - for (int i = 0; i < target->childCount(); ++i) { - auto* child = target->child(i); - if (child->entry()->compare(aSource->entry()->name()) == 0 - && !child->flags().testFlag(Qt::ItemNeverHasChildren)) { - child->setExpanded(true); - } - } - - // remove the source from its parent - source->parent()->removeChild(source); - - // actually perform the move on the underlying tree model - moveItem(aSource, target); - } - - // refresh the target item - this assumes that itemMoved is called synchronously - // and perform the FileTree changes - refreshItem(target); - -} +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "archivetree.h" + +#include +#include +#include + +#include +#include +#include + +using namespace MOBase; + +// Implementation details for the ArchiveTree widget: +// +// The ArchiveTreeWidget presents to the user the underlying IFileTree, but in order +// to increase performance, the tree is populated dynamically when required. Populating +// the tree is currently required: +// 1) when a branch of the tree widget is expanded, +// 2) when an item is moved to a tree, +// 3) when a directory is created, +// 4) when a directory is "set as data root". +// +// Case 1 is handled automatically in the setExpanded method of ArchiveTreeWidget. Cases +// 2 and 3 could be dealt with differently, but populating the tree before inserting an +// item makes everything else easier (not that populating the widget is different from +// populating the IFileTree which is done automatically). Case 4 is handled manually in +// setDataRoot. +// +// Another specificity of the implementation is the treeCheckStateChanged() signal +// emitted by the ArchiveTreeWidget. This signal is used to avoid having to connect to +// the itemChanged() signal or overriding the dataChanged() method which are called much +// more often than those. The treeCheckStateChanged() signal is send only for the item +// that has actually been changed by the user. While the interface is automatically +// updated by Qt, we need to update the underlying tree manually. This is done by doing +// the following things: +// 1) When an item is unchecked: +// - We detach the corresponding entry from its parent, and recursively detach the +// empty +// parents (or the ones that become empty). +// - If the entry is a directory and the item has been populated, we recursively +// detach +// all the child entries for all the child items that have been populated (no +// need to do it for non-populated items)> +// 2) When an item is checked, we do the same process but we re-attach parents and +// re-insert +// children. +// +// Detaching or re-attaching parents is also done when a directory is created (if the +// directory is created in an empty directory, we need to re-attach), or when an item is +// moved (if the directory the item comes from is now empty or if the target directory +// was empty). +// + +ArchiveTreeWidgetItem::ArchiveTreeWidgetItem(QString dataName) + : QTreeWidgetItem(QStringList(dataName)), m_Entry(nullptr) +{ + setFlags(flags() & ~Qt::ItemIsUserCheckable); + setExpanded(true); + m_Populated = true; +} + +ArchiveTreeWidgetItem::ArchiveTreeWidgetItem( + std::shared_ptr entry) + : QTreeWidgetItem(QStringList(entry->name())), m_Entry(entry) +{ + if (entry->isDir()) { + setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); + setFlags(flags() | Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate); + } else { + setFlags(flags() | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren); + } + setCheckState(0, Qt::Checked); + setToolTip(0, entry->path()); +} + +void ArchiveTreeWidgetItem::setData(int column, int role, const QVariant& value) +{ + ArchiveTreeWidget* tree = static_cast(treeWidget()); + if (tree != nullptr && tree->m_Emitter == nullptr) { + tree->m_Emitter = this; + } + QTreeWidgetItem::setData(column, role, value); + if (tree != nullptr && tree->m_Emitter == this) { + tree->m_Emitter = nullptr; + if (role == Qt::CheckStateRole) { + tree->onTreeCheckStateChanged(this); + } + } +} + +void ArchiveTreeWidgetItem::populate(bool force) +{ + + // Only populates once: + if (isPopulated() && !force) { + return; + } + + // Should never happen: + if (entry()->isFile()) { + return; + } + + // We go in reverse of the tree because we want to insert the original + // entries at the beginning (the item can only contains children if a + // directory has been created under it or if entries has been moved under + // it): + for (auto& entry : *entry()->astree()) { + auto newItem = new ArchiveTreeWidgetItem(entry); + newItem->setCheckState(0, flags().testFlag(Qt::ItemIsUserCheckable) ? checkState(0) + : Qt::Checked); + addChild(newItem); + } + + // If the item is unchecked, we need to clear it because it has not been cleared + // before: + if (flags().testFlag(Qt::ItemIsUserCheckable) && checkState(0) == Qt::Unchecked) { + entry()->astree()->clear(); + } + + m_Populated = true; +} + +ArchiveTreeWidget::ArchiveTreeWidget(QWidget* parent) : QTreeWidget(parent) +{ + setAutoExpandDelay(1000); + setDragDropOverwriteMode(true); + connect(this, &ArchiveTreeWidget::itemExpanded, this, + &ArchiveTreeWidget::populateItem); +} + +void ArchiveTreeWidget::setup(QString dataFolderName) +{ + m_ViewRoot = new ArchiveTreeWidgetItem("<" + dataFolderName + ">"); + m_DataRoot = nullptr; + addTopLevelItem(m_ViewRoot); +} + +void ArchiveTreeWidget::populateItem(QTreeWidgetItem* item) +{ + static_cast(item)->populate(); +} + +void ArchiveTreeWidget::setDataRoot(ArchiveTreeWidgetItem* const root) +{ + if (root != m_DataRoot) { + if (m_DataRoot != nullptr) { + m_DataRoot->addChildren(m_ViewRoot->takeChildren()); + } + + // Force populate: + root->populate(); + + m_DataRoot = root; + m_ViewRoot->setEntry(m_DataRoot->entry()); + m_ViewRoot->addChildren(m_DataRoot->takeChildren()); + m_ViewRoot->setExpanded(true); + } + + emit treeChanged(); +} + +void ArchiveTreeWidget::detachParents(ArchiveTreeWidgetItem* item) +{ + auto entry = item->entry(); + auto parent = entry->parent(); + entry->detach(); + while (parent != nullptr && parent->empty()) { + auto tmp = parent->parent(); + parent->detach(); + parent = tmp; + } +} + +void ArchiveTreeWidget::attachParents(ArchiveTreeWidgetItem* item) +{ + while (item->parent() != nullptr) { + auto parent = static_cast(item->parent()); + auto parentEntry = parent->entry(); + if (parentEntry != nullptr) { + parentEntry->astree()->insert(item->entry()); + } + item = parent; + } +} + +void ArchiveTreeWidget::recursiveInsert(ArchiveTreeWidgetItem* item) +{ + if (item->isPopulated()) { + auto tree = item->entry()->astree(); + for (int i = 0; i < item->childCount(); ++i) { + auto child = static_cast(item->child(i)); + tree->insert(child->entry()); + if (child->entry()->isDir()) { + recursiveInsert(child); + } + } + } +} + +void ArchiveTreeWidget::recursiveDetach(ArchiveTreeWidgetItem* item) +{ + if (item->isPopulated()) { + for (int i = 0; i < item->childCount(); ++i) { + auto child = static_cast(item->child(i)); + if (child->entry()->isDir()) { + recursiveDetach(child); + } + } + item->entry()->astree()->clear(); + } +} + +ArchiveTreeWidgetItem* ArchiveTreeWidget::addDirectory(ArchiveTreeWidgetItem* item, + QString name) +{ + auto tree = item->entry()->astree(); + auto* newItem = new ArchiveTreeWidgetItem(tree->addDirectory(name)); + + // find the insert position + auto it = std::find_if(tree->begin(), tree->end(), [name](auto&& entry) { + return entry->compare(name) == 0; + }); + int index = it - tree->begin(); + MOBase::log::debug("insert at: {}", index); + item->insertChild(index, newItem); + + newItem->setCheckState(0, Qt::Checked); + attachParents(item); + emit treeChanged(); + + return newItem; +} + +void ArchiveTreeWidget::moveItem(ArchiveTreeWidgetItem* source, + ArchiveTreeWidgetItem* target) +{ + // just insert the source in the target. + auto tree = target->entry()->astree(); + + detachParents(source); + + // check if an entry exists with the same name, we check + // in the tree widget to find unchecked items + for (int i = 0; i < target->childCount(); ++i) { + auto* child = target->child(i); + if (child->entry()->compare(source->entry()->name()) == 0) { + // remove existing file and force check existing directory + if (child->entry()->isFile()) { + target->removeChild(child); + } else { + child->setCheckState(0, Qt::Checked); + } + break; + } + } + + tree->insert(source->entry(), IFileTree::InsertPolicy::MERGE); + + attachParents(target); + + emit treeChanged(); +} + +void ArchiveTreeWidget::onTreeCheckStateChanged(ArchiveTreeWidgetItem* item) +{ + + auto entry = item->entry(); + + // If the entry is a directory, we need to either detach or re-attach all the + // children. It is not possible to only detach the directory because if the + // user uncheck a directory and then check a file under it, the other files would + // still be attached. + // + // The two recursive methods only go down to the expanded (based on isPopulated() + // tree, for two reasons: + // 1. If a tree item has not been populated, then detaching an entry from its parent + // will + // delete it since there would be no remaining shared pointers. + // 2. If the tree has not been populated yet, all the entries under it are still + // attached, + // so there is no need to process them differently. Detaching a non-expanded item + // can be done by simply detaching the tree, no need to detach all the children. + if (entry->isDir()) { + if (item->checkState(0) == Qt::Checked && item->isPopulated()) { + recursiveInsert(item); + } else if (item->checkState(0) == Qt::Unchecked && item->isPopulated()) { + recursiveDetach(item); + } + } + + // Unchecked: we go up the parent chain removing all trees that are now empty: + if (item->checkState(0) == Qt::Unchecked) { + detachParents(item); + } + // Otherwize, we need to-reattach the parent: + else { + attachParents(item); + } + + emit treeChanged(); +} + +bool ArchiveTreeWidget::testMovePossible(ArchiveTreeWidgetItem* source, + ArchiveTreeWidgetItem* target) +{ + if (target == nullptr || source == nullptr) { + return false; + } + + if (target->flags().testFlag(Qt::ItemNeverHasChildren)) { + return false; + } + + if (source == target || source->parent() == target) { + return false; + } + + return true; +} + +void ArchiveTreeWidget::dragEnterEvent(QDragEnterEvent* event) +{ + QTreeWidgetItem* source = this->currentItem(); + if ((source == nullptr) || (source->parent() == nullptr)) { + // can't change top level + event->ignore(); + return; + } else { + QTreeWidget::dragEnterEvent(event); + } +} + +void ArchiveTreeWidget::dragMoveEvent(QDragMoveEvent* event) +{ + if (!testMovePossible(static_cast(currentItem()), + static_cast(itemAt(event->pos())))) { + event->ignore(); + } else { + QTreeWidget::dragMoveEvent(event); + } +} + +static bool isAncestor(const QTreeWidgetItem* ancestor, const QTreeWidgetItem* item) +{ + QTreeWidgetItem* iter = item->parent(); + while (iter != nullptr) { + if (iter == ancestor) { + return true; + } + iter = iter->parent(); + } + return false; +} + +void ArchiveTreeWidget::refreshItem(ArchiveTreeWidgetItem* item) +{ + if (!item->isPopulated() || item->flags().testFlag(Qt::ItemNeverHasChildren)) { + return; + } + + // at this point, all child items are checked for we only remember the ones + // that were expanded to re-expand them + std::map expanded; + while (item->childCount() > 0) { + auto* child = item->child(0); + expanded[child->entry()->name()] = child->isExpanded(); + item->removeChild(child); + } + + item->populate(true); + + for (int i = 0; i < item->childCount(); ++i) { + auto* child = item->child(i); + if (expanded[child->entry()->name()]) { + child->setExpanded(true); + } + } +} + +void ArchiveTreeWidget::dropEvent(QDropEvent* event) +{ + event->ignore(); + + // target widget (should be a directory) + auto* target = static_cast(itemAt(event->pos())); + + // this should not really happen because it is prevent by dragMoveEvent + if (target->flags().testFlag(Qt::ItemNeverHasChildren)) { + + // this should really not happen, how should a file get to the top level? + if (target->parent() == nullptr) { + return; + } + + target = target->parent(); + } + + // populate target if required + target->populate(); + + auto sourceItems = this->selectedItems(); + + // check the selected items - we do not want to move only + // some items so we check everything first and then move + for (auto* source : sourceItems) { + + auto* aSource = static_cast(source); + + // do not allow element to be dropped into one of its + // own child + if (isAncestor(source, target)) { + event->accept(); + QMessageBox::warning(parentWidget(), tr("Cannot drop"), + tr("Cannot drop '%1' into one of its subfolder.") + .arg(aSource->entry()->name())); + return; + } + + auto sourceEntry = aSource->entry(); + auto targetEntry = target->entry()->astree()->find(sourceEntry->name()); + if (targetEntry && targetEntry->fileType() != sourceEntry->fileType()) { + event->accept(); + QMessageBox::warning(parentWidget(), tr("Cannot drop"), + targetEntry->isFile() + ? tr("A file '%1' already exists in folder '%2'.") + .arg(sourceEntry->name()) + .arg(target->entry()->name()) + : tr("A folder '%1' already exists in folder '%2'.") + .arg(sourceEntry->name()) + .arg(target->entry()->name())); + return; + } + } + + for (auto* source : sourceItems) { + + auto* aSource = static_cast(source); + + // this only check dropping an item on itself or dropping an item in + // its parent so it is ok, it just does not do anything + if (source->parent() == nullptr || !testMovePossible(aSource, target)) { + continue; + } + + // force expand item that are going to be merged + for (int i = 0; i < target->childCount(); ++i) { + auto* child = target->child(i); + if (child->entry()->compare(aSource->entry()->name()) == 0 && + !child->flags().testFlag(Qt::ItemNeverHasChildren)) { + child->setExpanded(true); + } + } + + // remove the source from its parent + source->parent()->removeChild(source); + + // actually perform the move on the underlying tree model + moveItem(aSource, target); + } + + // refresh the target item - this assumes that itemMoved is called synchronously + // and perform the FileTree changes + refreshItem(target); +} diff --git a/src/archivetree.h b/src/archivetree.h index 69b3ec6..9984bea 100644 --- a/src/archivetree.h +++ b/src/archivetree.h @@ -1,187 +1,178 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef ARCHIVETREE_H -#define ARCHIVETREE_H - -#include - -#include "ifiletree.h" - -class ArchiveTreeWidget; - -// custom tree widget that holds a shared pointer to the file tree entry -// they represent -// -class ArchiveTreeWidgetItem : public QTreeWidgetItem { -public: - - ArchiveTreeWidgetItem(QString dataName); - ArchiveTreeWidgetItem(std::shared_ptr entry); - -public: - - // populate this tree widget item if it has not been populated yet - // or if force is true - // - void populate(bool force = false); - - // check if this item has already been populated - // - bool isPopulated() const { return m_Populated; } - - // replace the entry corresponding to this item - // - void setEntry(std::shared_ptr entry) { - m_Entry = entry; - } - - // retrieve the entry corresponding to this item - // - std::shared_ptr entry() const { - return m_Entry; - } - - // overriden method to avoid propagating dataChanged events - // - void setData(int column, int role, const QVariant& value) override; - - ArchiveTreeWidgetItem* parent() const { - return static_cast(QTreeWidgetItem::parent()); - } - - ArchiveTreeWidgetItem* child(int index) const { - return static_cast(QTreeWidgetItem::child(index)); - } - -protected: - - std::shared_ptr m_Entry; - bool m_Populated = false; - - friend class ArchiveTreeWidget; -}; - - -// Qt tree widget used to display the content of an archive in the manual installation -// dialog -class ArchiveTreeWidget : public QTreeWidget -{ - - Q_OBJECT - -public: - - explicit ArchiveTreeWidget(QWidget* parent = 0); - void setup(QString dataFolderName); - -public: - - // set the data root widget - // - void setDataRoot(ArchiveTreeWidgetItem* const root); - - // create a directory under the given tree item, without - // performing any check - // - ArchiveTreeWidgetItem* addDirectory(ArchiveTreeWidgetItem* treeItem, QString name); - - // return the root of the tree (the item corresponding to ) - // - ArchiveTreeWidgetItem* root() const { return m_ViewRoot; } - -signals: - - // emitted when the tree has been modified - // - void treeChanged(); - -public slots: - -protected: - - // detach the entry of this item from its parent, and recursively detach - // all of its parent if they become - // - void detachParents(ArchiveTreeWidgetItem* item); - - // re-attach the entry of this item to its parent, and recursively attach - // all of its parent if they were empty (and thus detached) - // - void attachParents(ArchiveTreeWidgetItem* item); - - // recursively re-insert all the entries below the given item in their - // corresponding parents - // - // this method does not recurse in items that have not been populated yet - // - void recursiveInsert(ArchiveTreeWidgetItem* item); - - // recursively detach all the entries below the given item from their - // corresponding parents - // - // this method does not recurse in items that have not been populated yet - // - void recursiveDetach(ArchiveTreeWidgetItem* item); - - // slot that trigger the given item to be populated if it has not already - // been - // - void populateItem(QTreeWidgetItem* item); - - // move the source under the target - // - void moveItem(ArchiveTreeWidgetItem* source, ArchiveTreeWidgetItem* target); - - // called when the state of the item changed - unlike the standard QTreeWidget, - // this is only called for the actual item, not its parent/children - // - void onTreeCheckStateChanged(ArchiveTreeWidgetItem* item); - - void dragEnterEvent(QDragEnterEvent *event) override; - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; - -private: - - bool testMovePossible(ArchiveTreeWidgetItem* source, ArchiveTreeWidgetItem* target); - - // refresh the given item (after a drop) - // - void refreshItem(ArchiveTreeWidgetItem* item); - - // the widget item that emitted the dataChanged event - ArchiveTreeWidgetItem* m_Emitter = nullptr; - - // IMPORTANT: if you intend to work on this and understand this, read the detailed - // explanation at the beginning of the archivetree.cpp file - // - // - the data root is the real widget of the current data, this widget - // is not the real root that is added to the tree - // - the view root is the actual tree in the widget (should be const but cannot be since - // the parent tree cannot be consstructed in the member initializer list) - // - ArchiveTreeWidgetItem* m_DataRoot; - ArchiveTreeWidgetItem* m_ViewRoot; - - friend class ArchiveTreeWidgetItem; - -}; - -#endif // ARCHIVETREE_H +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef ARCHIVETREE_H +#define ARCHIVETREE_H + +#include + +#include "ifiletree.h" + +class ArchiveTreeWidget; + +// custom tree widget that holds a shared pointer to the file tree entry +// they represent +// +class ArchiveTreeWidgetItem : public QTreeWidgetItem +{ +public: + ArchiveTreeWidgetItem(QString dataName); + ArchiveTreeWidgetItem(std::shared_ptr entry); + +public: + // populate this tree widget item if it has not been populated yet + // or if force is true + // + void populate(bool force = false); + + // check if this item has already been populated + // + bool isPopulated() const { return m_Populated; } + + // replace the entry corresponding to this item + // + void setEntry(std::shared_ptr entry) { m_Entry = entry; } + + // retrieve the entry corresponding to this item + // + std::shared_ptr entry() const { return m_Entry; } + + // overriden method to avoid propagating dataChanged events + // + void setData(int column, int role, const QVariant& value) override; + + ArchiveTreeWidgetItem* parent() const + { + return static_cast(QTreeWidgetItem::parent()); + } + + ArchiveTreeWidgetItem* child(int index) const + { + return static_cast(QTreeWidgetItem::child(index)); + } + +protected: + std::shared_ptr m_Entry; + bool m_Populated = false; + + friend class ArchiveTreeWidget; +}; + +// Qt tree widget used to display the content of an archive in the manual installation +// dialog +class ArchiveTreeWidget : public QTreeWidget +{ + + Q_OBJECT + +public: + explicit ArchiveTreeWidget(QWidget* parent = 0); + void setup(QString dataFolderName); + +public: + // set the data root widget + // + void setDataRoot(ArchiveTreeWidgetItem* const root); + + // create a directory under the given tree item, without + // performing any check + // + ArchiveTreeWidgetItem* addDirectory(ArchiveTreeWidgetItem* treeItem, QString name); + + // return the root of the tree (the item corresponding to ) + // + ArchiveTreeWidgetItem* root() const { return m_ViewRoot; } + +signals: + + // emitted when the tree has been modified + // + void treeChanged(); + +public slots: + +protected: + // detach the entry of this item from its parent, and recursively detach + // all of its parent if they become + // + void detachParents(ArchiveTreeWidgetItem* item); + + // re-attach the entry of this item to its parent, and recursively attach + // all of its parent if they were empty (and thus detached) + // + void attachParents(ArchiveTreeWidgetItem* item); + + // recursively re-insert all the entries below the given item in their + // corresponding parents + // + // this method does not recurse in items that have not been populated yet + // + void recursiveInsert(ArchiveTreeWidgetItem* item); + + // recursively detach all the entries below the given item from their + // corresponding parents + // + // this method does not recurse in items that have not been populated yet + // + void recursiveDetach(ArchiveTreeWidgetItem* item); + + // slot that trigger the given item to be populated if it has not already + // been + // + void populateItem(QTreeWidgetItem* item); + + // move the source under the target + // + void moveItem(ArchiveTreeWidgetItem* source, ArchiveTreeWidgetItem* target); + + // called when the state of the item changed - unlike the standard QTreeWidget, + // this is only called for the actual item, not its parent/children + // + void onTreeCheckStateChanged(ArchiveTreeWidgetItem* item); + + void dragEnterEvent(QDragEnterEvent* event) override; + void dragMoveEvent(QDragMoveEvent* event) override; + void dropEvent(QDropEvent* event) override; + +private: + bool testMovePossible(ArchiveTreeWidgetItem* source, ArchiveTreeWidgetItem* target); + + // refresh the given item (after a drop) + // + void refreshItem(ArchiveTreeWidgetItem* item); + + // the widget item that emitted the dataChanged event + ArchiveTreeWidgetItem* m_Emitter = nullptr; + + // IMPORTANT: if you intend to work on this and understand this, read the detailed + // explanation at the beginning of the archivetree.cpp file + // + // - the data root is the real widget of the current data, this widget + // is not the real root that is added to the tree + // - the view root is the actual tree in the widget (should be const but cannot be + // since + // the parent tree cannot be consstructed in the member initializer list) + // + ArchiveTreeWidgetItem* m_DataRoot; + ArchiveTreeWidgetItem* m_ViewRoot; + + friend class ArchiveTreeWidgetItem; +}; + +#endif // ARCHIVETREE_H diff --git a/src/installdialog.cpp b/src/installdialog.cpp index 426c02d..1861d6f 100644 --- a/src/installdialog.cpp +++ b/src/installdialog.cpp @@ -1,193 +1,215 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "installdialog.h" -#include "ui_installdialog.h" - -#include "report.h" -#include "utility.h" -#include "log.h" - -#include -#include -#include -#include -#include - -using namespace MOBase; - - -InstallDialog::InstallDialog(std::shared_ptr tree, const GuessedValue &modName, std::shared_ptr modDataChecker, const QString& dataName, QWidget *parent) - : TutorableDialog("InstallDialog", parent), - ui(new Ui::InstallDialog), - m_Checker(modDataChecker), - m_DataFolderName(dataName) -{ - - ui->setupUi(this); - - for (auto iter = modName.variants().begin(); iter != modName.variants().end(); ++iter) { - ui->nameCombo->addItem(*iter); - } - - ui->nameCombo->setCurrentIndex(ui->nameCombo->findText(modName)); - ui->nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); - - m_ProblemLabel = ui->problemLabel; - - m_Tree = ui->treeContent; - m_TreeRoot = new ArchiveTreeWidgetItem(tree); - m_Tree->setup(m_DataFolderName); - connect(m_Tree, &ArchiveTreeWidget::treeChanged, [this] { updateProblems(); }); - - m_Tree->setDataRoot(m_TreeRoot); -} - -InstallDialog::~InstallDialog() -{ - delete ui; -} - - -QString InstallDialog::getModName() const -{ - return ui->nameCombo->currentText(); -} - -/** - * @brief Retrieve the user-modified directory structure. - * - * @return the new tree represented by this dialog, which can be a new - * tree or a subtree of the original tree. - **/ -std::shared_ptr InstallDialog::getModifiedTree() const { - return m_Tree->root()->entry()->astree(); -} - -bool InstallDialog::testForProblem() -{ - if (!m_Checker) { - return true; - } - return m_Checker->dataLooksValid(m_Tree->root()->entry()->astree()) == ModDataChecker::CheckReturn::VALID; -} - -void InstallDialog::updateProblems() -{ - if (!m_Checker) { - m_Tree->setStyleSheet("QTreeWidget { border: none; }"); - m_ProblemLabel->setText(tr("Cannot check the content of <%1>.").arg(m_DataFolderName)); - m_ProblemLabel->setToolTip(tr("The plugin for the current game does not provide a way to check the content of <%1>.").arg(m_DataFolderName)); - m_ProblemLabel->setStyleSheet("color: darkYellow;"); - } - else if (testForProblem()) { - m_Tree->setStyleSheet("QTreeWidget { border: 1px solid darkGreen; border-radius: 2px; }"); - m_ProblemLabel->setText(tr("The content of <%1> looks valid.").arg(m_DataFolderName)); - m_ProblemLabel->setToolTip(tr("The content of <%1> seems valid for the current game.").arg(m_DataFolderName)); - m_ProblemLabel->setStyleSheet("color: darkGreen;"); - } else { - m_Tree->setStyleSheet("QTreeWidget { border: 1px solid red; border-radius: 2px; }"); - m_ProblemLabel->setText(tr("The content of <%1> does not look valid.").arg(m_DataFolderName)); - m_ProblemLabel->setToolTip(tr("The content of <%1> is probably not valid for the current game.").arg(m_DataFolderName)); - m_ProblemLabel->setStyleSheet("color: red;"); - } -} - -void InstallDialog::createDirectoryUnder(ArchiveTreeWidgetItem* item) -{ - // Should never happen if we customize the context menu depending - // on the item: - if (!item->entry()->isDir()) { - reportError(tr("Cannot create directory under a file.")); - return; - } - - // Retrieve the directory: - auto fileTree = item->entry()->astree(); - - bool ok = false; - QString result = QInputDialog::getText(this, tr("Enter a directory name"), tr("Name"), - QLineEdit::Normal, QString(), &ok); - result = result.trimmed(); - - if (ok && !result.isEmpty()) { - - // If a file with this name already exists: - if (fileTree->exists(result)) { - reportError(tr("A directory or file with that name already exists.")); - return; - } - - item->setExpanded(true); - auto* newItem = m_Tree->addDirectory(item, result); - m_Tree->scrollToItem(newItem); - } -} - -void InstallDialog::on_treeContent_customContextMenuRequested(QPoint pos) -{ - ArchiveTreeWidgetItem* selectedItem = static_cast(m_Tree->itemAt(pos)); - if (selectedItem == nullptr) { - return; - } - - QMenu menu; - - if (selectedItem != m_Tree->root() && selectedItem->entry()->isDir()) { - menu.addAction(tr("Set as <%1> directory").arg(m_DataFolderName), [this, selectedItem]() { m_Tree->setDataRoot(selectedItem); }); - } - - if (m_Tree->root()->entry() != m_TreeRoot->entry()) { - menu.addAction(tr("Unset <%1> directory").arg(m_DataFolderName), [this]() { m_Tree->setDataRoot(m_TreeRoot); }); - } - - // Add a separator if not empty: - if (!menu.isEmpty()) { - menu.addSeparator(); - } - - if (selectedItem->entry()->isDir()) { - menu.addAction(tr("Create directory..."), [this, selectedItem]() { createDirectoryUnder(selectedItem); }); - } - else { - menu.addAction(tr("&Open"), [this, selectedItem]() { - emit openFile(selectedItem->entry().get()); - }); - } - menu.exec(m_Tree->mapToGlobal(pos)); -} - - -void InstallDialog::on_okButton_clicked() -{ - if (!testForProblem()) { - if (QMessageBox::question(this, tr("Continue?"), - tr("This mod was probably NOT set up correctly, most likely it will NOT work. " - "You should first correct the directory layout using the content-tree."), - QMessageBox::Ignore | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Cancel) { - return; - } - } - this->accept(); -} - -void InstallDialog::on_cancelButton_clicked() -{ - this->reject(); -} +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "installdialog.h" +#include "ui_installdialog.h" + +#include "log.h" +#include "report.h" +#include "utility.h" + +#include +#include +#include +#include +#include + +using namespace MOBase; + +InstallDialog::InstallDialog( + std::shared_ptr tree, const GuessedValue& modName, + std::shared_ptr modDataChecker, + const QString& dataName, QWidget* parent) + : TutorableDialog("InstallDialog", parent), ui(new Ui::InstallDialog), + m_Checker(modDataChecker), m_DataFolderName(dataName) +{ + + ui->setupUi(this); + + for (auto iter = modName.variants().begin(); iter != modName.variants().end(); + ++iter) { + ui->nameCombo->addItem(*iter); + } + + ui->nameCombo->setCurrentIndex(ui->nameCombo->findText(modName)); + ui->nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); + + m_ProblemLabel = ui->problemLabel; + + m_Tree = ui->treeContent; + m_TreeRoot = new ArchiveTreeWidgetItem(tree); + m_Tree->setup(m_DataFolderName); + connect(m_Tree, &ArchiveTreeWidget::treeChanged, [this] { + updateProblems(); + }); + + m_Tree->setDataRoot(m_TreeRoot); +} + +InstallDialog::~InstallDialog() +{ + delete ui; +} + +QString InstallDialog::getModName() const +{ + return ui->nameCombo->currentText(); +} + +/** + * @brief Retrieve the user-modified directory structure. + * + * @return the new tree represented by this dialog, which can be a new + * tree or a subtree of the original tree. + **/ +std::shared_ptr InstallDialog::getModifiedTree() const +{ + return m_Tree->root()->entry()->astree(); +} + +bool InstallDialog::testForProblem() +{ + if (!m_Checker) { + return true; + } + return m_Checker->dataLooksValid(m_Tree->root()->entry()->astree()) == + ModDataChecker::CheckReturn::VALID; +} + +void InstallDialog::updateProblems() +{ + if (!m_Checker) { + m_Tree->setStyleSheet("QTreeWidget { border: none; }"); + m_ProblemLabel->setText( + tr("Cannot check the content of <%1>.").arg(m_DataFolderName)); + m_ProblemLabel->setToolTip(tr("The plugin for the current game does not provide a " + "way to check the content of <%1>.") + .arg(m_DataFolderName)); + m_ProblemLabel->setStyleSheet("color: darkYellow;"); + } else if (testForProblem()) { + m_Tree->setStyleSheet( + "QTreeWidget { border: 1px solid darkGreen; border-radius: 2px; }"); + m_ProblemLabel->setText( + tr("The content of <%1> looks valid.").arg(m_DataFolderName)); + m_ProblemLabel->setToolTip( + tr("The content of <%1> seems valid for the current game.") + .arg(m_DataFolderName)); + m_ProblemLabel->setStyleSheet("color: darkGreen;"); + } else { + m_Tree->setStyleSheet("QTreeWidget { border: 1px solid red; border-radius: 2px; }"); + m_ProblemLabel->setText( + tr("The content of <%1> does not look valid.").arg(m_DataFolderName)); + m_ProblemLabel->setToolTip( + tr("The content of <%1> is probably not valid for the current game.") + .arg(m_DataFolderName)); + m_ProblemLabel->setStyleSheet("color: red;"); + } +} + +void InstallDialog::createDirectoryUnder(ArchiveTreeWidgetItem* item) +{ + // Should never happen if we customize the context menu depending + // on the item: + if (!item->entry()->isDir()) { + reportError(tr("Cannot create directory under a file.")); + return; + } + + // Retrieve the directory: + auto fileTree = item->entry()->astree(); + + bool ok = false; + QString result = QInputDialog::getText(this, tr("Enter a directory name"), tr("Name"), + QLineEdit::Normal, QString(), &ok); + result = result.trimmed(); + + if (ok && !result.isEmpty()) { + + // If a file with this name already exists: + if (fileTree->exists(result)) { + reportError(tr("A directory or file with that name already exists.")); + return; + } + + item->setExpanded(true); + auto* newItem = m_Tree->addDirectory(item, result); + m_Tree->scrollToItem(newItem); + } +} + +void InstallDialog::on_treeContent_customContextMenuRequested(QPoint pos) +{ + ArchiveTreeWidgetItem* selectedItem = + static_cast(m_Tree->itemAt(pos)); + if (selectedItem == nullptr) { + return; + } + + QMenu menu; + + if (selectedItem != m_Tree->root() && selectedItem->entry()->isDir()) { + menu.addAction(tr("Set as <%1> directory").arg(m_DataFolderName), + [this, selectedItem]() { + m_Tree->setDataRoot(selectedItem); + }); + } + + if (m_Tree->root()->entry() != m_TreeRoot->entry()) { + menu.addAction(tr("Unset <%1> directory").arg(m_DataFolderName), [this]() { + m_Tree->setDataRoot(m_TreeRoot); + }); + } + + // Add a separator if not empty: + if (!menu.isEmpty()) { + menu.addSeparator(); + } + + if (selectedItem->entry()->isDir()) { + menu.addAction(tr("Create directory..."), [this, selectedItem]() { + createDirectoryUnder(selectedItem); + }); + } else { + menu.addAction(tr("&Open"), [this, selectedItem]() { + emit openFile(selectedItem->entry().get()); + }); + } + menu.exec(m_Tree->mapToGlobal(pos)); +} + +void InstallDialog::on_okButton_clicked() +{ + if (!testForProblem()) { + if (QMessageBox::question( + this, tr("Continue?"), + tr("This mod was probably NOT set up correctly, most likely it will NOT " + "work. " + "You should first correct the directory layout using the content-tree."), + QMessageBox::Ignore | QMessageBox::Cancel, + QMessageBox::Cancel) == QMessageBox::Cancel) { + return; + } + } + this->accept(); +} + +void InstallDialog::on_cancelButton_clicked() +{ + this->reject(); +} diff --git a/src/installdialog.h b/src/installdialog.h index 5321180..d49afa7 100644 --- a/src/installdialog.h +++ b/src/installdialog.h @@ -1,126 +1,126 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef INSTALLDIALOG_H -#define INSTALLDIALOG_H - -#include "archivetree.h" -#include "tutorabledialog.h" -#include -#include - -#include -#include - -#include -#include -#include -#include -#define WIN32_LEAN_AND_MEAN -#include - - -namespace Ui { - class InstallDialog; -} - - -/** - * a dialog presented to manually define how a mod is to be installed. It provides - * a tree view of the file contents that can modified directly - **/ -class InstallDialog : public MOBase::TutorableDialog -{ - Q_OBJECT - -public: - /** - * @brief Create a new install dialog for the given tree. The tree - * is "own" by the dialog, i.e., any change made by the user is immediately - * reflected to the given tree, except for the changes to the root. - * - * @param tree Tree structure describing the original archive structure. - * @param modName Name of the mod. The name can be modified through the dialog. - * @param modDataChecker The mod data checker to use to check. - * @param dataName The name of the data folder for the game. - * @param parent Parent widget. - **/ - explicit InstallDialog(std::shared_ptr tree, const MOBase::GuessedValue &modName, std::shared_ptr modDataChecker, const QString& dataName, QWidget *parent = 0); - ~InstallDialog(); - - /** - * @brief retrieve the (modified) mod name - * - * @return updated mod name - **/ - QString getModName() const; - - /** - * @brief Retrieve the user-modified directory structure. - * - * @return the new tree represented by this dialog, which can be a new - * tree or a subtree of the original tree. - **/ - std::shared_ptr getModifiedTree() const; - -signals: - - /** - * @brief Signal emitted when user request the file corresponding - * to the given entry to be opened. - * - * @param entry Entry corresponding to the file to open. - */ - void openFile(const MOBase::FileTreeEntry *entry); - -private: - - bool testForProblem(); - void updateProblems(); - void createDirectoryUnder(ArchiveTreeWidgetItem* treeItem); - -private slots: - - // Automatic slots that are directly bound to the UI: - void on_treeContent_customContextMenuRequested(QPoint pos); - void on_cancelButton_clicked(); - void on_okButton_clicked(); - -private: - Ui::InstallDialog *ui; - - std::shared_ptr m_Checker; - - // Name of the "data" directory: - QString m_DataFolderName; - - // the tree root is the initial root that will never change (should be const - // but cannot be since the parent tree cannot be constructed in the member - // initializer list) - // - // the tree root is not actually added to the tree, but is used to maintain - // the state of the tree and not lose entries when unsetting data root - // - ArchiveTreeWidget *m_Tree; - ArchiveTreeWidgetItem* m_TreeRoot; - QLabel *m_ProblemLabel; - -}; - -#endif // INSTALLDIALOG_H +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef INSTALLDIALOG_H +#define INSTALLDIALOG_H + +#include "archivetree.h" +#include "tutorabledialog.h" +#include +#include + +#include +#include + +#include +#include +#include +#include +#define WIN32_LEAN_AND_MEAN +#include + +namespace Ui +{ +class InstallDialog; +} + +/** + * a dialog presented to manually define how a mod is to be installed. It provides + * a tree view of the file contents that can modified directly + **/ +class InstallDialog : public MOBase::TutorableDialog +{ + Q_OBJECT + +public: + /** + * @brief Create a new install dialog for the given tree. The tree + * is "own" by the dialog, i.e., any change made by the user is immediately + * reflected to the given tree, except for the changes to the root. + * + * @param tree Tree structure describing the original archive structure. + * @param modName Name of the mod. The name can be modified through the dialog. + * @param modDataChecker The mod data checker to use to check. + * @param dataName The name of the data folder for the game. + * @param parent Parent widget. + **/ + explicit InstallDialog(std::shared_ptr tree, + const MOBase::GuessedValue& modName, + std::shared_ptr modDataChecker, + const QString& dataName, QWidget* parent = 0); + ~InstallDialog(); + + /** + * @brief retrieve the (modified) mod name + * + * @return updated mod name + **/ + QString getModName() const; + + /** + * @brief Retrieve the user-modified directory structure. + * + * @return the new tree represented by this dialog, which can be a new + * tree or a subtree of the original tree. + **/ + std::shared_ptr getModifiedTree() const; + +signals: + + /** + * @brief Signal emitted when user request the file corresponding + * to the given entry to be opened. + * + * @param entry Entry corresponding to the file to open. + */ + void openFile(const MOBase::FileTreeEntry* entry); + +private: + bool testForProblem(); + void updateProblems(); + void createDirectoryUnder(ArchiveTreeWidgetItem* treeItem); + +private slots: + + // Automatic slots that are directly bound to the UI: + void on_treeContent_customContextMenuRequested(QPoint pos); + void on_cancelButton_clicked(); + void on_okButton_clicked(); + +private: + Ui::InstallDialog* ui; + + std::shared_ptr m_Checker; + + // Name of the "data" directory: + QString m_DataFolderName; + + // the tree root is the initial root that will never change (should be const + // but cannot be since the parent tree cannot be constructed in the member + // initializer list) + // + // the tree root is not actually added to the tree, but is used to maintain + // the state of the tree and not lose entries when unsetting data root + // + ArchiveTreeWidget* m_Tree; + ArchiveTreeWidgetItem* m_TreeRoot; + QLabel* m_ProblemLabel; +}; + +#endif // INSTALLDIALOG_H diff --git a/src/installermanual.cpp b/src/installermanual.cpp index 661c3f1..25d302d 100644 --- a/src/installermanual.cpp +++ b/src/installermanual.cpp @@ -1,138 +1,130 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "installermanual.h" - -#include -#include -#include -#include -#include - -#include -#include - -#include - -#include "installdialog.h" - - -using namespace MOBase; - - -InstallerManual::InstallerManual() - : m_MOInfo(nullptr) -{ -} - -bool InstallerManual::init(IOrganizer* moInfo) -{ - m_MOInfo = moInfo; - return true; -} - -QString InstallerManual::name() const -{ - return "Manual Installer"; -} - -QString InstallerManual::localizedName() const -{ - return tr("Manual Installer"); -} - -QString InstallerManual::author() const -{ - return "Tannin, Holt59"; -} - -QString InstallerManual::description() const -{ - return tr("Fallback installer for mods that can be extracted but can't be handled by another installer"); -} - -VersionInfo InstallerManual::version() const -{ - return VersionInfo(1, 0, 1, VersionInfo::RELEASE_FINAL); -} - -QList InstallerManual::settings() const -{ - return QList(); -} - -unsigned int InstallerManual::priority() const -{ - return 0; -} - - -bool InstallerManual::isManualInstaller() const -{ - return true; -} - - -bool InstallerManual::isArchiveSupported(std::shared_ptr) const -{ - return true; -} - - -void InstallerManual::openFile(const FileTreeEntry* entry) -{ - QString tempName = manager()->extractFile(entry->shared_from_this()); - - SHELLEXECUTEINFOW execInfo; - memset(&execInfo, 0, sizeof(SHELLEXECUTEINFOW)); - execInfo.cbSize = sizeof(SHELLEXECUTEINFOW); - execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; - execInfo.lpVerb = L"open"; - std::wstring fileNameW = ToWString(tempName); - execInfo.lpFile = fileNameW.c_str(); - execInfo.nShow = SW_SHOWNORMAL; - if (!::ShellExecuteExW(&execInfo)) { - qCritical("failed to spawn %s: %d", qUtf8Printable(tempName), ::GetLastError()); - } -} - - -IPluginInstaller::EInstallResult InstallerManual::install( - GuessedValue& modName, std::shared_ptr& tree, QString&, int&) -{ - qDebug("offering installation dialog"); - InstallDialog dialog(tree, modName, m_MOInfo->gameFeatures()->gameFeature(), - m_MOInfo->managedGame()->dataDirectory().dirName().toLower(), parentWidget()); - connect(&dialog, &InstallDialog::openFile, this, &InstallerManual::openFile); - if (dialog.exec() == QDialog::Accepted) { - modName.update(dialog.getModName(), GUESS_USER); - - // TODO probably more complicated than necessary - tree = dialog.getModifiedTree(); - return IPluginInstaller::RESULT_SUCCESS; - } - else { - return IPluginInstaller::RESULT_CANCELED; - } -} - -#if QT_VERSION < QT_VERSION_CHECK(5,0,0) -Q_EXPORT_PLUGIN2(installerManual, InstallerManual) -#endif - +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "installermanual.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "installdialog.h" + +using namespace MOBase; + +InstallerManual::InstallerManual() : m_MOInfo(nullptr) {} + +bool InstallerManual::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + return true; +} + +QString InstallerManual::name() const +{ + return "Manual Installer"; +} + +QString InstallerManual::localizedName() const +{ + return tr("Manual Installer"); +} + +QString InstallerManual::author() const +{ + return "Tannin, Holt59"; +} + +QString InstallerManual::description() const +{ + return tr("Fallback installer for mods that can be extracted but can't be handled by " + "another installer"); +} + +VersionInfo InstallerManual::version() const +{ + return VersionInfo(1, 0, 1, VersionInfo::RELEASE_FINAL); +} + +QList InstallerManual::settings() const +{ + return QList(); +} + +unsigned int InstallerManual::priority() const +{ + return 0; +} + +bool InstallerManual::isManualInstaller() const +{ + return true; +} + +bool InstallerManual::isArchiveSupported(std::shared_ptr) const +{ + return true; +} + +void InstallerManual::openFile(const FileTreeEntry* entry) +{ + QString tempName = manager()->extractFile(entry->shared_from_this()); + + SHELLEXECUTEINFOW execInfo; + memset(&execInfo, 0, sizeof(SHELLEXECUTEINFOW)); + execInfo.cbSize = sizeof(SHELLEXECUTEINFOW); + execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + execInfo.lpVerb = L"open"; + std::wstring fileNameW = ToWString(tempName); + execInfo.lpFile = fileNameW.c_str(); + execInfo.nShow = SW_SHOWNORMAL; + if (!::ShellExecuteExW(&execInfo)) { + qCritical("failed to spawn %s: %d", qUtf8Printable(tempName), ::GetLastError()); + } +} + +IPluginInstaller::EInstallResult +InstallerManual::install(GuessedValue& modName, + std::shared_ptr& tree, QString&, int&) +{ + qDebug("offering installation dialog"); + InstallDialog dialog( + tree, modName, m_MOInfo->gameFeatures()->gameFeature(), + m_MOInfo->managedGame()->dataDirectory().dirName().toLower(), parentWidget()); + connect(&dialog, &InstallDialog::openFile, this, &InstallerManual::openFile); + if (dialog.exec() == QDialog::Accepted) { + modName.update(dialog.getModName(), GUESS_USER); + + // TODO probably more complicated than necessary + tree = dialog.getModifiedTree(); + return IPluginInstaller::RESULT_SUCCESS; + } else { + return IPluginInstaller::RESULT_CANCELED; + } +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(installerManual, InstallerManual) +#endif diff --git a/src/installermanual.h b/src/installermanual.h index 333047c..14f16d3 100644 --- a/src/installermanual.h +++ b/src/installermanual.h @@ -1,75 +1,72 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef INSTALLERMANUAL_H -#define INSTALLERMANUAL_H - -#include -#include - - -class InstallerManual : public MOBase::IPluginInstallerSimple -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) -#if QT_VERSION >= QT_VERSION_CHECK(5,0,0) - Q_PLUGIN_METADATA(IID "org.tannin.InstallerManual" FILE "installermanual.json") -#endif - -public: - - InstallerManual(); - - virtual bool init(MOBase::IOrganizer* moInfo) override; - virtual QString name() const override; - virtual QString localizedName() const override; - virtual QString author() const override; - virtual QString description() const override; - virtual MOBase::VersionInfo version() const override; - virtual QList settings() const override; - - virtual unsigned int priority() const; - virtual bool isManualInstaller() const; - - virtual bool isArchiveSupported(std::shared_ptr tree) const; - virtual EInstallResult install(MOBase::GuessedValue &modName, std::shared_ptr &tree, - QString &version, int &modID); - -private: - - bool isSimpleArchiveTopLayer(const std::shared_ptr tree) const; - std::shared_ptr getSimpleArchiveBase(const std::shared_ptr tree) const; - -private slots: - - /** - * @brief Opens a file from the archive in the (system-)default editor/viewer. - * - * @param entry Entry corresponding to the file to open. - */ - void openFile(const MOBase::FileTreeEntry* entry); - -private: - - const MOBase::IOrganizer *m_MOInfo; - -}; - - -#endif // INSTALLERMANUAL_H +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef INSTALLERMANUAL_H +#define INSTALLERMANUAL_H + +#include +#include + +class InstallerManual : public MOBase::IPluginInstallerSimple +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.InstallerManual" FILE "installermanual.json") +#endif + +public: + InstallerManual(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + + virtual unsigned int priority() const; + virtual bool isManualInstaller() const; + + virtual bool isArchiveSupported(std::shared_ptr tree) const; + virtual EInstallResult install(MOBase::GuessedValue& modName, + std::shared_ptr& tree, + QString& version, int& modID); + +private: + bool + isSimpleArchiveTopLayer(const std::shared_ptr tree) const; + std::shared_ptr + getSimpleArchiveBase(const std::shared_ptr tree) const; + +private slots: + + /** + * @brief Opens a file from the archive in the (system-)default editor/viewer. + * + * @param entry Entry corresponding to the file to open. + */ + void openFile(const MOBase::FileTreeEntry* entry); + +private: + const MOBase::IOrganizer* m_MOInfo; +}; + +#endif // INSTALLERMANUAL_H