From 2fd8cef01bd177bdf9deb3af0e342d18fcdf6931 Mon Sep 17 00:00:00 2001 From: Tomaz Canabrava Date: Tue, 2 Jul 2019 15:15:52 +0000 Subject: [PATCH] Implement Drag & Drop for Tiling Operations This allows the split views to be dragged around the current tab by their headers. It also implements a toggle button to maximize/restore each view. --- src/DetachableTabBar.cpp | 22 ++++++++ src/DetachableTabBar.h | 2 + src/TerminalDisplay.cpp | 60 ++++++++++++++++---- src/TerminalDisplay.h | 10 ++++ src/TerminalHeaderBar.cpp | 34 +++++++---- src/TerminalHeaderBar.h | 2 + src/ViewContainer.cpp | 12 ++++ src/ViewContainer.h | 3 + src/ViewManager.cpp | 15 +++-- src/ViewSplitter.cpp | 116 ++++++++++++++++++++++++++++++++------ src/ViewSplitter.h | 26 +++++---- 11 files changed, 250 insertions(+), 52 deletions(-) diff --git a/src/DetachableTabBar.cpp b/src/DetachableTabBar.cpp index e50bde5a..0888d53b 100644 --- a/src/DetachableTabBar.cpp +++ b/src/DetachableTabBar.cpp @@ -23,6 +23,7 @@ #include #include +#include namespace Konsole { @@ -32,6 +33,7 @@ DetachableTabBar::DetachableTabBar(QWidget *parent) : _originalCursor(cursor()), tabId(-1) { + setAcceptDrops(true); setUsesScrollButtons(false); setElideMode(Qt::TextElideMode::ElideMiddle); } @@ -115,4 +117,24 @@ void DetachableTabBar::mouseReleaseEvent(QMouseEvent *event) } } +void DetachableTabBar::dragEnterEvent(QDragEnterEvent* event) +{ + const auto dragId = QStringLiteral("konsole/terminal_display"); + if (event->mimeData()->hasFormat(dragId)) { + auto other_pid = event->mimeData()->data(dragId).toInt(); + // don't accept the drop if it's another instance of konsole + if (qApp->applicationPid() != other_pid) + return; + event->accept(); + } +} + +void DetachableTabBar::dragMoveEvent(QDragMoveEvent* event) +{ + int tabIdx = tabAt(event->pos()); + if (tabIdx != -1) { + setCurrentIndex(tabIdx); + } +} + } diff --git a/src/DetachableTabBar.h b/src/DetachableTabBar.h index 4b8886fa..36dc7452 100644 --- a/src/DetachableTabBar.h +++ b/src/DetachableTabBar.h @@ -41,6 +41,8 @@ protected: void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent*event) override; void mouseReleaseEvent(QMouseEvent *event) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent * event) override; private: DragType dragType; diff --git a/src/TerminalDisplay.cpp b/src/TerminalDisplay.cpp index a6adc0e3..47aa50c9 100644 --- a/src/TerminalDisplay.cpp +++ b/src/TerminalDisplay.cpp @@ -485,6 +485,7 @@ TerminalDisplay::TerminalDisplay(QWidget* parent) , _searchBar(new IncrementalSearchBar(this)) , _headerBar(new TerminalHeaderBar(this)) , _searchResultRect(QRect()) + , _drawOverlay(false) { // terminal applications are not designed with Right-To-Left in mind, // so the layout is forced to Left-To-Right @@ -541,7 +542,6 @@ TerminalDisplay::TerminalDisplay(QWidget* parent) _verticalLayout->setSpacing(0); _verticalLayout->setMargin(0); setLayout(_verticalLayout); - new AutoScrollHandler(this); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(Konsole::accessibleInterfaceFactory); @@ -562,6 +562,35 @@ TerminalDisplay::~TerminalDisplay() _outputSuspendedMessageWidget = nullptr; } +void TerminalDisplay::hideDragTarget() +{ + _drawOverlay = false; + update(); +} + +void TerminalDisplay::showDragTarget() +{ + auto cursorPos = mapFromGlobal(QCursor::pos()); + using EdgeDistance = std::pair; + auto closerToEdge = std::min( + { + {cursorPos.x(), Qt::LeftEdge}, + {cursorPos.y(), Qt::TopEdge}, + {width() - cursorPos.x(), Qt::RightEdge}, + {height() - cursorPos.y(), Qt::BottomEdge} + }, + [](const EdgeDistance& left, const EdgeDistance& right) -> bool { + return left.first < right.first; + } + ); + if (_overlayEdge == closerToEdge.second) { + return; + } + _overlayEdge = closerToEdge.second; + _drawOverlay = true; + update(); +} + /* ------------------------------------------------------------------------- */ /* */ /* Display Operations */ @@ -928,6 +957,13 @@ void TerminalDisplay::scrollImage(int lines , const QRect& screenWindowRegion) Q_ASSERT(linesToMove > 0); Q_ASSERT(bytesToMove > 0); + scrollRect.setTop( lines > 0 ? top : top + abs(lines) * _fontHeight); + scrollRect.setHeight(linesToMove * _fontHeight); + + if (!scrollRect.isValid() || scrollRect.isEmpty()) { + return; + } + //scroll internal image if (lines > 0) { // check that the memory areas that we are going to move are valid @@ -938,9 +974,6 @@ void TerminalDisplay::scrollImage(int lines , const QRect& screenWindowRegion) //scroll internal image down memmove(firstCharPos , lastCharPos , bytesToMove); - - //set region of display to scroll - scrollRect.setTop(top); } else { // check that the memory areas that we are going to move are valid Q_ASSERT((char*)firstCharPos + bytesToMove < @@ -948,13 +981,7 @@ void TerminalDisplay::scrollImage(int lines , const QRect& screenWindowRegion) //scroll internal image up memmove(lastCharPos , firstCharPos , bytesToMove); - - //set region of the display to scroll - scrollRect.setTop(top + abs(lines) * _fontHeight); } - scrollRect.setHeight(linesToMove * _fontHeight); - - Q_ASSERT(scrollRect.isValid() && !scrollRect.isEmpty()); //scroll the display vertically to match internal _image scroll(0 , _fontHeight * (-lines) , scrollRect); @@ -1271,6 +1298,19 @@ void TerminalDisplay::paintEvent(QPaintEvent* pe) paint.fillRect(rect, dimColor); } } + + if (_drawOverlay) { + const auto y = _headerBar->isVisible() ? _headerBar->height() : 0; + const auto rect = _overlayEdge == Qt::LeftEdge ? QRect(0, y, width() / 2, height()) + : _overlayEdge == Qt::TopEdge ? QRect(0, y, width(), height() / 2) + : _overlayEdge == Qt::RightEdge ? QRect(width() - width() / 2, y, width() / 2, height()) + : QRect(0, height() - height() / 2, width(), height() / 2); + + paint.setRenderHint(QPainter::Antialiasing); + paint.setPen(Qt::NoPen); + paint.setBrush(QColor(100,100,100, 127)); + paint.drawRect(rect); + } } void TerminalDisplay::printContent(QPainter& painter, bool friendly) diff --git a/src/TerminalDisplay.h b/src/TerminalDisplay.h index e56a7908..1d30bfb0 100644 --- a/src/TerminalDisplay.h +++ b/src/TerminalDisplay.h @@ -74,6 +74,9 @@ public: explicit TerminalDisplay(QWidget *parent = nullptr); ~TerminalDisplay() Q_DECL_OVERRIDE; + void showDragTarget(); + void hideDragTarget(); + void applyProfile(const Profile::Ptr& profile); /** Returns the terminal color palette used by the display. */ @@ -483,6 +486,10 @@ public Q_SLOTS: return _scrollbarLocation; } + Qt::Edge droppedEdge() const { + return _overlayEdge; + } + // Used to show/hide the message widget void updateReadOnlyState(bool readonly); IncrementalSearchBar *searchBar() const; @@ -852,6 +859,9 @@ private: TerminalHeaderBar *_headerBar; QRect _searchResultRect; friend class TerminalDisplayAccessible; + + bool _drawOverlay; + Qt::Edge _overlayEdge; }; class AutoScrollHandler : public QObject diff --git a/src/TerminalHeaderBar.cpp b/src/TerminalHeaderBar.cpp index 1dafba49..33cf563f 100644 --- a/src/TerminalHeaderBar.cpp +++ b/src/TerminalHeaderBar.cpp @@ -41,6 +41,8 @@ #include #include #include +#include +#include namespace Konsole { @@ -80,22 +82,19 @@ TerminalHeaderBar::TerminalHeaderBar(QWidget *parent) setAutoFillBackground(true); terminalFocusOut(); + connect(m_toggleExpandedMode, &QToolButton::clicked, + this, &TerminalHeaderBar::requestToggleExpansion); + } // Hack untill I can detangle the creation of the TerminalViews void TerminalHeaderBar::finishHeaderSetup(ViewProperties *properties) { - //TODO: Fix ViewProperties signals. - connect(properties, &Konsole::ViewProperties::titleChanged, this, - [this, properties]{ + auto controller = dynamic_cast(properties); + connect(properties, &Konsole::ViewProperties::titleChanged, this, [this, properties]{ m_terminalTitle->setText(properties->title()); }); - connect(m_closeBtn, &QToolButton::clicked, this, [properties]{ - auto controller = qobject_cast(properties); - controller->closeSession(); - }); - connect(properties, &Konsole::ViewProperties::iconChanged, this, [this, properties] { m_terminalIcon->setPixmap(properties->icon().pixmap(QSize(22,22))); }); @@ -104,8 +103,7 @@ void TerminalHeaderBar::finishHeaderSetup(ViewProperties *properties) m_terminalActivity->setPixmap(QPixmap()); }); - connect(m_toggleExpandedMode, &QToolButton::clicked, - this, &TerminalHeaderBar::requestToggleExpansion); + connect(m_closeBtn, &QToolButton::clicked, controller, &SessionController::closeSession); } void TerminalHeaderBar::paintEvent(QPaintEvent *paintEvent) @@ -145,12 +143,24 @@ void TerminalHeaderBar::paintEvent(QPaintEvent *paintEvent) void TerminalHeaderBar::mouseMoveEvent(QMouseEvent* ev) { - Q_UNUSED(ev); + if (m_toggleExpandedMode->isChecked()) { + return; + } + auto point = ev->pos() - m_startDrag; + if (point.manhattanLength() > 10) { + auto drag = new QDrag(parent()); + auto mimeData = new QMimeData(); + QByteArray payload; + payload.setNum(qApp->applicationPid()); + mimeData->setData(QStringLiteral("konsole/terminal_display"), payload); + drag->setMimeData(mimeData); + drag->start(); + } } void TerminalHeaderBar::mousePressEvent(QMouseEvent* ev) { - Q_UNUSED(ev); + m_startDrag = ev->pos(); } void TerminalHeaderBar::mouseReleaseEvent(QMouseEvent* ev) diff --git a/src/TerminalHeaderBar.h b/src/TerminalHeaderBar.h index 0b27fb4d..81c66779 100644 --- a/src/TerminalHeaderBar.h +++ b/src/TerminalHeaderBar.h @@ -23,6 +23,7 @@ #define TERMINAL_HEADER_BAR_H #include +#include class QLabel; class QToolButton; @@ -59,6 +60,7 @@ private: QToolButton *m_closeBtn; QToolButton *m_toggleExpandedMode; bool m_terminalIsFocused; + QPoint m_startDrag; }; } // namespace Konsole diff --git a/src/ViewContainer.cpp b/src/ViewContainer.cpp index b4fd32a1..5972eae6 100644 --- a/src/ViewContainer.cpp +++ b/src/ViewContainer.cpp @@ -267,6 +267,12 @@ void TabbedViewContainer::moveActiveView(MoveDirection direction) setCurrentIndex(newIndex); } +void TabbedViewContainer::terminalDisplayDropped(TerminalDisplay *terminalDisplay) { + Session* terminalSession = terminalDisplay->sessionController()->session(); + terminalDisplay->sessionController()->deleteLater(); + connectedViewManager()->attachView(terminalDisplay, terminalSession); +} + void TabbedViewContainer::addSplitter(ViewSplitter *viewSplitter, int index) { if (index == -1) { index = addTab(viewSplitter, QString()); @@ -274,6 +280,10 @@ void TabbedViewContainer::addSplitter(ViewSplitter *viewSplitter, int index) { insertTab(index, viewSplitter, QString()); } connect(viewSplitter, &ViewSplitter::destroyed, this, &TabbedViewContainer::viewDestroyed); + + disconnect(viewSplitter, &ViewSplitter::terminalDisplayDropped, nullptr, nullptr); + connect(viewSplitter, &ViewSplitter::terminalDisplayDropped, this, &TabbedViewContainer::terminalDisplayDropped); + auto terminalDisplays = viewSplitter->findChildren(); foreach(TerminalDisplay* terminal, terminalDisplays) { connectTerminalDisplay(terminal); @@ -298,6 +308,8 @@ void TabbedViewContainer::addView(TerminalDisplay *view) connectTerminalDisplay(view); connect(viewSplitter, &ViewSplitter::destroyed, this, &TabbedViewContainer::viewDestroyed); + connect(viewSplitter, &ViewSplitter::terminalDisplayDropped, this, &TabbedViewContainer::terminalDisplayDropped); + setCurrentIndex(index); emit viewAdded(view); } diff --git a/src/ViewContainer.h b/src/ViewContainer.h index 2a2f7659..74071367 100644 --- a/src/ViewContainer.h +++ b/src/ViewContainer.h @@ -161,6 +161,7 @@ public: }; void setNavigationBehavior(int behavior); + void terminalDisplayDropped(TerminalDisplay* terminalDisplay); Q_SIGNALS: /** Emitted when the container has no more children */ @@ -172,6 +173,8 @@ Q_SIGNALS: /** Requests creation of a new view, with the selected profile. */ void newViewWithProfileRequest(const Profile::Ptr&); + /** a terminalDisplay was dropped in a child Splitter */ + /** * Emitted when the user requests to move a view from another container * into this container. If 'success' is set to true by a connected slot diff --git a/src/ViewManager.cpp b/src/ViewManager.cpp index 5f178fc0..1a7d430a 100644 --- a/src/ViewManager.cpp +++ b/src/ViewManager.cpp @@ -253,7 +253,6 @@ void ViewManager::setupActions() collection->addAction(QStringLiteral("switch-to-tab-%1").arg(i), action); } - connect(_viewContainer, &TabbedViewContainer::viewAdded, this, &ViewManager::toggleActionsBasedOnState); connect(_viewContainer, &TabbedViewContainer::viewRemoved, this, &ViewManager::toggleActionsBasedOnState); connect(_viewContainer, &QTabWidget::currentChanged, this, &ViewManager::toggleActionsBasedOnState); @@ -433,6 +432,8 @@ QHash ViewManager::forgetAll(ViewSplitter* splitter) Session* ViewManager::forgetTerminal(TerminalDisplay* terminal) { + disconnect(terminal, &TerminalDisplay::requestToggleExpansion, nullptr, nullptr); + removeController(terminal->sessionController()); auto session = _sessionMap.take(terminal); if (session != nullptr) { @@ -591,10 +592,8 @@ SessionController *ViewManager::createController(Session *session, TerminalDispl // should this be handed by ViewManager::unplugController signal void ViewManager::removeController(SessionController* controller) { - disconnect(controller, &Konsole::SessionController::focused, this, - &Konsole::ViewManager::controllerChanged); if (_pluggedController == controller) { - _pluggedController = nullptr; + _pluggedController.clear(); } controller->deleteLater(); } @@ -620,6 +619,14 @@ void ViewManager::attachView(TerminalDisplay *terminal, Session *session) { connect(session, &Konsole::Session::finished, this, &Konsole::ViewManager::sessionFinished, Qt::UniqueConnection); + + // Disconnect from the other viewcontainer. + disconnect(terminal, &TerminalDisplay::requestToggleExpansion, nullptr, nullptr); + + // reconnect on this container. + connect(terminal, &TerminalDisplay::requestToggleExpansion, + _viewContainer, &TabbedViewContainer::toggleMaximizeCurrentTerminal, Qt::UniqueConnection); + _sessionMap[terminal] = session; createController(session, terminal); toggleActionsBasedOnState(); diff --git a/src/ViewSplitter.cpp b/src/ViewSplitter.cpp index 95178250..af01b010 100644 --- a/src/ViewSplitter.cpp +++ b/src/ViewSplitter.cpp @@ -26,6 +26,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include + // Konsole #include "ViewContainer.h" @@ -39,14 +47,20 @@ using Konsole::TerminalDisplay; ViewSplitter::ViewSplitter(QWidget *parent) : QSplitter(parent) { + setAcceptDrops(true); } +/* This function is called on the toplevel splitter, we need to look at the actual ViewSplitter inside it */ void ViewSplitter::adjustActiveTerminalDisplaySize(int percentage) { - const int containerIndex = indexOf(activeTerminalDisplay()); + auto focusedTerminalDisplay = activeTerminalDisplay(); + Q_ASSERT(focusedTerminalDisplay); + + auto parentSplitter = qobject_cast(focusedTerminalDisplay->parent()); + const int containerIndex = parentSplitter->indexOf(activeTerminalDisplay()); Q_ASSERT(containerIndex != -1); - QList containerSizes = sizes(); + QList containerSizes = parentSplitter->sizes(); const int oldSize = containerSizes[containerIndex]; const int newSize = static_cast(oldSize * (1.0 + percentage / 100.0)); @@ -57,7 +71,7 @@ void ViewSplitter::adjustActiveTerminalDisplaySize(int percentage) } containerSizes[containerIndex] = newSize; - setSizes(containerSizes); + parentSplitter->setSizes(containerSizes); } // Get the first splitter that's a parent of the current focused widget. @@ -82,31 +96,26 @@ void ViewSplitter::updateSizes() setSizes(QVector(count(), space).toList()); } -void ViewSplitter::addTerminalDisplay(TerminalDisplay *terminalDisplay, Qt::Orientation containerOrientation) +void ViewSplitter::addTerminalDisplay(TerminalDisplay *terminalDisplay, Qt::Orientation containerOrientation, AddBehavior behavior) { ViewSplitter *splitter = activeSplitter(); + const int currentIndex = !splitter->activeTerminalDisplay() ? splitter->count() + : splitter->indexOf(splitter->activeTerminalDisplay()); + if (splitter->count() < 2) { - splitter->addWidget(terminalDisplay); + splitter->insertWidget(behavior == AddBehavior::AddBefore ? currentIndex : currentIndex + 1, terminalDisplay); splitter->setOrientation(containerOrientation); } else if (containerOrientation == splitter->orientation()) { - auto activeDisplay = splitter->activeTerminalDisplay(); - if (!activeDisplay) { - splitter->addWidget(terminalDisplay); - } else { - const int currentIndex = splitter->indexOf(activeDisplay); - splitter->insertWidget(currentIndex, terminalDisplay); - } + splitter->insertWidget(currentIndex, terminalDisplay); } else { auto newSplitter = new ViewSplitter(); - TerminalDisplay *oldTerminalDisplay = splitter->activeTerminalDisplay(); const int oldContainerIndex = splitter->indexOf(oldTerminalDisplay); - newSplitter->addWidget(oldTerminalDisplay); - newSplitter->addWidget(terminalDisplay); + newSplitter->addWidget(behavior == AddBehavior::AddBefore ? terminalDisplay : oldTerminalDisplay); + newSplitter->addWidget(behavior == AddBehavior::AddBefore ? oldTerminalDisplay : terminalDisplay); newSplitter->setOrientation(containerOrientation); newSplitter->updateSizes(); newSplitter->show(); - splitter->insertWidget(oldContainerIndex, newSplitter); } splitter->updateSizes(); @@ -269,3 +278,78 @@ ViewSplitter *ViewSplitter::getToplevelSplitter() } return current; } + +namespace { + TerminalDisplay *currentDragTarget = nullptr; +} + +void Konsole::ViewSplitter::dragEnterEvent(QDragEnterEvent* ev) +{ + const auto dragId = QStringLiteral("konsole/terminal_display"); + if (ev->mimeData()->hasFormat(dragId)) { + auto other_pid = ev->mimeData()->data(dragId).toInt(); + // don't accept the drop if it's another instance of konsole + if (qApp->applicationPid() != other_pid) + return; + if (getToplevelSplitter()->terminalMaximized()) { + return; + } + ev->accept(); + } +} + +void Konsole::ViewSplitter::dragMoveEvent(QDragMoveEvent* ev) +{ + auto currentWidget = childAt(ev->pos()); + if (auto terminal = qobject_cast(currentWidget)) { + if (currentDragTarget && currentDragTarget != terminal) { + currentDragTarget->hideDragTarget(); + } + if (terminal == ev->source()) { + return; + } + currentDragTarget = terminal; + currentDragTarget->showDragTarget(); + } +} + +void Konsole::ViewSplitter::dragLeaveEvent(QDragLeaveEvent* event) +{ + if (currentDragTarget) { + currentDragTarget->hideDragTarget(); + currentDragTarget = nullptr; + } +} + +void Konsole::ViewSplitter::dropEvent(QDropEvent* ev) +{ + if (ev->mimeData()->hasFormat(QStringLiteral("konsole/terminal_display"))) { + if (getToplevelSplitter()->terminalMaximized()) { + return; + } + if (currentDragTarget) { + currentDragTarget->hideDragTarget(); + auto source = qobject_cast(ev->source()); + source->setVisible(false); + source->setParent(nullptr); + + currentDragTarget->setFocus(Qt::OtherFocusReason); + const auto droppedEdge = currentDragTarget->droppedEdge(); + + AddBehavior behavior = droppedEdge == Qt::LeftEdge || droppedEdge == Qt::TopEdge + ? AddBehavior::AddBefore : AddBehavior::AddAfter; + + Qt::Orientation orientation = droppedEdge == Qt::LeftEdge || droppedEdge == Qt::RightEdge + ? Qt::Horizontal : Qt::Vertical; + + // topLevel is the splitter that's connected with the ViewManager + // that in turn can call the SessionController. + getToplevelSplitter()->terminalDisplayDropped(source); + addTerminalDisplay(source, orientation, behavior); + source->setVisible(true); + currentDragTarget = nullptr; + } + } +} + + diff --git a/src/ViewSplitter.h b/src/ViewSplitter.h index d91864a2..16bc03fd 100644 --- a/src/ViewSplitter.h +++ b/src/ViewSplitter.h @@ -30,6 +30,10 @@ #include "konsoleprivate_export.h" class QFocusEvent; +class QDragMoveEvent; +class QDragEnterEvent; +class QDropEvent; +class QDragLeaveEvent; namespace Konsole { class TerminalDisplay; @@ -52,7 +56,7 @@ class KONSOLEPRIVATE_EXPORT ViewSplitter : public QSplitter public: explicit ViewSplitter(QWidget *parent = nullptr); - + enum class AddBehavior {AddBefore, AddAfter}; /** * Locates the child ViewSplitter widget which currently has the focus * and inserts the container into it. @@ -68,10 +72,7 @@ public: * will be created, into which the container will * be inserted. */ - void addTerminalDisplay(TerminalDisplay *terminalDisplay, Qt::Orientation orientation); - - /** Removes a container from the splitter. The container is not deleted. */ - void removeTerminalDisplay(TerminalDisplay *terminalDisplay); + void addTerminalDisplay(TerminalDisplay* terminalDisplay, Qt::Orientation containerOrientation, AddBehavior behavior = AddBehavior::AddAfter); /** Returns the child ViewSplitter widget which currently has the focus */ ViewSplitter *activeSplitter(); @@ -98,11 +99,6 @@ public: /** returns the splitter that has no splitter as a parent. */ ViewSplitter *getToplevelSplitter(); - /** - * Gives the focus to the active view in the specified container - */ - void setActiveTerminalDisplay(TerminalDisplay *container); - /** * Changes the size of the specified @p container by a given @p percentage. * @p percentage may be positive ( in which case the size of the container @@ -122,6 +118,16 @@ public: void handleFocusDirection(Qt::Orientation orientation, int direction); void childEvent(QChildEvent* event) override; + bool terminalMaximized() const { return m_terminalMaximized; } + +protected: + void dragEnterEvent(QDragEnterEvent *ev) override; + void dragMoveEvent(QDragMoveEvent *ev) override; + void dragLeaveEvent(QDragLeaveEvent * event) override; + void dropEvent(QDropEvent *ev) override; + +Q_SIGNALS: + void terminalDisplayDropped(TerminalDisplay *terminalDisplay); private: /** recursively walks the object tree looking for Splitters and