1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2026-01-15 01:30:53 +00:00
openmw/apps/openmw/mwgui/journalviewmodel.cpp
2025-09-22 20:59:13 +02:00

328 lines
12 KiB
C++

#include "journalviewmodel.hpp"
#include <map>
#include <MyGUI_LanguageManager.h>
#include <components/misc/strings/algorithm.hpp>
#include <components/translation/translation.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/journal.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwdialogue/keywordsearch.hpp"
#include "../mwworld/datetimemanager.hpp"
namespace MWGui
{
struct JournalViewModelImpl;
struct JournalViewModelImpl : JournalViewModel
{
using TopicSearch = MWDialogue::KeywordSearch<const MWDialogue::Topic*>;
mutable bool mKeywordSearchLoaded;
mutable TopicSearch mKeywordSearch;
JournalViewModelImpl() { mKeywordSearchLoaded = false; }
virtual ~JournalViewModelImpl() = default;
void load() override {}
void unload() override
{
mKeywordSearch.clear();
mKeywordSearchLoaded = false;
}
void ensureKeyWordSearchLoaded() const
{
if (!mKeywordSearchLoaded)
{
MWBase::Journal* journal = MWBase::Environment::get().getJournal();
for (const auto& [_, topic] : journal->getTopics())
mKeywordSearch.seed(topic.getName(), &topic);
mKeywordSearchLoaded = true;
}
}
bool isEmpty() const override
{
MWBase::Journal* journal = MWBase::Environment::get().getJournal();
return journal->getEntries().empty();
}
template <typename EntryType, typename Interface>
struct BaseEntry : Interface
{
const EntryType* mEntry;
JournalViewModelImpl const* mModel;
BaseEntry(JournalViewModelImpl const* model, const EntryType& entry)
: mEntry(&entry)
, mModel(model)
, loaded(false)
{
}
virtual ~BaseEntry() = default;
mutable bool loaded;
mutable std::string utf8text;
// hyperlinks in @link# notation
mutable std::map<std::pair<size_t, size_t>, const MWDialogue::Topic*> mHyperLinks;
virtual std::string getText() const = 0;
void ensureLoaded() const
{
if (!loaded)
{
mModel->ensureKeyWordSearchLoaded();
utf8text = getText();
size_t posEnd = 0;
for (;;)
{
const size_t posBegin = utf8text.find('@');
if (posBegin != std::string::npos)
posEnd = utf8text.find('#', posBegin);
if (posBegin != std::string::npos && posEnd != std::string::npos)
{
std::string link = utf8text.substr(posBegin + 1, posEnd - posBegin - 1);
const char specialPseudoAsteriskCharacter = 127;
std::replace(link.begin(), link.end(), specialPseudoAsteriskCharacter, '*');
std::string_view topicName = MWBase::Environment::get()
.getWindowManager()
->getTranslationDataStorage()
.topicStandardForm(link);
std::string displayName = link;
while (displayName[displayName.size() - 1] == '*')
displayName.erase(displayName.size() - 1, 1);
utf8text.replace(posBegin, posEnd + 1 - posBegin, displayName);
const MWDialogue::Topic* value = nullptr;
if (mModel->mKeywordSearch.containsKeyword(topicName, value))
mHyperLinks[std::make_pair(posBegin, posBegin + displayName.size())] = value;
}
else
break;
}
loaded = true;
}
}
std::string_view body() const override
{
ensureLoaded();
return utf8text;
}
void visitSpans(std::function<void(const MWDialogue::Topic*, size_t, size_t)> visitor) const override
{
ensureLoaded();
mModel->ensureKeyWordSearchLoaded();
if (mHyperLinks.size()
&& MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation())
{
size_t formatted = 0; // points to the first character that is not laid out yet
for (const auto& [range, topicId] : mHyperLinks)
{
if (formatted < range.first)
visitor(0, formatted, range.first);
visitor(topicId, range.first, range.second);
formatted = range.second;
}
if (formatted < utf8text.size())
visitor(0, formatted, utf8text.size());
}
else
{
std::vector<TopicSearch::Match> matches;
mModel->mKeywordSearch.highlightKeywords(utf8text.begin(), utf8text.end(), matches);
std::string::const_iterator i = utf8text.begin();
for (const TopicSearch::Match& match : matches)
{
if (i != match.mBeg)
visitor(0, i - utf8text.begin(), match.mBeg - utf8text.begin());
visitor(match.mValue, match.mBeg - utf8text.begin(), match.mEnd - utf8text.begin());
i = match.mEnd;
}
if (i != utf8text.end())
visitor(0, i - utf8text.begin(), utf8text.size());
}
}
};
void visitQuestNames(bool activeOnly, std::function<void(std::string_view, bool)> visitor) const override
{
MWBase::Journal* journal = MWBase::Environment::get().getJournal();
std::set<std::string_view, Misc::StringUtils::CiComp> visitedQuests;
// Note that for purposes of the journal GUI, quests are identified by the name, not the ID, so several
// different quest IDs can end up in the same quest log. A quest log should be considered finished
// when any quest ID in that log is finished.
for (const auto& [_, quest] : journal->getQuests())
{
// Unfortunately Morrowind.esm has no quest names, since the quest book was added with tribunal.
// Note that even with Tribunal, some quests still don't have quest names. I'm assuming those are not
// supposed to appear in the quest book.
const std::string_view questName = quest.getName();
if (questName.empty())
continue;
// Don't list the same quest name twice
if (!visitedQuests.insert(questName).second)
continue;
bool isFinished = std::ranges::find_if(journal->getQuests(), [&](const auto& pair) {
return pair.second.isFinished() && Misc::StringUtils::ciEqual(questName, pair.second.getName());
}) != journal->getQuests().end();
if (activeOnly && isFinished)
continue;
visitor(questName, isFinished);
}
}
struct JournalEntryImpl : BaseEntry<MWDialogue::StampedJournalEntry, JournalEntry>
{
mutable std::string timestamp_buffer;
JournalEntryImpl(JournalViewModelImpl const* model, const MWDialogue::StampedJournalEntry& entry)
: BaseEntry(model, entry)
{
}
std::string getText() const override { return mEntry->getText(); }
std::string_view timestamp() const override
{
if (timestamp_buffer.empty())
{
std::string dayStr = MyGUI::LanguageManager::getInstance().replaceTags("#{sDay}");
std::ostringstream os;
os << mEntry->mDayOfMonth << ' '
<< MWBase::Environment::get().getWorld()->getTimeManager()->getMonthName(mEntry->mMonth) << " ("
<< dayStr << " " << (mEntry->mDay) << ')';
timestamp_buffer = os.str();
}
return timestamp_buffer;
}
};
void visitJournalEntries(
std::string_view questName, std::function<void(JournalEntry const&)> visitor) const override
{
MWBase::Journal* journal = MWBase::Environment::get().getJournal();
if (!questName.empty())
{
std::vector<MWDialogue::Quest const*> quests;
for (const auto& [_, quest] : journal->getQuests())
{
if (Misc::StringUtils::ciEqual(quest.getName(), questName))
quests.push_back(&quest);
}
for (const MWDialogue::StampedJournalEntry& journalEntry : journal->getEntries())
{
for (const MWDialogue::Quest* quest : quests)
{
if (quest->getTopic() != journalEntry.mTopic)
continue;
for (const MWDialogue::Entry& questEntry : *quest)
{
if (journalEntry.mInfoId == questEntry.mInfoId)
{
visitor(JournalEntryImpl(this, journalEntry));
break;
}
}
}
}
}
else
{
for (const MWDialogue::StampedJournalEntry& journalEntry : journal->getEntries())
visitor(JournalEntryImpl(this, journalEntry));
}
}
void visitTopicName(
const MWDialogue::Topic& topic, std::function<void(std::string_view)> visitor) const override
{
visitor(topic.getName());
}
void visitTopicNamesStartingWith(
Utf8Stream::UnicodeChar character, std::function<void(std::string_view)> visitor) const override
{
MWBase::Journal* journal = MWBase::Environment::get().getJournal();
for (const auto& [_, topic] : journal->getTopics())
{
Utf8Stream stream(topic.getName());
Utf8Stream::UnicodeChar first = Utf8Stream::toLowerUtf8(stream.peek());
if (first != Utf8Stream::toLowerUtf8(character))
continue;
visitor(topic.getName());
}
}
struct TopicEntryImpl : BaseEntry<MWDialogue::Entry, TopicEntry>
{
MWDialogue::Topic const& mTopic;
TopicEntryImpl(
JournalViewModelImpl const* model, MWDialogue::Topic const& topic, const MWDialogue::Entry& entry)
: BaseEntry(model, entry)
, mTopic(topic)
{
}
std::string getText() const override { return mEntry->getText(); }
std::string_view source() const override { return mEntry->mActorName; }
};
void visitTopicEntries(
const MWDialogue::Topic& topic, std::function<void(TopicEntry const&)> visitor) const override
{
for (const MWDialogue::Entry& entry : topic)
visitor(TopicEntryImpl(this, topic, entry));
}
};
std::shared_ptr<JournalViewModel> JournalViewModel::create()
{
return std::make_shared<JournalViewModelImpl>();
}
}