#include "viewarea.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "viewwindow.h" #include "mainwindow.h" #include "propertydefs.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "editors/plantumlhelper.h" #include "editors/graphvizhelper.h" #include using namespace vnotex; ViewArea::ViewArea(QWidget *p_parent) : QWidget(p_parent), NavigationMode(NavigationMode::Type::DoubleKeys, this) { setupUI(); setAcceptDrops(true); setupShortcuts(); connect(this, &ViewArea::viewSplitsCountChanged, this, &ViewArea::handleViewSplitsCountChange); auto mainWindow = VNoteX::getInst().getMainWindow(); connect(mainWindow, &MainWindow::mainWindowClosed, this, [this](const QSharedPointer &p_event) { if (p_event->m_handled) { return; } if (ConfigMgr::getInst().getCoreConfig().isRecoverLastSessionOnStartEnabled()) { saveSession(); } bool ret = close(false); if (!ret) { p_event->m_response = false; p_event->m_handled = true; } }); connect(mainWindow, &MainWindow::mainWindowClosedOnQuit, this, [this]() { close(true); }); connect(mainWindow, &MainWindow::mainWindowStarted, this, &ViewArea::loadSession); connect(&VNoteX::getInst(), &VNoteX::nodeAboutToMove, this, &ViewArea::handleNodeChange); connect(&VNoteX::getInst(), &VNoteX::nodeAboutToRemove, this, &ViewArea::handleNodeChange); connect(&VNoteX::getInst(), &VNoteX::nodeAboutToRename, this, &ViewArea::handleNodeChange); connect(&VNoteX::getInst(), &VNoteX::nodeAboutToReload, this, &ViewArea::handleNodeChange); connect(&VNoteX::getInst(), &VNoteX::closeFileRequested, this, &ViewArea::closeFile); auto &configMgr = ConfigMgr::getInst(); connect(&configMgr, &ConfigMgr::editorConfigChanged, this, [this]() { forEachViewWindow([](ViewWindow *p_win) { p_win->handleEditorConfigChange(); return true; }); const auto &markdownEditorConfig = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig(); PlantUmlHelper::getInst().update(markdownEditorConfig.getPlantUmlJar(), markdownEditorConfig.getGraphvizExe(), markdownEditorConfig.getPlantUmlCommand()); GraphvizHelper::getInst().update(markdownEditorConfig.getGraphvizExe()); }); m_fileCheckTimer = new QTimer(this); m_fileCheckTimer->setSingleShot(false); m_fileCheckTimer->setInterval(2000); connect(m_fileCheckTimer, &QTimer::timeout, this, [this]() { auto win = getCurrentViewWindow(); if (win) { win->checkFileMissingOrChangedOutsidePeriodically(); } }); connect(qApp, &QApplication::focusChanged, this, [this](QWidget *p_old, QWidget *p_now) { if (!p_now) { m_fileCheckTimer->stop(); } else if (!p_old && m_currentSplit) { m_fileCheckTimer->start(); } }); } ViewArea::~ViewArea() { // All splits/workspaces/windows should be released during close() before destruction. Q_ASSERT(m_splits.isEmpty() && m_currentSplit == nullptr); Q_ASSERT(m_workspaces.isEmpty()); } void ViewArea::handleNodeChange(Node *p_node, const QSharedPointer &p_event) { if (p_event->m_handled) { return; } bool ret = close(p_node, false); p_event->m_response = ret; p_event->m_handled = !ret; } void ViewArea::setupUI() { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_mainLayout = new QVBoxLayout(this); m_mainLayout->setContentsMargins(0, 0, 0, 0); } QSize ViewArea::sizeHint() const { const QSize preferredSize(400, 300); auto sz = QWidget::sizeHint(); if (sz.width() < preferredSize.width()) { sz = preferredSize; } return sz; } void ViewArea::openBuffer(Buffer *p_buffer, const QSharedPointer &p_paras) { // We allow multiple ViewWindows of the same buffer in different workspaces by default. QVector wins; if (!p_paras->m_alwaysNewWindow) { wins = findBufferInViewSplits(p_buffer); } if (wins.isEmpty()) { if (!m_currentSplit) { addFirstViewSplit(); } Q_ASSERT(m_currentSplit); // Create a ViewWindow from @p_buffer. auto window = p_buffer->createViewWindow(p_paras, nullptr); m_currentSplit->addViewWindow(window); setCurrentViewWindow(window); } else { auto selectedWin = wins.first(); for (auto win : wins) { // Prefer window in current split. if (win->getViewSplit() == m_currentSplit) { selectedWin = win; break; } } selectedWin->openTwice(p_paras); setCurrentViewWindow(selectedWin); } if (p_paras->m_focus) { auto win = getCurrentViewWindow(); if (win) { win->setFocus(Qt::OtherFocusReason); } } } QVector ViewArea::findBufferInViewSplits(const Buffer *p_buffer) const { QVector wins; for (auto split : m_splits) { auto winsInSplit = split->findBuffer(p_buffer); if (!winsInSplit.isEmpty()) { wins.append(winsInSplit); } } return wins; } ViewSplit *ViewArea::createViewSplit(QWidget *p_parent, ID p_viewSplitId) { auto workspace = createWorkspace(); m_workspaces.push_back(workspace); if (p_viewSplitId == InvalidViewSplitId) { p_viewSplitId = m_nextViewSplitId++; } else if (p_viewSplitId >= m_nextViewSplitId) { m_nextViewSplitId = p_viewSplitId + 1; } auto split = new ViewSplit(m_workspaces, workspace, p_viewSplitId, p_parent); connect(split, &ViewSplit::viewWindowCloseRequested, this, [this](ViewWindow *p_win) { closeViewWindow(p_win, false, true); }); connect(split, &ViewSplit::verticalSplitRequested, this, [this](ViewSplit *p_split) { splitViewSplit(p_split, SplitType::Vertical); }); connect(split, &ViewSplit::horizontalSplitRequested, this, [this](ViewSplit *p_split) { splitViewSplit(p_split, SplitType::Horizontal); }); connect(split, &ViewSplit::maximizeSplitRequested, this, &ViewArea::maximizeViewSplit); connect(split, &ViewSplit::distributeSplitsRequested, this, &ViewArea::distributeViewSplits); connect(split, &ViewSplit::removeSplitRequested, this, [this](ViewSplit *p_split) { removeViewSplit(p_split, false); }); connect(split, &ViewSplit::removeSplitAndWorkspaceRequested, this, [this](ViewSplit *p_split) { removeViewSplit(p_split, true); }); connect(split, &ViewSplit::newWorkspaceRequested, this, &ViewArea::newWorkspaceInViewSplit); connect(split, &ViewSplit::removeWorkspaceRequested, this, [this](ViewSplit *p_split) { removeWorkspaceInViewSplit(p_split, true); }); connect(split, &ViewSplit::focused, this, [this](ViewSplit *p_split) { setCurrentViewSplit(p_split, false); checkCurrentViewWindowChange(); }); connect(split, &ViewSplit::currentViewWindowChanged, this, [this](ViewWindow *p_win) { checkCurrentViewWindowChange(); if (shouldUseGlobalStatusWidget()) { if (p_win) { p_win->setStatusWidgetVisible(false); } m_currentStatusWidget = p_win ? p_win->statusWidget() : nullptr; emit statusWidgetChanged(m_currentStatusWidget.get()); } else { Q_ASSERT(!m_currentStatusWidget); if (p_win) { p_win->setStatusWidgetVisible(true); } } }); connect(split, &ViewSplit::moveViewWindowOneSplitRequested, this, &ViewArea::moveViewWindowOneSplit); return split; } void ViewArea::showSceneWidget() { Q_ASSERT(!m_sceneWidget); Q_ASSERT(m_splits.isEmpty()); auto text = DocsUtils::getDocText(QStringLiteral("get_started.txt")); // TODO: a more informative widget, such as adding workspace list and LRU files. m_sceneWidget = new QLabel(text, this); m_mainLayout->addWidget(m_sceneWidget); } void ViewArea::hideSceneWidget() { Q_ASSERT(m_sceneWidget); m_mainLayout->removeWidget(m_sceneWidget); delete m_sceneWidget; m_sceneWidget = nullptr; } void ViewArea::addFirstViewSplit() { Q_ASSERT(!m_currentSplit && m_splits.isEmpty()); auto split = createViewSplit(this); m_splits.push_back(split); hideSceneWidget(); m_mainLayout->addWidget(split); postFirstViewSplit(); } void ViewArea::postFirstViewSplit() { Q_ASSERT(!m_splits.isEmpty()); auto currentSplit = m_splits.first(); // Check if any split has focus. If there is any, then set it as current split. auto focusWidget = QApplication::focusWidget(); if (focusWidget) { for (const auto &split : m_splits) { if (split == focusWidget || split->isAncestorOf(focusWidget)) { currentSplit = split; break; } } } setCurrentViewSplit(currentSplit, false); emit viewSplitsCountChanged(); checkCurrentViewWindowChange(); m_fileCheckTimer->start(); } static ViewSplit *fetchFirstChildViewSplit(const QSplitter *p_splitter) { if (p_splitter->count() == 0) { return nullptr; } auto child = p_splitter->widget(0); auto split = dynamic_cast(child); if (split) { return split; } auto childSplitter = dynamic_cast(child); Q_ASSERT(childSplitter); return fetchFirstChildViewSplit(childSplitter); } void ViewArea::removeViewSplit(ViewSplit *p_split, bool p_removeWorkspace) { if (p_removeWorkspace) { // Remove workspace. bool ret = removeWorkspaceInViewSplit(p_split, false); if (!ret) { return; } } else { // Detach workspace. p_split->setWorkspace(nullptr); } // Remove split. disconnect(p_split, 0, this, 0); disconnect(this, 0, p_split, 0); m_splits.removeAll(p_split); // Get new current split. ViewSplit *newCurrentSplit = nullptr; auto splitter = tryGetParentSplitter(p_split); if (splitter) { Q_ASSERT(splitter->count() >= 2); p_split->hide(); p_split->setParent(this); newCurrentSplit = fetchFirstChildViewSplit(splitter); if (splitter->count() == 1) { // Remove the splitter if there is only one child in it after the removal. unwrapSplitter(splitter); } } else { m_mainLayout->removeWidget(p_split); if (!m_splits.isEmpty()) { newCurrentSplit = m_splits.first(); } } p_split->deleteLater(); // Show scene widget and update current split. if (m_splits.isEmpty()) { Q_ASSERT(newCurrentSplit == nullptr); setCurrentViewSplit(newCurrentSplit, false); showSceneWidget(); m_fileCheckTimer->stop(); } else if (m_currentSplit == p_split) { setCurrentViewSplit(newCurrentSplit, true); } emit viewSplitsCountChanged(); checkCurrentViewWindowChange(); } ViewWindow *ViewArea::getCurrentViewWindow() const { auto split = getCurrentViewSplit(); if (split) { return split->getCurrentViewWindow(); } return nullptr; } void ViewArea::setCurrentViewWindow(ViewWindow *p_win) { auto split = p_win->getViewSplit(); Q_ASSERT(split); split->setCurrentViewWindow(p_win); setCurrentViewSplit(split, false); checkCurrentViewWindowChange(); } ViewSplit *ViewArea::getCurrentViewSplit() const { return m_currentSplit; } void ViewArea::setCurrentViewSplit(ViewSplit *p_split, bool p_focus) { Q_ASSERT(!p_split || m_splits.contains(p_split)); if (p_split == m_currentSplit) { if (p_split && p_focus) { p_split->focus(); } return; } if (m_currentSplit) { m_currentSplit->setActive(false); } m_currentSplit = p_split; if (m_currentSplit) { m_currentSplit->setActive(true); if (p_focus) { m_currentSplit->focus(); } } } bool ViewArea::closeViewWindow(ViewWindow *p_win, bool p_force, bool p_removeSplitIfEmpty) { Q_ASSERT(p_win && p_win->getViewSplit()); // Make it current ViewWindow. setCurrentViewWindow(p_win); // Get info before close. const auto session = p_win->saveSession(); Notebook *notebook = nullptr; if (p_win->getBuffer()) { auto node = p_win->getBuffer()->getNode(); if (node) { notebook = node->getNotebook(); } } if (!p_win->aboutToClose(p_force)) { return false; } // Update history. updateHistory(session, notebook); // Remove the status widget. if (m_currentStatusWidget && p_win == getCurrentViewWindow()) { Q_ASSERT(m_currentStatusWidget == p_win->statusWidget()); emit statusWidgetChanged(nullptr); } auto split = p_win->getViewSplit(); split->takeViewWindow(p_win); delete p_win; if (p_removeSplitIfEmpty && split->getViewWindowCount() == 0) { // Remove this split and workspace. removeViewSplit(split, true); } return true; } QSharedPointer ViewArea::createWorkspace() { // Get the id of the workspace. ID id = 1; QSet usedIds; for (auto ws : m_workspaces) { usedIds.insert(ws->m_id); } while (true) { if (usedIds.contains(id)) { ++id; } else { break; } } return QSharedPointer::create(id); } ViewSplit *ViewArea::splitViewSplit(ViewSplit *p_split, SplitType p_type, bool p_cloneViewWindow) { Q_ASSERT(p_split); // Create the new split. auto newSplit = createViewSplit(this); // Clone a ViewWindow for the same buffer to display in the new split. if (p_cloneViewWindow) { auto win = p_split->getCurrentViewWindow(); if (win) { auto buffer = win->getBuffer(); auto newWindow = buffer->createViewWindow(QSharedPointer::create(), newSplit); newSplit->addViewWindow(newWindow); } } // Obey Vim's practice, which is the opposite of Qt. auto orientation = p_type == SplitType::Vertical ? Qt::Horizontal : Qt::Vertical; auto splitter = tryGetParentSplitter(p_split); if (splitter) { int idx = splitter->indexOf(p_split); if (splitter->orientation() == orientation) { // Same orientation. splitter->insertWidget(idx + 1, newSplit); } else { // Split it further. auto newSplitter = createSplitter(orientation, this); splitter->replaceWidget(idx, newSplitter); newSplitter->addWidget(p_split); newSplitter->addWidget(newSplit); } } else { Q_ASSERT(p_split->parent() == this); m_mainLayout->removeWidget(p_split); auto newSplitter = createSplitter(orientation, this); newSplitter->addWidget(p_split); newSplitter->addWidget(newSplit); m_mainLayout->addWidget(newSplitter); } m_splits.push_back(newSplit); setCurrentViewSplit(newSplit, true); // Let Qt decide the size of splitter first. QCoreApplication::sendPostedEvents(); distributeViewSplitsOfSplitter(tryGetParentSplitter(newSplit)); emit viewSplitsCountChanged(); checkCurrentViewWindowChange(); return newSplit; } QSplitter *ViewArea::createSplitter(Qt::Orientation p_orientation, QWidget *p_parent) const { auto splitter = new QSplitter(p_orientation, p_parent); splitter->setChildrenCollapsible(false); return splitter; } QSplitter *ViewArea::tryGetParentSplitter(const QWidget *p_widget) const { return dynamic_cast(p_widget->parent()); } void ViewArea::distributeViewSplitsOfSplitter(QSplitter *p_splitter) { if (!p_splitter || p_splitter->count() <= 1) { return; } // Distribute the direct children of splitter. { auto sizes = p_splitter->sizes(); int totalWidth = 0; for (auto sz : sizes) { totalWidth += sz; } int newWidth = totalWidth / sizes.size(); if (newWidth <= 0) { return; } for (int i = 0; i < sizes.size(); ++i) { sizes[i] = newWidth; } p_splitter->setSizes(sizes); } // Distribute child splitter. for (int i = 0; i < p_splitter->count(); ++i) { auto childSplitter = dynamic_cast(p_splitter->widget(i)); if (childSplitter) { distributeViewSplitsOfSplitter(childSplitter); } } return; } void ViewArea::unwrapSplitter(QSplitter *p_splitter) { Q_ASSERT(p_splitter->count() == 1); auto paSplitter = tryGetParentSplitter(p_splitter); if (paSplitter) { Q_ASSERT(paSplitter->count() >= 2); int idx = paSplitter->indexOf(p_splitter); auto child = p_splitter->widget(0); child->setParent(this); paSplitter->replaceWidget(idx, child); } else { // This is the top child of ViewArea. Q_ASSERT(p_splitter->parent() == this); m_mainLayout->removeWidget(p_splitter); // Maybe another splitter or ViewSplit. auto child = p_splitter->widget(0); child->setParent(this); m_mainLayout->addWidget(child); } delete p_splitter; } void ViewArea::maximizeViewSplit(ViewSplit *p_split) { QWidget *widget = p_split; while (widget && widget != this) { maximizeWidgetOfSplitter(widget); widget = dynamic_cast(widget->parent()); } } void ViewArea::maximizeWidgetOfSplitter(QWidget *p_widget) { auto splitter = tryGetParentSplitter(p_widget); if (!splitter || splitter->count() <= 1) { return; } const int minSplitWidth = 20 * WidgetUtils::calculateScaleFactor(); auto sizes = splitter->sizes(); int totalWidth = 0; for (auto sz : sizes) { totalWidth += sz; } int newWidth = totalWidth - minSplitWidth * (sizes.size() - 1); if (newWidth <= 0) { return; } int idx = splitter->indexOf(p_widget); for (int i = 0; i < sizes.size(); ++i) { sizes[i] = (i == idx) ? newWidth : minSplitWidth; } splitter->setSizes(sizes); } void ViewArea::distributeViewSplits() { // Get the top splitter if there is any. auto splitter = dynamic_cast(m_mainLayout->itemAt(0)->widget()); if (!splitter) { return; } distributeViewSplitsOfSplitter(splitter); } void ViewArea::removeWorkspace(QSharedPointer p_workspace) { if (!p_workspace) { return; } Q_ASSERT(!p_workspace->m_visible && p_workspace->m_viewWindows.isEmpty()); p_workspace->clear(); m_workspaces.removeAll(p_workspace); } void ViewArea::newWorkspaceInViewSplit(ViewSplit *p_split) { auto workspace = createWorkspace(); m_workspaces.push_back(workspace); p_split->setWorkspace(workspace); setCurrentViewSplit(p_split, true); } bool ViewArea::removeWorkspaceInViewSplit(ViewSplit *p_split, bool p_insertNew) { // Close all the ViewWindows. setCurrentViewSplit(p_split, true); auto wins = getAllViewWindows(p_split); for (const auto win : wins) { if (!closeViewWindow(win, false, false)) { return false; } } Q_ASSERT(p_split->getViewWindowCount() == 0); auto workspace = p_split->getWorkspace(); p_split->setWorkspace(nullptr); removeWorkspace(workspace); if (p_insertNew) { // Find an invisible workspace. bool found = false; for (auto &ws : m_workspaces) { if (!ws->m_visible) { p_split->setWorkspace(ws); found = true; break; } } // No invisible workspace. Create a new empty workspace. if (!found) { newWorkspaceInViewSplit(p_split); } } return true; } bool ViewArea::shouldUseGlobalStatusWidget() const { return m_splits.size() <= 1; } void ViewArea::handleViewSplitsCountChange() { if (shouldUseGlobalStatusWidget()) { // Hide the status widget for all ViewWindows. forEachViewWindow([](ViewWindow *p_win) { p_win->setStatusWidgetVisible(false); return true; }); // Show global status widget for current ViewWindow. auto win = getCurrentViewWindow(); m_currentStatusWidget = win ? win->statusWidget() : nullptr; emit statusWidgetChanged(m_currentStatusWidget.get()); } else { // Show standalone status widget for all ViewWindows. emit statusWidgetChanged(nullptr); m_currentStatusWidget = nullptr; forEachViewWindow([](ViewWindow *p_win) { p_win->setStatusWidgetVisible(true); return true; }); } } void ViewArea::forEachViewWindow(const ViewSplit::ViewWindowSelector &p_func) { for (auto split : m_splits) { if (!split->forEachViewWindow(p_func)) { return; } } for (auto &ws : m_workspaces) { if (!ws->m_visible) { for (auto win : ws->m_viewWindows) { if (!p_func(win)) { return; } } } } } bool ViewArea::close(bool p_force) { return closeIf(p_force, [](ViewWindow *p_win) { Q_UNUSED(p_win); return true; }, true); } void ViewArea::setupShortcuts() { const auto &coreConfig = ConfigMgr::getInst().getCoreConfig(); // CloseTab. { auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::CloseTab), this); if (shortcut) { connect(shortcut, &QShortcut::activated, this, [this]() { auto win = getCurrentViewWindow(); if (win) { closeViewWindow(win, false, true); } }); } } // LocateNode. { auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::LocateNode), this); if (shortcut) { connect(shortcut, &QShortcut::activated, this, [this]() { auto win = getCurrentViewWindow(); if (win) { auto node = win->getBuffer()->getNode(); if (node) { emit VNoteX::getInst().locateNodeRequested(node); } } }); } } // FocusContentArea. { auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::FocusContentArea), this); if (shortcut) { connect(shortcut, &QShortcut::activated, this, [this]() { auto win = getCurrentViewWindow(); if (win) { win->setFocus(); } else { setFocus(); } }); } } // OneSplitLeft. { auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::OneSplitLeft), this); if (shortcut) { connect(shortcut, &QShortcut::activated, this, [this]() { focusSplitByDirection(Direction::Left); }); } } // OneSplitDown. { auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::OneSplitDown), this); if (shortcut) { connect(shortcut, &QShortcut::activated, this, [this]() { focusSplitByDirection(Direction::Down); }); } } // OneSplitUp. { auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::OneSplitUp), this); if (shortcut) { connect(shortcut, &QShortcut::activated, this, [this]() { focusSplitByDirection(Direction::Up); }); } } // OneSplitRight. { auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::OneSplitRight), this); if (shortcut) { connect(shortcut, &QShortcut::activated, this, [this]() { focusSplitByDirection(Direction::Right); }); } } // OpenLastClosedFile. { auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::OpenLastClosedFile), this); if (shortcut) { connect(shortcut, &QShortcut::activated, this, [this]() { auto file = HistoryMgr::getInst().popLastClosedFile(); if (file.m_path.isEmpty()) { VNoteX::getInst().showStatusMessageShort(tr("No recently closed file")); return; } auto paras = QSharedPointer::create(); paras->m_lineNumber = file.m_lineNumber; paras->m_mode = file.m_mode; paras->m_readOnly = file.m_readOnly; emit VNoteX::getInst().openFileRequested(file.m_path, paras); }); } } } bool ViewArea::close(Node *p_node, bool p_force) { return closeIf(p_force, [p_node](ViewWindow *p_win) { auto buffer = p_win->getBuffer(); return buffer->match(p_node) || buffer->isChildOf(p_node); }, true); } bool ViewArea::close(const Notebook *p_notebook, bool p_force) { return close(p_notebook->getRootNode().data(), p_force); } void ViewArea::checkCurrentViewWindowChange() { auto win = getCurrentViewWindow(); if (win == m_currentWindow) { return; } m_currentWindow = win; emit currentViewWindowChanged(); } bool ViewArea::closeIf(bool p_force, const ViewSplit::ViewWindowSelector &p_func, bool p_closeEmptySplit) { // Go through all hidden workspace. Use current split to show the workspace. if (m_workspaces.size() > m_splits.size()) { if (!m_currentSplit) { // Create at least one split. addFirstViewSplit(); } // Need to restore it. auto currentWorkspace = m_currentSplit->getWorkspace(); QVector> hiddenWorkspaces; for (auto &ws : m_workspaces) { if (!ws->m_visible) { Q_ASSERT(ws != currentWorkspace); hiddenWorkspaces.push_back(ws); } } Q_ASSERT(!hiddenWorkspaces.isEmpty()); for (auto &ws : hiddenWorkspaces) { m_currentSplit->setWorkspace(ws); // Go through this split. auto wins = getAllViewWindows(m_currentSplit, p_func); for (const auto win : wins) { // Do not remove the split even if it is empty. bool ret = closeViewWindow(win, p_force, false); if (!ret) { // User cancels the close of one ViewWindow. No need to restore the workspace. return false; } } m_currentSplit->setWorkspace(nullptr); // Remove this workspace if it is empty. if (ws->m_viewWindows.isEmpty()) { removeWorkspace(ws); } } // Restore. m_currentSplit->setWorkspace(currentWorkspace); } // Go through all splits. // Collect the ViewWindows first. Collect empty splits. QVector wins; QVector emptySplits; for (auto split : m_splits) { if (p_closeEmptySplit && split->getViewWindowCount() == 0) { emptySplits.push_back(split); continue; } wins.append(getAllViewWindows(split, p_func)); } if (!emptySplits.isEmpty()) { // Remove empty splits. for (auto split : emptySplits) { removeViewSplit(split, true); } } if (wins.isEmpty()) { return true; } // Close the ViewWindow. for (auto win : wins) { bool ret = closeViewWindow(win, p_force, true); if (!ret) { return false; } } return true; } void ViewArea::focus() { auto split = getCurrentViewSplit(); if (split) { split->focus(); } } QVector ViewArea::getVisibleNavigationItems() { QVector items; m_navigationItems.clear(); int idx = 0; for (auto split : m_splits) { if (split->getViewWindowCount() == 0) { continue; } if (idx >= NavigationMode::c_maxNumOfNavigationItems) { break; } auto info = split->getNavigationModeInfo(); for (int i = 0; i < info.size() && idx < NavigationMode::c_maxNumOfNavigationItems; ++i, ++idx) { items.push_back(info[i].m_viewWindow); m_navigationItems.push_back(info[i]); } } return items; } void ViewArea::placeNavigationLabel(int p_idx, void *p_item, QLabel *p_label) { Q_UNUSED(p_item); Q_ASSERT(p_idx > -1); p_label->setParent(m_navigationItems[p_idx].m_viewWindow->getViewSplit()); p_label->move(m_navigationItems[p_idx].m_topLeft); } void ViewArea::handleTargetHit(void *p_item) { if (p_item) { setCurrentViewWindow(static_cast(p_item)); focus(); } } void ViewArea::clearNavigation() { NavigationMode::clearNavigation(); m_navigationItems.clear(); } void ViewArea::dragEnterEvent(QDragEnterEvent *p_event) { if (UrlDragDropUtils::handleDragEnterEvent(p_event)) { return; } QWidget::dragEnterEvent(p_event); } void ViewArea::dropEvent(QDropEvent *p_event) { if (UrlDragDropUtils::handleDropEvent(p_event, [](const QStringList &p_files) { for (const auto &file : p_files) { emit VNoteX::getInst().openFileRequested(file, QSharedPointer::create()); } })) { return; } QWidget::dropEvent(p_event); } QVector ViewArea::getAllViewWindows(ViewSplit *p_split, const ViewSplit::ViewWindowSelector &p_func) const { QVector wins; p_split->forEachViewWindow([p_func, &wins](ViewWindow *p_win) { if (p_func(p_win)) { wins.push_back(p_win); } return true; }); return wins; } QVector ViewArea::getAllViewWindows(ViewSplit *p_split) const { return getAllViewWindows(p_split, [](ViewWindow *) { return true; }); } QList ViewArea::getAllBuffersInViewSplits() const { QSet bufferSet; for (auto split : m_splits) { auto wins = getAllViewWindows(split); for (auto win : wins) { bufferSet.insert(win->getBuffer()); } } return bufferSet.values(); } void ViewArea::loadSession() { auto &sessionConfig = ConfigMgr::getInst().getSessionConfig(); // Clear it if recover is disabled. auto sessionData = sessionConfig.getViewAreaSessionAndClear(); if (!ConfigMgr::getInst().getCoreConfig().isRecoverLastSessionOnStartEnabled()) { showSceneWidget(); return; } auto session = ViewAreaSession::deserialize(sessionData); // Load widgets layout. if (session.m_root.isEmpty()) { showSceneWidget(); } else { Q_ASSERT(m_splits.isEmpty()); if (session.m_root.m_type == ViewAreaSession::Node::Type::Splitter) { // Splitter. auto splitter = createSplitter(session.m_root.m_orientation, this); m_mainLayout->addWidget(splitter); loadSplitterFromSession(session.m_root, splitter); } else { // Just only one ViewSplit. Q_ASSERT(session.m_root.m_type == ViewAreaSession::Node::Type::ViewSplit); auto split = createViewSplit(this, session.m_root.m_viewSplitId); m_splits.push_back(split); m_mainLayout->addWidget(split); } QHash viewSplitToWorkspace; setCurrentViewSplit(m_splits.first(), false); // Load invisible workspace. for (int i = 0; i < session.m_workspaces.size(); ++i) { const auto &ws = session.m_workspaces[i]; if (ws.m_viewSplitId != InvalidViewSplitId) { viewSplitToWorkspace.insert(ws.m_viewSplitId, i); continue; } for (const auto &winSession : ws.m_viewWindows) { openViewWindowFromSession(winSession); } // Check if there is any window. if (m_currentSplit->getViewWindowCount() > 0) { m_currentSplit->setCurrentViewWindow(ws.m_currentViewWindowIndex); // New another workspace. auto newWs = createWorkspace(); m_workspaces.push_back(newWs); m_currentSplit->setWorkspace(newWs); } } // Load visible workspace. for (auto split : m_splits) { setCurrentViewSplit(split, false); auto it = viewSplitToWorkspace.find(split->getId()); Q_ASSERT(it != viewSplitToWorkspace.end()); const auto &ws = session.m_workspaces[it.value()]; for (const auto &winSession : ws.m_viewWindows) { openViewWindowFromSession(winSession); } if (m_currentSplit->getViewWindowCount() > 0) { m_currentSplit->setCurrentViewWindow(ws.m_currentViewWindowIndex); } } postFirstViewSplit(); distributeViewSplits(); } } void ViewArea::saveSession() const { ViewAreaSession session; takeSnapshot(session); auto &sessionConfig = ConfigMgr::getInst().getSessionConfig(); sessionConfig.setViewAreaSession(session.serialize()); } static void takeSnapshotOfWidgetNodes(ViewAreaSession::Node &p_node, const QWidget *p_widget, QHash &p_workspaceToViewSplit) { p_node.clear(); // Splitter. auto splitter = dynamic_cast(p_widget); if (splitter) { p_node.m_type = ViewAreaSession::Node::Type::Splitter; p_node.m_orientation = splitter->orientation(); p_node.m_children.resize(splitter->count()); for (int i = 0; i < p_node.m_children.size(); ++i) { takeSnapshotOfWidgetNodes(p_node.m_children[i], splitter->widget(i), p_workspaceToViewSplit); } return; } // ViewSplit. auto viewSplit = dynamic_cast(p_widget); Q_ASSERT(viewSplit); p_node.m_type = ViewAreaSession::Node::Type::ViewSplit; p_node.m_viewSplitId = viewSplit->getId(); auto ws = viewSplit->getWorkspace(); if (ws) { viewSplit->updateStateToWorkspace(); p_workspaceToViewSplit.insert(ws->m_id, viewSplit->getId()); } } void ViewArea::takeSnapshot(ViewAreaSession &p_session) const { QHash workspaceToViewSplit; // Widget hirarchy. p_session.m_root.clear(); if (!m_splits.isEmpty()) { auto topWidget = m_mainLayout->itemAt(0)->widget(); takeSnapshotOfWidgetNodes(p_session.m_root, topWidget, workspaceToViewSplit); } // Workspaces. p_session.m_workspaces.clear(); p_session.m_workspaces.reserve(m_workspaces.size()); for (const auto &ws : m_workspaces) { p_session.m_workspaces.push_back(ViewAreaSession::Workspace()); auto &wsSnap = p_session.m_workspaces.last(); if (ws->m_visible) { auto it = workspaceToViewSplit.find(ws->m_id); Q_ASSERT(it != workspaceToViewSplit.end()); wsSnap.m_viewSplitId = it.value(); } wsSnap.m_currentViewWindowIndex = ws->m_currentViewWindowIndex; for (auto win : ws->m_viewWindows) { if (win->isSessionEnabled()) { wsSnap.m_viewWindows.push_back(win->saveSession()); } } } } void ViewArea::loadSplitterFromSession(const ViewAreaSession::Node &p_node, QSplitter *p_splitter) { // @p_splitter is the splitter corresponding to @p_node. Q_ASSERT(p_node.m_type == ViewAreaSession::Node::Type::Splitter); for (const auto &child : p_node.m_children) { if (child.m_type == ViewAreaSession::Node::Type::Splitter) { auto childSplitter = createSplitter(child.m_orientation, p_splitter); p_splitter->addWidget(childSplitter); loadSplitterFromSession(child, childSplitter); } else { Q_ASSERT(child.m_type == ViewAreaSession::Node::Type::ViewSplit); auto childSplit = createViewSplit(this, child.m_viewSplitId); m_splits.push_back(childSplit); p_splitter->addWidget(childSplit); } } } void ViewArea::openViewWindowFromSession(const ViewWindowSession &p_session) { if (p_session.m_bufferPath.isEmpty()) { return; } auto paras = QSharedPointer::create(); paras->m_mode = p_session.m_viewWindowMode; paras->m_readOnly = p_session.m_readOnly; paras->m_lineNumber = p_session.m_lineNumber; paras->m_alwaysNewWindow = true; emit VNoteX::getInst().openFileRequested(p_session.m_bufferPath, paras); } void ViewArea::focusSplitByDirection(Direction p_direction) { auto split = findSplitByDirection(m_currentSplit, p_direction); if (!split) { qWarning() << "failed to focus split by direction"; return; } setCurrentViewSplit(split, true); flashViewSplit(split); } ViewSplit *ViewArea::findSplitByDirection(ViewSplit *p_split, Direction p_direction) { if (!p_split) { return nullptr; } QWidget *widget = p_split; const auto targetSplitType = splitTypeOfDirection(p_direction); int splitIdx = 0; QSplitter *targetSplitter = nullptr; while (true) { auto splitter = tryGetParentSplitter(widget); if (!splitter) { return nullptr; } if (checkSplitType(splitter) == targetSplitType) { targetSplitter = splitter; splitIdx = splitter->indexOf(widget); break; } else { widget = splitter; } } Q_ASSERT(targetSplitter); switch (p_direction) { case Direction::Left: --splitIdx; break; case Direction::Right: ++splitIdx; break; case Direction::Up: --splitIdx; break; case Direction::Down: ++splitIdx; break; } if (splitIdx < 0 || splitIdx >= targetSplitter->count()) { return nullptr; } auto targetWidget = targetSplitter->widget(splitIdx); // Find first split from targetWidget. while (true) { auto splitter = dynamic_cast(targetWidget); if (splitter) { if (splitter->count() == 0) { // Should not be an empty splitter. Q_ASSERT(false); return nullptr; } targetWidget = splitter->widget(0); } else { auto viewSplit = dynamic_cast(targetWidget); Q_ASSERT(viewSplit); return viewSplit; } } } ViewArea::SplitType ViewArea::checkSplitType(const QSplitter *p_splitter) const { return p_splitter->orientation() == Qt::Horizontal ? SplitType::Vertical : SplitType::Horizontal; } void ViewArea::flashViewSplit(ViewSplit *p_split) { auto tabBar = p_split->tabBar(); if (!tabBar) { return; } // Directly set the property of ViewSplit won't work. WidgetUtils::setPropertyDynamically(tabBar, PropertyDefs::c_viewSplitFlash, true); QTimer::singleShot(1000, tabBar, [tabBar]() { WidgetUtils::setPropertyDynamically(tabBar, PropertyDefs::c_viewSplitFlash, false); }); } void ViewArea::moveViewWindowOneSplit(ViewSplit *p_split, ViewWindow *p_win, Direction p_direction) { bool splitCountChanged = false; auto targetSplit = findSplitByDirection(p_split, p_direction); if (!targetSplit) { if (p_split->getViewWindowCount() == 1) { // Only one ViewWindow left. Skip it. return; } // Create a new split. targetSplit = splitViewSplit(p_split, splitTypeOfDirection(p_direction), false); if (p_direction == Direction::Left || p_direction == Direction::Up) { // Need to swap the position of the two splits. auto splitter = tryGetParentSplitter(targetSplit); Q_ASSERT(splitter); Q_ASSERT(splitter->indexOf(targetSplit) == 1); splitter->insertWidget(0, targetSplit); } splitCountChanged = true; } // Take ViewWindow out of @p_split. p_split->takeViewWindow(p_win); if (p_split->getViewWindowCount() == 0) { removeViewSplit(p_split, true); splitCountChanged = true; } targetSplit->addViewWindow(p_win); setCurrentViewWindow(p_win); flashViewSplit(targetSplit); if (splitCountChanged) { emit viewSplitsCountChanged(); } } ViewArea::SplitType ViewArea::splitTypeOfDirection(Direction p_direction) { if (p_direction == Direction::Up || p_direction == Direction::Down) { return SplitType::Horizontal; } else { return SplitType::Vertical; } } void ViewArea::updateHistory(const ViewWindowSession &p_session, Notebook *p_notebook) const { HistoryMgr::getInst().add(p_session.m_bufferPath, p_session.m_lineNumber, p_session.m_viewWindowMode, p_session.m_readOnly, p_notebook); } void ViewArea::closeFile(const QString &p_filePath, const QSharedPointer &p_event) { if (p_event->m_handled) { return; } auto node = VNoteX::getInst().getNotebookMgr().loadNodeByPath(p_filePath); bool done = false; if (node) { done = close(node.data(), false); } else { done = closeIf(false, [p_filePath](ViewWindow *p_win) { auto buffer = p_win->getBuffer(); return buffer->match(p_filePath); }, true); } p_event->m_response = done; p_event->m_handled = !done; }