summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/createinstancedialog.cpp10
-rw-r--r--src/createinstancedialogpages.cpp173
-rw-r--r--src/createinstancedialogpages.h368
3 files changed, 448 insertions, 103 deletions
diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp
index f52d3ebd..1c1b62d0 100644
--- a/src/createinstancedialog.cpp
+++ b/src/createinstancedialog.cpp
@@ -13,6 +13,12 @@ using namespace MOBase;
class Failed {};
+// create() will create all the directories in `target`; if any path component
+// fails to create, it will throw Failed
+//
+// unless commit() is called, all the created directories will be deleted in
+// the destructor
+//
class DirectoryCreator
{
public:
@@ -39,6 +45,7 @@ public:
{
try
{
+ // delete each directory starting from the end
for (auto itor=m_created.rbegin(); itor!=m_created.rend(); ++itor) {
const auto r = shell::DeleteDirectoryRecursive(*itor);
if (!r) {
@@ -62,6 +69,7 @@ private:
{
try
{
+ // split on separators
const QString s = QDir::toNativeSeparators(target.absolutePath());
const QStringList cs = s.split("\\");
@@ -69,8 +77,10 @@ private:
return;
}
+ // root directory
QDir d(cs[0]);
+ // for each directory after the root
for (int i=1; i<cs.size(); ++i) {
d = d.filePath(cs[i]);
diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp
index a08bf5a1..8431abd1 100644
--- a/src/createinstancedialogpages.cpp
+++ b/src/createinstancedialogpages.cpp
@@ -21,6 +21,21 @@ QString makeDefaultPath(const std::wstring& dir)
QString::fromStdWString(dir)));
}
+QString toLocalizedString(CreateInstanceDialog::Types t)
+{
+ switch (t)
+ {
+ case CreateInstanceDialog::Global:
+ return QObject::tr("Global");
+
+ case CreateInstanceDialog::Portable:
+ return QObject::tr("Portable");
+
+ default:
+ return QObject::tr("Instance type: %1").arg(QObject::tr("?"));
+ }
+}
+
PlaceholderLabel::PlaceholderLabel(QLabel* label)
@@ -333,6 +348,15 @@ std::vector<IPluginGame*> GamePage::sortedGamePlugins() const
return v;
}
+void GamePage::createGames()
+{
+ m_games.clear();
+
+ for (auto* game : sortedGamePlugins()) {
+ m_games.push_back(std::make_unique<Game>(game));
+ }
+}
+
GamePage::Game* GamePage::findGame(IPluginGame* game)
{
for (auto& g : m_games) {
@@ -344,13 +368,24 @@ GamePage::Game* GamePage::findGame(IPluginGame* game)
return nullptr;
}
-void GamePage::createGames()
+void GamePage::createGameButton(Game* g)
{
- m_games.clear();
+ g->button = new QCommandLinkButton;
+ g->button->setCheckable(true);
- for (auto* game : sortedGamePlugins()) {
- m_games.push_back(std::make_unique<Game>(game));
- }
+ updateButton(g);
+
+ QObject::connect(g->button, &QAbstractButton::clicked, [g, this] {
+ select(g->game);
+ });
+}
+
+void GamePage::addButton(QAbstractButton* b)
+{
+ auto* ly = static_cast<QVBoxLayout*>(ui->games->layout());
+
+ // insert before the stretch
+ ly->insertWidget(ly->count() - 1, b);
}
void GamePage::updateButton(Game* g)
@@ -407,6 +442,26 @@ void GamePage::selectButton(Game* g)
}
}
+void GamePage::clearButtons()
+{
+ auto* ly = static_cast<QVBoxLayout*>(ui->games->layout());
+
+ ui->games->setUpdatesEnabled(false);
+
+ // delete all children
+ qDeleteAll(ui->games->findChildren<QWidget*>("", Qt::FindDirectChildrenOnly));
+
+ // stretch widgets added with addStretch() are not in the parent widget,
+ // they have to be deleted from the layout itself
+ while (auto* child=ly->takeAt(0))
+ delete child;
+
+ // add a stretch, buttons will be added before
+ ly->addStretch();
+
+ ui->games->setUpdatesEnabled(true);
+}
+
QCommandLinkButton* GamePage::createCustomButton()
{
auto* b = new QCommandLinkButton;
@@ -422,18 +477,6 @@ QCommandLinkButton* GamePage::createCustomButton()
return b;
}
-void GamePage::createGameButton(Game* g)
-{
- g->button = new QCommandLinkButton;
- g->button->setCheckable(true);
-
- updateButton(g);
-
- QObject::connect(g->button, &QAbstractButton::clicked, [g, this] {
- select(g->game);
- });
-}
-
void GamePage::fillList()
{
const bool showAll = ui->showAllGames->isChecked();
@@ -459,34 +502,6 @@ void GamePage::fillList()
addButton(createCustomButton());
}
-void GamePage::clearButtons()
-{
- auto* ly = static_cast<QVBoxLayout*>(ui->games->layout());
-
- ui->games->setUpdatesEnabled(false);
-
- // delete all children
- qDeleteAll(ui->games->findChildren<QWidget*>("", Qt::FindDirectChildrenOnly));
-
- // stretch widgets added with addStretch() are not in the parent widget,
- // they have to be deleted from the layout itself
- while (auto* child=ly->takeAt(0))
- delete child;
-
- // add a stretch, buttons will be added before
- ly->addStretch();
-
- ui->games->setUpdatesEnabled(true);
-}
-
-void GamePage::addButton(QAbstractButton* b)
-{
- auto* ly = static_cast<QVBoxLayout*>(ui->games->layout());
-
- // insert before the stretch
- ly->insertWidget(ly->count() - 1, b);
-}
-
GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g)
{
if (g->game->looksValid(path)) {
@@ -495,7 +510,15 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g)
}
// the selected game can't use that folder, find another one
- auto* otherGame = findAnotherGame(path);
+ IPluginGame* otherGame = nullptr;
+
+ for (auto* gg : m_pc.plugins<IPluginGame>()) {
+ if (gg->looksValid(path)) {
+ otherGame = gg;
+ break;
+ }
+ }
+
if (otherGame == g->game) {
// shouldn't happen, but okay
return g;
@@ -531,17 +554,6 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g)
return g;
}
-IPluginGame* GamePage::findAnotherGame(const QString& path)
-{
- for (auto* otherGame : m_pc.plugins<IPluginGame>()) {
- if (otherGame->looksValid(path)) {
- return otherGame;
- }
- }
-
- return nullptr;
-}
-
bool GamePage::confirmUnknown(const QString& path, IPluginGame* game)
{
const auto r = TaskDialog(&m_dlg)
@@ -733,7 +745,7 @@ void NamePage::activated()
m_modified = false;
}
- updateWarnings();
+ verify();
}
QString NamePage::selectedInstanceName() const
@@ -749,13 +761,12 @@ QString NamePage::selectedInstanceName() const
void NamePage::onChanged()
{
m_modified = true;
- updateWarnings();
+ verify();
}
-void NamePage::updateWarnings()
+void NamePage::verify()
{
const auto root = InstanceManager::singleton().globalInstancesRootPath();
-
m_okay = checkName(root, ui->instanceName->text());
updateNavigation();
}
@@ -803,7 +814,8 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) :
m_label(ui->pathsLabel),
m_simpleExists(ui->locationExists), m_simpleInvalid(ui->locationInvalid),
m_advancedExists(ui->advancedDirExists),
- m_advancedInvalid(ui->advancedDirInvalid)
+ m_advancedInvalid(ui->advancedDirInvalid),
+ m_okay(false)
{
QObject::connect(ui->location, &QLineEdit::textEdited, [&]{ onChanged(); });
QObject::connect(ui->base, &QLineEdit::textEdited, [&]{ onChanged(); });
@@ -821,7 +833,7 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) :
bool PathsPage::ready() const
{
- return checkPaths();
+ return m_okay;
}
void PathsPage::activated()
@@ -863,21 +875,27 @@ void PathsPage::onChanged()
updateNavigation();
}
-bool PathsPage::checkPaths() const
+void PathsPage::checkPaths()
{
if (ui->advancedPathOptions->isChecked()) {
- return
+ m_okay =
checkAdvancedPath(ui->base->text()) &&
checkAdvancedPath(resolve(ui->downloads->text())) &&
checkAdvancedPath(resolve(ui->mods->text())) &&
checkAdvancedPath(resolve(ui->profiles->text())) &&
checkAdvancedPath(resolve(ui->overwrite->text()));
} else {
- return checkPath(ui->location->text(), m_simpleExists, m_simpleInvalid);
+ m_okay =
+ checkSimplePath(ui->location->text());
}
}
-bool PathsPage::checkAdvancedPath(const QString& path) const
+bool PathsPage::checkSimplePath(const QString& path)
+{
+ return checkPath(path, m_simpleExists, m_simpleInvalid);
+}
+
+bool PathsPage::checkAdvancedPath(const QString& path)
{
return checkPath(path, m_advancedExists, m_advancedInvalid);
}
@@ -931,7 +949,7 @@ void PathsPage::setIfEmpty(QLineEdit* e, const QString& path, bool force)
bool PathsPage::checkPath(
QString path,
- PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) const
+ PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel)
{
bool exists = false;
bool invalid = false;
@@ -1013,10 +1031,6 @@ bool NexusPage::doSkip() const
return m_skip;
}
-void NexusPage::activated()
-{
-}
-
ConfirmationPage::ConfirmationPage(CreateInstanceDialog& dlg)
: Page(dlg)
@@ -1029,21 +1043,6 @@ void ConfirmationPage::activated()
ui->creationLog->clear();
}
-QString ConfirmationPage::toLocalizedString(CreateInstanceDialog::Types t) const
-{
- switch (t)
- {
- case CreateInstanceDialog::Global:
- return QObject::tr("Global");
-
- case CreateInstanceDialog::Portable:
- return QObject::tr("Portable");
-
- default:
- return QObject::tr("Instance type: %1").arg(QObject::tr("?"));
- }
-}
-
QString ConfirmationPage::makeReview() const
{
QStringList lines;
diff --git a/src/createinstancedialogpages.h b/src/createinstancedialogpages.h
index 099071a2..e2eaf0fb 100644
--- a/src/createinstancedialogpages.h
+++ b/src/createinstancedialogpages.h
@@ -15,14 +15,26 @@ class NexusConnectionUI;
namespace cid
{
+// returns "%base_dir%/dir"
+//
QString makeDefaultPath(const std::wstring& dir);
+// remembers the original text of the given label and, if it contains a %1,
+// sets it in setText()
+//
class PlaceholderLabel
{
public:
PlaceholderLabel(QLabel* label);
+
+ // if the original label text contained a %1, replaces it by the arg and
+ // sets that as the new label text
+ //
void setText(const QString& arg);
+
+ // whether the label is visible
+ //
void setVisible(bool b);
private:
@@ -31,25 +43,67 @@ private:
};
+// one page in the wizard
+//
+// each page can implement one or more selected*() below; those are called
+// by CreateInstanceDialog to gather data from all pages
+//
class Page
{
public:
Page(CreateInstanceDialog& dlg);
+ // whether this page has been filled and is valid; used by the dialog to
+ // determine if it can move to the next page
+ //
virtual bool ready() const;
+
+ // called every time a page is shown in the screen
+ //
virtual void activated();
+ // overrides whether this page should be skipped; this is used by
+ // CreateInstanceDialog::setSinglePage() to disable all other pages
+ //
void setSkip(bool b);
+
+ // whether this page should be skipped
+ //
bool skip() const;
+ // asks the dialog to update its navigation buttons, typically used when a
+ // page changes its ready state without moving to a different page
+ //
void updateNavigation();
+
+ // asks the dialog to move to the next page; some pages will automatically
+ // advance once the user has made the proper selection
+ //
void next();
+
+ // returns the instance type
+ //
virtual CreateInstanceDialog::Types selectedInstanceType() const;
+
+ // returns the game plugin
+ //
virtual MOBase::IPluginGame* selectedGame() const;
+
+ // returns the game directory
+ //
virtual QString selectedGameLocation() const;
+
+ // returns the game variant
+ //
virtual QString selectedGameVariant(MOBase::IPluginGame* game) const;
+
+ // returns the instance name
+ //
virtual QString selectedInstanceName() const;
+
+ // returns the various paths
+ //
virtual CreateInstanceDialog::Paths selectedPaths() const;
protected:
@@ -58,10 +112,15 @@ protected:
const PluginContainer& m_pc;
bool m_skip;
+
+ // implemented by derived classes, overridden by setSkip(true)
+ //
virtual bool doSkip() const;
};
+// introduction page, can be disabled by a global setting
+//
class IntroPage : public Page
{
public:
@@ -72,15 +131,27 @@ protected:
};
+// instance type page
+//
class TypePage : public Page
{
public:
TypePage(CreateInstanceDialog& dlg);
+ // whether a type has been been selected
+ //
bool ready() const override;
+
+ // returns the selected type
+ //
CreateInstanceDialog::Types selectedInstanceType() const override;
+ // selects a global instance
+ //
void global();
+
+ // selects a portable instance
+ //
void portable();
private:
@@ -88,160 +159,425 @@ private:
};
+// game plugin page, displays a list of command buttons for each game, along
+// with a "browse" button for custom directories and filtering stuff
+//
+// the game list initially only shows plugins that report isInstalled(), and the
+// user has two ways of specifying paths for games that were not found:
+//
+// 1) by clicking the "Browse..." button and selecting an arbitrary directory
+//
+// all plugins are checked until one returns true for looksValid(); if none
+// of them do, this is an error
+//
+// 2) by checking the "Show all supported games" checkbox and clicking one
+// of the games on the list
+//
+// if the selected plugin doesn't recognize the directory, the user is
+// warned, but is allowed to continue; there's also some logic to try to
+// find another plugin that can manage this directory and suggest it
+// instead
+//
class GamePage : public Page
{
public:
GamePage(CreateInstanceDialog& dlg);
+ // whether a game has been selected
+ //
bool ready() const override;
+
+ // returns the selected game
+ //
MOBase::IPluginGame* selectedGame() const override;
+
+ // returns the selected game directory
QString selectedGameLocation() const override;
+
+ // selects the given game and toggles its associated button; the game
+ // directory can be overridden
+ //
+ // pops up a directory selection dialog if `dir` is empty and the plugin
+ // hasn't detected the game
+ //
void select(MOBase::IPluginGame* game, const QString& dir={});
+
+ // pops up a directory selection dialog and looks for a plugin to manage
+ // it
+ //
void selectCustom();
+ // pops up a warning dialog that the game at the given path is not supported
+ // by any plugin, includes a list of all game plugins in the details section
+ // of the dialog
+ //
void warnUnrecognized(const QString& path);
private:
+ // a single game, with its button and custom directory, if any
+ //
struct Game
{
+ // game plugin
MOBase::IPluginGame* game = nullptr;
+
+ // button on the ui
QCommandLinkButton* button = nullptr;
+
+ // game directory; set in ctor if the plugin has detected the game, or
+ // set later when the user selects a directory
QString dir;
+
+ // whether a directory has been set for this game, either auto detected
+ // or by the user
bool installed = false;
+
Game(MOBase::IPluginGame* g);
Game(const Game&) = delete;
Game& operator=(const Game&) = delete;
};
+ // list of all game plugins, even if they're not installed; those are filtered
+ // from the ui if the checkbox isn't checked
std::vector<std::unique_ptr<Game>> m_games;
+
+ // current selection
Game* m_selection;
+
+ // filter
MOBase::FilterWidget m_filter;
+
+ // returns a list of all the game plugins sorted with natsort
+ //
std::vector<MOBase::IPluginGame*> sortedGamePlugins() const;
- Game* findGame(MOBase::IPluginGame* game);
+
+ // creates the m_games list
+ //
void createGames();
+
+ // finds the game struct associated with the given game
+ //
+ Game* findGame(MOBase::IPluginGame* game);
+
+
+ // creates the ui for the given game button
+ //
+ void createGameButton(Game* g);
+
+ // adds the given button to the ui
+ //
+ void addButton(QAbstractButton* b);
+
+ // updates the given button on the ui, sets the text, icon, etc.
+ //
void updateButton(Game* g);
+
+ // called when a button has been clicked; selects the game or asks the user
+ // for directory, depending
+ //
void selectButton(Game* g);
+
+ // removes all buttons from the ui
+ //
void clearButtons();
- void addButton(QAbstractButton* b);
+
+ // creates the "Browse" button
+ //
QCommandLinkButton* createCustomButton();
- void createGameButton(Game* g);
+
+
+ // clears the button list and adds all the buttons to it, depending on
+ // filtering and stuff
+ //
void fillList();
- void onFilter();
+
+ // checks whether the given path looks valid to the given game plugin
+ //
+ // if the plugin doesn't like the path, allows the user to override and
+ // accept, but also attempts to find another plugin that wants it and
+ // propose that as an alternative, if there's one
+ //
+ // returns:
+ // - if the user selects the alternative plugin, returns that plugin
+ // instead;
+ // - if the path is bad but the user overrides, returns the given plugin
+ // - if the user cancels or if no plugins can manage the directory, returns
+ // null
+ //
Game* checkInstallation(const QString& path, Game* g);
- MOBase::IPluginGame* findAnotherGame(const QString& path);
+
+ // tells the user that the path cannot be handled by any game plugin, returns
+ // true if the user decides to accept anyway
+ //
bool confirmUnknown(const QString& path, MOBase::IPluginGame* game);
+
+ // tells the user that the path can be handled by a different plugin than the
+ // selected one and allows them to either
+ // 1) use the alternative, guessedGame is returned;
+ // 2) use the selection anyway, selectedGame is returned; or
+ // 3) cancel, null is returned
+ //
MOBase::IPluginGame* confirmOtherGame(
const QString& path,
MOBase::IPluginGame* selectedGame, MOBase::IPluginGame* guessedGame);
};
+// game variants page; displays a list of command buttons for game variants, as
+// reported by the game plugin
+//
+// this page is always skipped if the game plugin reports no variants
+//
class VariantsPage : public Page
{
public:
VariantsPage(CreateInstanceDialog& dlg);
+ // whether a variant has been selected or the game plugin reports no variants
+ //
bool ready() const override;
+
+ // uses the game selected in the previous page to fill the list, this must be
+ // called every time because the user may go back in forth in the wizard
+ //
void activated() override;
+
+ // returns the selected variant, if any
+ //
QString selectedGameVariant(MOBase::IPluginGame* game) const override;
+ // selects the given variant
+ //
void select(const QString& variant);
protected:
+ // returns true if the game has no variants
+ //
bool doSkip() const override;
private:
+ // game that was selected the last time this page was active
MOBase::IPluginGame* m_previousGame;
+
+ // buttons
std::vector<QCommandLinkButton*> m_buttons;
+
+ // selected variant
QString m_selection;
+
+ // fills the list with buttons
void fillList();
};
+// instance name page; displays a textbox where the user can enter a name and
+// does basic checks to make sure the name is valid and not a duplicate
+//
+// skipped for portable instances
+//
class NamePage : public Page
{
public:
NamePage(CreateInstanceDialog& dlg);
+ // whether a valid name has been entered
+ //
bool ready() const override;
+
+ // uses the selected game to generate an instance name
+ //
+ // as long as the user hasn't modified the textbox, this will regenerate a new
+ // instance name every time the selected game changes
+ //
void activated() override;
+
+ // returns the instance name
+ //
QString selectedInstanceName() const override;
protected:
+ // returns true for portable instances
+ //
bool doSkip() const override;
private:
- mutable PlaceholderLabel m_label, m_exists, m_invalid;
+ // game label, replaces %1 with the game name
+ PlaceholderLabel m_label;
+
+ // "instance already exists" label, replaces %1 with instance name
+ PlaceholderLabel m_exists;
+
+ // "instance name invalid" label, replaces %1 with instance name
+ PlaceholderLabel m_invalid;
+
+ // whether the user has modified the text, prevents auto generation when the
+ // selected game changes
bool m_modified;
+
+ // whether the instance name is valid
bool m_okay;
+
+ // called when the user modifies the textbox, remember that it has changed and
+ // calls verify()
+ //
void onChanged();
- void updateWarnings();
+
+ // check if the entered name is valid, sets m_okay and calls checkName()
+ //
+ void verify();
+
+ // updates the ui depending on whether the given instance name is valid in
+ // the given directory; returns false if the name is invalid
+ //
bool checkName(QString parentDir, QString name);
};
+// instance paths page; shows a single textbox for the base directory, or a
+// series of textboxes for all the configurable paths if the advanced checkbox
+// is checked
+//
class PathsPage : public Page
{
public:
PathsPage(CreateInstanceDialog& dlg);
+ // whether all paths make sense
+ //
bool ready() const override;
+
+ // resets all the paths if the instance type or instance name have changed,
+ // the current values are kept as long as these don't change; also updates the
+ // game name in the ui
+ //
void activated() override;
+ // returns the selected paths
+ //
CreateInstanceDialog::Paths selectedPaths() const override;
private:
+ // instance name the last time this page was active
QString m_lastInstanceName;
+
+ // instance type the last time this page was active
CreateInstanceDialog::Types m_lastType;
+
+ // help label, replaces %1 by the game name
PlaceholderLabel m_label;
- mutable PlaceholderLabel m_simpleExists, m_simpleInvalid;
- mutable PlaceholderLabel m_advancedExists, m_advancedInvalid;
+ // path exists/is invalid labels for the simple page, replaces %1 with the
+ // path
+ PlaceholderLabel m_simpleExists, m_simpleInvalid;
+
+ // path exists/is invalid labels for the advanced page, replaces %1 with the
+ // path
+ PlaceholderLabel m_advancedExists, m_advancedInvalid;
+
+ // whether the paths are valid
+ bool m_okay;
+
+
+ // called when the user changes any textbox, checks the path and updates nav
+ //
void onChanged();
- bool checkPaths() const;
- bool checkAdvancedPath(const QString& path) const;
+
+ // checks the simple or advanced paths, sets m_okay
+ //
+ void checkPaths();
+
+ // checks a simple path, forwards to checkPath() with the simple labels
+ //
+ bool checkSimplePath(const QString& path);
+
+ // checks an advanced path, forwards to checkPath() with the advanced labels
+ //
+ bool checkAdvancedPath(const QString& path);
+
+ // returns false if the path is invalid or already exists, sets the given
+ // labels accordingly
+ //
+ bool checkPath(
+ QString path,
+ PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel);
+
+ // replaces %base_dir% in the given path by whatever's in the base path
+ // textbox
+ //
QString resolve(const QString& path) const;
+
+ // called when the advanced checkbox is toggled, switches the active page
+ // and checks the paths
+ //
void onAdvanced();
+
+ // called whenever the page becomes active
+ //
+ // this normally doesn't change the textboxes unless they're empty, but if the
+ // instance name or type have changed, `force` is true, which forces all paths
+ // to reset
+ //
void setPaths(const QString& name, bool force);
+
+ // sets the given textbox to the path if it's empty or if `force` is true
+ //
void setIfEmpty(QLineEdit* e, const QString& path, bool force);
- bool checkPath(
- QString path,
- PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) const;
};
+// nexus connection page; this reuses the ui found in the settings dialog and
+// is skipped if there's already an api key in the credentials manager
+//
class NexusPage : public Page
{
public:
NexusPage(CreateInstanceDialog& dlg);
~NexusPage();
+ // always returns true, this is an optional page
+ //
bool ready() const override;
- void activated() override;
protected:
+ // returns true if the api key was already detected
+ //
bool doSkip() const override;
private:
+ // connection ui
std::unique_ptr<NexusConnectionUI> m_connectionUI;
+
+ // set to true only if the api key was detected when opening the dialog, or
+ // going back and forth would skip the page after the process is completed,
+ // which would be unexpected
bool m_skip;
};
+// shows a text log of all the creation parameters
+//
class ConfirmationPage : public Page
{
public:
ConfirmationPage(CreateInstanceDialog& dlg);
+ // recreates the log with the latest settings
+ //
void activated() override;
- QString toLocalizedString(CreateInstanceDialog::Types t) const;
+ // returns the text for the log
+ //
QString makeReview() const;
+
+private:
+ // returns a log line with the given caption and path, something like
+ // " - caption: path"
+ //
QString dirLine(const QString& caption, const QString& path) const;
};