#include "config.hpp" #include "luacxx.hpp" #include #include #include namespace getsuyomi { using fud::FudStatus; ShortcutSet shortcutSetFromList(const ShortcutList& shortcutList) { ShortcutSet shortcutSet{}; for (const auto& entry : shortcutList) { shortcutSet.insert(entry); } return shortcutSet; } ShortcutList shortcutListFromSet(const ShortcutSet& shortcutSet) { ShortcutList shortcutList{}; for (const auto& entry : shortcutSet) { shortcutList.push_back(entry); } return shortcutList; } Shortcuts::Shortcuts(ShortcutMap&& shortcutMap) : m_actionToShortcuts{std::move(shortcutMap)}, m_shortcuts{}, m_shortcutToAction{} { for (const auto& [action, shortcuts] : m_actionToShortcuts) { for (const auto& shortcut : shortcuts) { auto bindResult = bind(action, shortcut); if (bindResult != FudStatus::Success) { qWarning("Error: %s", FudStatusToString(bindResult)); return; } } } m_valid = true; } bool Shortcuts::contains(QKeySequence keySequence) const { return m_shortcuts.contains(keySequence); } bool Shortcuts::contains(ActionType actionType) const { return m_actionToShortcuts.contains(actionType); } std::vector Shortcuts::actions() const { std::vector actionList{}; actionList.reserve(m_actionToShortcuts.size()); for (const auto& [action, discarded] : m_actionToShortcuts) { actionList.push_back(action); } return actionList; } std::optional Shortcuts::action(QKeySequence keySequence) const { if (m_shortcutToAction.contains(keySequence)) { return m_shortcutToAction.at(keySequence); } else { return std::nullopt; } } std::optional Shortcuts::shortcuts(ActionType action) const { if (contains(action)) { const auto& shortcuts = m_actionToShortcuts.at(action); ShortcutList shortcutList{}; shortcutList.reserve(static_cast(shortcuts.size())); for (const auto& shortcut : shortcuts) { shortcutList.push_back(shortcut); } return shortcutList; } else { qWarning("Action %s not found", actionTypeToString(action)); return std::nullopt; } } FudStatus Shortcuts::bind(ActionType action, QKeySequence keySequence) { if (contains(keySequence)) { return FudStatus::Full; } m_shortcuts.insert(keySequence); if (!contains(action)) { m_actionToShortcuts[action] = ShortcutSet{}; } m_actionToShortcuts[action].insert(keySequence); m_shortcutToAction[keySequence] = action; return FudStatus::Success; } fud::FudStatus Shortcuts::remove(QKeySequence keySequence) { if (!contains(keySequence)) { return FudStatus::NotFound; } auto action = m_shortcutToAction[keySequence]; auto& actionShortcuts = m_actionToShortcuts[action]; actionShortcuts.erase(keySequence); m_shortcutToAction.erase(keySequence); m_shortcuts.erase(keySequence); return FudStatus::Success; } fud::FudStatus Shortcuts::clear(ActionType action) { if (!contains(action)) { return FudStatus::NotFound; } auto& actionShortcuts = m_actionToShortcuts[action]; for (const auto& shortcut : actionShortcuts) { m_shortcuts.erase(shortcut); m_shortcutToAction.erase(shortcut); } m_actionToShortcuts[action] = {}; return FudStatus::Success; } bool Shortcuts::valid() const { return m_valid; } const ShortcutMap& Shortcuts::shortcutMap() const { return m_actionToShortcuts; } Shortcuts Shortcuts::fromLuaConfig(const std::filesystem::path& configFileName) { Shortcuts shortcuts{}; shortcuts.m_valid = true; LuaContext luaContext{}; if (!luaContext.valid()) { qCritical("Failed to create lua context"); return shortcuts; } auto luaStatus = luaContext.loadFile(configFileName.c_str()); if (luaStatus != FudStatus::Success) { qCritical("Failed to load file in lua %s", fud::FudStatusToString(luaStatus)); return shortcuts; } std::vector actions{ ActionType::OpenFile, ActionType::OpenDirectory, ActionType::Quit, ActionType::Configure, // ActionType::Help, // ActionType::About // ActionType::GotoFirst, ActionType::Next, ActionType::Back, // ActionType::GotoLast, ActionType::SinglePage, ActionType::DualPage, ActionType::MangaPage}; for (auto action : actions) { auto result = luaContext.getGlobalStringArray(actionTypeToString(action)); if (result.isError()) { qWarning( "Failed to get variable %s from lua %s", actionTypeToString(action), fud::FudStatusToString(result.getError())); // return RetType::error(result.getError()); continue; } auto actionBindings = result.getOkay(); for (const auto& shortcut : actionBindings) { QKeySequence keySeq{shortcut.c_str()}; if (keySeq == Qt::Key_unknown) { qCritical("Error: shortcut %s unknown", shortcut.c_str()); continue; } auto bindStatus = shortcuts.bind(action, keySeq); if (bindStatus != FudStatus::Success) { qCritical( "Error: failure to bind to action %s keySeq %s", actionTypeToString(action), shortcut.c_str()); continue; } } } return shortcuts; } ShortcutMap Shortcuts::fromUserConfig(const std::filesystem::path& configFileName) { constexpr mode_t configFileMode = 0600; auto fdResult = open(configFileName.c_str(), O_CREAT, configFileMode); if (fdResult == -1) { if (errno != EEXIST) { qCritical("Could not load or create user config file %s.", configFileName.c_str()); } } close(fdResult); auto shortcuts = Shortcuts::fromLuaConfig(configFileName); auto binder = [&](ActionType action, QKeySequence keySeq) { auto bindDefaultStatus = shortcuts.bind(action, keySeq); if (bindDefaultStatus != FudStatus::Success) { qWarning("%s already bound", actionTypeToString(action)); } }; binder(ActionType::OpenFile, QKeySequence(QKeySequence::Open)); binder(ActionType::OpenDirectory, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_O)); binder(ActionType::Quit, QKeySequence(QKeySequence::Quit)); binder(ActionType::Configure, QKeySequence(Qt::CTRL | Qt::Key_P)); ShortcutList nextDefaults{QKeySequence{Qt::Key_L}, QKeySequence{Qt::Key_Right}, QKeySequence{Qt::Key_Down}}; for (const auto& binding : nextDefaults) { binder(ActionType::Next, binding); } ShortcutList backDefaults{QKeySequence{Qt::Key_H}, QKeySequence{Qt::Key_Left}, QKeySequence{Qt::Key_Up}}; for (const auto& binding : backDefaults) { binder(ActionType::Back, binding); } binder(ActionType::SinglePage, QKeySequence{Qt::Key_S}); binder(ActionType::DualPage, QKeySequence{Qt::Key_D}); binder(ActionType::MangaPage, QKeySequence{Qt::Key_M}); return shortcuts.shortcutMap(); } ShortcutMap GetsuyomiConfig::shortcuts() const { ShortcutMap shortcuts{}; shortcuts.insert({ActionType::OpenFile, shortcutSetFromList(openFileShortcuts)}); shortcuts.insert({ActionType::OpenDirectory, shortcutSetFromList(openDirectoryShortcuts)}); shortcuts.insert({ActionType::Quit, shortcutSetFromList(quitShortcuts)}); shortcuts.insert({ActionType::Configure, shortcutSetFromList(settingsShortcuts)}); shortcuts.insert({ActionType::Next, shortcutSetFromList(nextShortcuts)}); shortcuts.insert({ActionType::Back, shortcutSetFromList(backShortcuts)}); shortcuts.insert({ActionType::SinglePage, shortcutSetFromList(singlePageShortcuts)}); shortcuts.insert({ActionType::DualPage, shortcutSetFromList(dualPageShortcuts)}); shortcuts.insert({ActionType::MangaPage, shortcutSetFromList(mangaPageShortcuts)}); return shortcuts; } ShortcutDisplay::ShortcutDisplay(QWidget* parent, QKeySequence shortcut) : QWidget{parent}, m_binding{shortcut} { auto layout = new QHBoxLayout(); layout->addWidget(new QLabel(shortcut.toString())); auto* deleteButton = new QPushButton("Delete", this); connect(deleteButton, &QPushButton::clicked, this, &ShortcutDisplay::removeOnClicked); layout->addWidget(deleteButton); setLayout(layout); } void ShortcutDisplay::removeOnClicked() { emit removeClicked(m_binding); } ShortcutCollector::ShortcutCollector(QWidget* parent, ActionType action, Shortcuts& shortcuts) : QWidget(parent), m_action{action}, m_shortcuts{shortcuts}, m_bindings{} { m_layout = new QVBoxLayout(); auto headerLayout = new QHBoxLayout(); auto* name = new QLabel(actionTypeToString(m_action)); headerLayout->addWidget(name); m_shortcutEditor = new QKeySequenceEdit(this); m_shortcutEditor->setMaximumSequenceLength(1); connect(m_shortcutEditor, &QKeySequenceEdit::editingFinished, this, &ShortcutCollector::checkBinding); headerLayout->addWidget(m_shortcutEditor); m_acceptButton = new QPushButton("Accept", this); m_acceptButton->setEnabled(false); connect(m_acceptButton, &QPushButton::clicked, this, &ShortcutCollector::addBinding); headerLayout->addWidget(m_acceptButton); headerLayout->addStretch(); m_layout->addLayout(headerLayout); auto shortcutOptions = m_shortcuts.shortcuts(m_action); if (shortcutOptions != std::nullopt) { for (const auto& binding : *shortcutOptions) { createBinding(binding); } } else { qWarning("No shortcuts found for %s", actionTypeToString(action)); } setLayout(m_layout); } void ShortcutCollector::createBinding(QKeySequence binding) { auto displayItem = new ShortcutDisplay(this, binding); m_bindings[binding] = displayItem; m_layout->addWidget(displayItem); connect(displayItem, &ShortcutDisplay::removeClicked, this, &ShortcutCollector::removeBinding); } void ShortcutCollector::checkBinding() { auto keySequence = m_shortcutEditor->keySequence(); if (keySequence == QKeySequence::UnknownKey) { m_acceptButton->setEnabled(false); return; } m_acceptButton->setEnabled(not m_shortcuts.contains(keySequence)); } void ShortcutCollector::addBinding() { auto keySequence = m_shortcutEditor->keySequence(); if (keySequence == QKeySequence::UnknownKey) { qWarning("Invalid state - can't accept unknown key"); return; } else if (m_shortcuts.contains(keySequence)) { qWarning("Shortcut %s already bound", qPrintable(keySequence.toString())); return; } auto result = m_shortcuts.bind(m_action, keySequence); if (result != FudStatus::Success) { qCritical("Error binding %s to action %s", qPrintable(keySequence.toString()), actionTypeToString(m_action)); return; } if (m_bindings.contains(keySequence)) { qWarning("binding %s to action %s already exists", qPrintable(keySequence.toString()), actionTypeToString(m_action)); m_layout->removeWidget(m_bindings[keySequence]); delete m_bindings[keySequence]; } createBinding(keySequence); m_shortcutEditor->clear(); } void ShortcutCollector::removeBinding(QKeySequence binding) { auto result = m_shortcuts.remove(binding); if (result == FudStatus::NotFound) { qWarning("binding %s not found", qPrintable(binding.toString())); } else if (result != FudStatus::Success) { qWarning("error removing binding %s: %s", qPrintable(binding.toString()), FudStatusToString(result)); } auto bindingHandle = m_bindings.extract(binding); if (bindingHandle) { m_layout->removeWidget(bindingHandle.mapped()); delete bindingHandle.mapped(); } else { qWarning("Could not remove widget!"); } checkBinding(); } Settings::Settings(QWidget* parent, Shortcuts&& shortcuts) : QDialog{parent}, m_shortcuts{std::move(shortcuts)} { auto* layout = new QVBoxLayout(); setWindowTitle("getsuyomi settings"); if (!m_shortcuts.valid()) { return; } auto containerLayout = new QHBoxLayout(); auto* columnLayout = new QVBoxLayout(); size_t counter{0}; constexpr size_t maxEntriesPerColumn{4}; for (const auto& action : m_shortcuts.actions()) { auto* collector = new ShortcutCollector(this, action, m_shortcuts); columnLayout->addWidget(collector); counter++; if (counter % (maxEntriesPerColumn + 1) == 0) { containerLayout->addLayout(columnLayout); columnLayout = new QVBoxLayout(); } } if (columnLayout->count() > 0) { containerLayout->addLayout(columnLayout); } layout->addLayout(containerLayout); auto* dialogButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(dialogButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(dialogButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); layout->addWidget(dialogButtonBox); setLayout(layout); } bool Settings::valid() const { return m_shortcuts.valid(); } const Shortcuts& Settings::shortcuts() const { return m_shortcuts; } } // namespace getsuyomi