#include "journalviewmodel.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwdialogue/keywordsearch.hpp" namespace MWGui { struct JournalViewModelImpl; struct JournalViewModelImpl : JournalViewModel { typedef MWDialogue::KeywordSearch KeywordSearchT; mutable bool mKeywordSearchLoaded; mutable KeywordSearchT mKeywordSearch; JournalViewModelImpl() { mKeywordSearchLoaded = false; } virtual ~JournalViewModelImpl() {} /// \todo replace this nasty BS static Utf8Span toUtf8Span(std::string_view str) { if (str.size() == 0) return Utf8Span(Utf8Point(nullptr), Utf8Point(nullptr)); Utf8Point point = reinterpret_cast(str.data()); return Utf8Span(point, point + str.size()); } void load() override {} void unload() override { mKeywordSearch.clear(); mKeywordSearchLoaded = false; } void ensureKeyWordSearchLoaded() const { if (!mKeywordSearchLoaded) { MWBase::Journal* journal = MWBase::Environment::get().getJournal(); for (MWBase::Journal::TTopicIter i = journal->topicBegin(); i != journal->topicEnd(); ++i) mKeywordSearch.seed(i->first.getRefIdString(), intptr_t(&i->second)); mKeywordSearchLoaded = true; } } bool isEmpty() const override { MWBase::Journal* journal = MWBase::Environment::get().getJournal(); return journal->begin() == journal->end(); } template struct BaseEntry : Interface { typedef t_iterator iterator_t; iterator_t itr; JournalViewModelImpl const* mModel; BaseEntry(JournalViewModelImpl const* model, iterator_t itr) : itr(itr) , mModel(model) , loaded(false) { } virtual ~BaseEntry() {} mutable bool loaded; mutable std::string utf8text; typedef std::pair Range; // hyperlinks in @link# notation mutable std::map mHyperLinks; virtual std::string getText() const = 0; void ensureLoaded() const { if (!loaded) { mModel->ensureKeyWordSearchLoaded(); utf8text = getText(); size_t pos_end = 0; for (;;) { size_t pos_begin = utf8text.find('@'); if (pos_begin != std::string::npos) pos_end = utf8text.find('#', pos_begin); if (pos_begin != std::string::npos && pos_end != std::string::npos) { std::string link = utf8text.substr(pos_begin + 1, pos_end - pos_begin - 1); const char specialPseudoAsteriskCharacter = 127; std::replace(link.begin(), link.end(), specialPseudoAsteriskCharacter, '*'); std::string 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(pos_begin, pos_end + 1 - pos_begin, displayName); intptr_t value = 0; if (mModel->mKeywordSearch.containsKeyword(topicName, value)) mHyperLinks[std::make_pair(pos_begin, pos_begin + displayName.size())] = value; } else break; } loaded = true; } } Utf8Span body() const override { ensureLoaded(); return toUtf8Span(utf8text); } void visitSpans(std::function 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 (std::map::const_iterator it = mHyperLinks.begin(); it != mHyperLinks.end(); ++it) { intptr_t topicId = it->second; if (formatted < it->first.first) visitor(0, formatted, it->first.first); visitor(topicId, it->first.first, it->first.second); formatted = it->first.second; } if (formatted < utf8text.size()) visitor(0, formatted, utf8text.size()); } else { std::vector matches; mModel->mKeywordSearch.highlightKeywords(utf8text.begin(), utf8text.end(), matches); std::string::const_iterator i = utf8text.begin(); for (std::vector::const_iterator it = matches.begin(); it != matches.end(); ++it) { const KeywordSearchT::Match& match = *it; 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 active_only, std::function visitor) const override { MWBase::Journal* journal = MWBase::Environment::get().getJournal(); std::set> 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 (MWBase::Journal::TQuestIter i = journal->questBegin(); i != journal->questEnd(); ++i) { const MWDialogue::Quest& quest = i->second; bool isFinished = false; for (MWBase::Journal::TQuestIter j = journal->questBegin(); j != journal->questEnd(); ++j) { if (quest.getName() == j->second.getName() && j->second.isFinished()) isFinished = true; } if (active_only && isFinished) continue; // 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. if (!quest.getName().empty()) { // Don't list the same quest name twice if (visitedQuests.find(quest.getName()) != visitedQuests.end()) continue; visitor(quest.getName(), isFinished); visitedQuests.emplace(quest.getName()); } } } template struct JournalEntryImpl : BaseEntry { using BaseEntry::itr; mutable std::string timestamp_buffer; JournalEntryImpl(JournalViewModelImpl const* model, iterator_t itr) : BaseEntry(model, itr) { } std::string getText() const override { return itr->getText(); } Utf8Span timestamp() const override { if (timestamp_buffer.empty()) { std::string dayStr = MyGUI::LanguageManager::getInstance().replaceTags("#{sDay}"); std::ostringstream os; os << itr->mDayOfMonth << ' ' << MWBase::Environment::get().getWorld()->getMonthName(itr->mMonth) << " (" << dayStr << " " << (itr->mDay) << ')'; timestamp_buffer = os.str(); } return toUtf8Span(timestamp_buffer); } }; void visitJournalEntries( std::string_view questName, std::function visitor) const override { MWBase::Journal* journal = MWBase::Environment::get().getJournal(); if (!questName.empty()) { std::vector quests; for (MWBase::Journal::TQuestIter questIt = journal->questBegin(); questIt != journal->questEnd(); ++questIt) { if (Misc::StringUtils::ciEqual(questIt->second.getName(), questName)) quests.push_back(&questIt->second); } for (MWBase::Journal::TEntryIter i = journal->begin(); i != journal->end(); ++i) { for (std::vector::iterator questIt = quests.begin(); questIt != quests.end(); ++questIt) { MWDialogue::Quest const* quest = *questIt; for (MWDialogue::Topic::TEntryIter j = quest->begin(); j != quest->end(); ++j) { if (i->mInfoId == j->mInfoId) visitor(JournalEntryImpl(this, i)); } } } } else { for (MWBase::Journal::TEntryIter i = journal->begin(); i != journal->end(); ++i) visitor(JournalEntryImpl(this, i)); } } void visitTopicName(TopicId topicId, std::function visitor) const override { MWDialogue::Topic const& topic = *reinterpret_cast(topicId); visitor(toUtf8Span(topic.getName())); } void visitTopicNamesStartingWith( Utf8Stream::UnicodeChar character, std::function visitor) const override { MWBase::Journal* journal = MWBase::Environment::get().getJournal(); for (MWBase::Journal::TTopicIter i = journal->topicBegin(); i != journal->topicEnd(); ++i) { Utf8Stream stream(i->first.getRefIdString().c_str()); Utf8Stream::UnicodeChar first = Utf8Stream::toLowerUtf8(stream.peek()); if (first != Utf8Stream::toLowerUtf8(character)) continue; visitor(i->second.getName()); } } struct TopicEntryImpl : BaseEntry { MWDialogue::Topic const& mTopic; TopicEntryImpl(JournalViewModelImpl const* model, MWDialogue::Topic const& topic, iterator_t itr) : BaseEntry(model, itr) , mTopic(topic) { } std::string getText() const override { return itr->getText(); } Utf8Span source() const override { return toUtf8Span(itr->mActorName); } }; void visitTopicEntries(TopicId topicId, std::function visitor) const override { typedef MWDialogue::Topic::TEntryIter iterator_t; MWDialogue::Topic const& topic = *reinterpret_cast(topicId); for (iterator_t i = topic.begin(); i != topic.end(); ++i) visitor(TopicEntryImpl(this, topic, i)); } }; JournalViewModel::Ptr JournalViewModel::create() { return std::make_shared(); } }