kde-extraapps/kdevplatform/shell/assistantpopup.cpp
2015-07-26 14:23:17 +03:00

416 lines
13 KiB
C++

/*
Copyright 2009 David Nolden <david.nolden.kdevelop@art-master.de>
Copyright 2012 Milian Wolff <mail@milianw.de>
Copyright 2014 Sven Brauch <svenbrauch@gmail.com>
Copyright 2014 Kevin Funk <kfunk@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License version 2 as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "assistantpopup.h"
#include "sublime/holdupdates.h"
#include "util/kdevstringhandler.h"
#include <QKeyEvent>
#include <QDebug>
#include <QTimer>
#include <QDeclarativeContext>
#include <KLocalizedString>
#include <KAction>
#include <KParts/MainWindow>
#include <KStandardDirs>
#include <KTextEditor/HighlightInterface>
#include <KTextEditor/Document>
#include <KTextEditor/View>
#include <KTextEditor/ConfigInterface>
#include <KColorUtils>
#include <interfaces/icore.h>
#include <interfaces/iuicontroller.h>
#include <interfaces/idocumentcontroller.h>
using namespace KDevelop;
namespace {
/// Interval after which the state of the popup is re-evaluated
/// Used to avoid flickering caused when user is quickly inserting code
const int UPDATE_STATE_INTERVAL = 300; // ms
const int ASSISTANT_MODIFIER =
#ifdef Q_OS_MAC
Qt::CTRL;
#else
Qt::ALT;
#endif
const int ASSISTANT_MOD_KEY =
#ifdef Q_OS_MAC
Qt::Key_Control;
#else
Qt::Key_Alt;
#endif
QWidget* findByClassname(const KTextEditor::View* view, const QString& klass)
{
auto children = view->findChildren<QWidget*>();
for ( auto child: children ) {
if ( child->metaObject()->className() == klass ) {
return child;
}
}
return nullptr;
};
/**
* @brief Get the geometry of the inner part (with the text) of the KTextEditor::View being used.
*/
QRect textWidgetGeometry(const KTextEditor::View *view)
{
// Subtract the width of the right scrollbar
int scrollbarWidth = 0;
if ( auto scrollbar = findByClassname(view, "KateScrollBar") ) {
scrollbarWidth = scrollbar->width();
}
// Subtract the width of the bottom scrollbar
int bottomScrollbarWidth = 0;
if ( auto bottom = findByClassname(view, "QScrollBar") ) {
bottomScrollbarWidth = bottom->height();
}
auto geom = view->geometry();
geom.adjust(0, 0, -scrollbarWidth, -bottomScrollbarWidth);
return geom;
}
}
AssistantPopupConfig::AssistantPopupConfig(QObject *parent)
: QObject(parent)
, m_active(false)
, m_useVerticalLayout(false)
{
}
void AssistantPopupConfig::setColorsFromView(QObject *view)
{
auto iface = dynamic_cast<KTextEditor::ConfigInterface*>(view);
Q_ASSERT(iface);
m_foreground = iface->configValue("line-number-color").value<QColor>();
m_background = iface->configValue("icon-border-color").value<QColor>();
m_highlight = iface->configValue("folding-marker-color").value<QColor>();
if ( KColorUtils::luma(m_background) < 0.3 ) {
m_foreground = KColorUtils::lighten(m_foreground, 0.7);
}
const float lumaDiff = KColorUtils::luma(m_highlight) - KColorUtils::luma(m_background);
if ( qAbs(lumaDiff) < 0.5 ) {
m_highlight = QColor::fromHsv(m_highlight.hue(),
qMin(255, m_highlight.saturation() + 80),
lumaDiff > 0 ? qMin(255, m_highlight.value() + 120)
: qMax(80, m_highlight.value() - 40));
}
emit colorsChanged();
}
bool AssistantPopupConfig::isActive() const
{
return m_active;
}
void AssistantPopupConfig::setActive(bool active)
{
if (m_active == active) {
return;
}
m_active = active;
emit activeChanged(m_active);
}
void AssistantPopupConfig::setUseVerticalLayout(bool vertical)
{
if (m_useVerticalLayout == vertical) {
return;
}
m_useVerticalLayout = vertical;
emit useVerticalLayoutChanged(m_useVerticalLayout);
}
void AssistantPopupConfig::setTitle(const QString& title)
{
if (m_title == title) {
return;
}
m_title = title;
emit titleChanged(m_title);
}
void AssistantPopupConfig::setModel(const QList<QObject*>& model)
{
if (m_model == model) {
return;
}
qDeleteAll( m_model );
m_model = model;
emit modelChanged(model);
}
AssistantPopup::AssistantPopup()
// main window as parent to use maximal space available in worst case
: QDeclarativeView(ICore::self()->uiController()->activeMainWindow())
, m_config(new AssistantPopupConfig(this))
, m_shownAtBottom(false)
, m_reopening(false)
, m_updateTimer(new QTimer(this))
{
QPalette p = palette();
p.setColor(QPalette::Window, Qt::transparent);
setPalette(p);
setBackgroundRole(QPalette::Window);
setBackgroundBrush(QBrush(QColor(0, 0, 0, 0)));
setResizeMode(QDeclarativeView::SizeViewToRootObject);
setAttribute(Qt::WA_ShowWithoutActivating);
rootContext()->setContextProperty("config", m_config);
setSource(QUrl::fromLocalFile(KStandardDirs::locate("data", "kdevelop/assistantpopup.qml")));
if (!rootObject()) {
kWarning() << "Failed to load assistant markup! The assistant will not work.";
}
m_updateTimer->setInterval(UPDATE_STATE_INTERVAL);
m_updateTimer->setSingleShot(true);
connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(updateState()));
for (int i = Qt::Key_0; i <= Qt::Key_9; ++i) {
m_shortcuts.append(new QShortcut(ASSISTANT_MODIFIER + i, this));
}
setActive(false);
}
void AssistantPopup::reset(KTextEditor::View* view, const IAssistant::Ptr& assistant)
{
setView(view);
setAssistant(assistant);
updateLayoutType();
m_updateTimer->start();
}
void AssistantPopup::setView(KTextEditor::View* view)
{
if (m_view == view) {
return;
}
setActive(false);
if (m_view) {
m_view->removeEventFilter(this);
disconnect(m_view, SIGNAL(verticalScrollPositionChanged(KTextEditor::View*,KTextEditor::Cursor)),
this, SLOT(updatePosition(KTextEditor::View*,KTextEditor::Cursor)));
}
m_view = view;
if (m_view) {
m_view->installEventFilter(this);
connect(m_view, SIGNAL(verticalScrollPositionChanged(KTextEditor::View*,KTextEditor::Cursor)),
this, SLOT(updatePosition(KTextEditor::View*,KTextEditor::Cursor)));
}
}
void AssistantPopup::setAssistant(const IAssistant::Ptr& assistant)
{
if (m_assistant == assistant) {
return;
}
if (m_assistant) {
disconnect(m_assistant.data(), SIGNAL(actionsChanged()), m_updateTimer, SLOT(start()));
disconnect(m_assistant.data(), SIGNAL(hide()), this, SLOT(hideAssistant()));
}
m_assistant = assistant;
if (m_assistant) {
connect(m_assistant.data(), SIGNAL(actionsChanged()), m_updateTimer, SLOT(start()));
connect(m_assistant.data(), SIGNAL(hide()), this, SLOT(hideAssistant()));
}
}
void AssistantPopup::setActive(bool active)
{
m_config->setActive(active);
for (auto shortcut : m_shortcuts) {
shortcut->setEnabled(active);
}
}
bool AssistantPopup::viewportEvent(QEvent *event)
{
// For some reason, QGraphicsView posts a WindowActivate event
// when it is shown, even if disabled through setting the WA_ShowWithoutActivate
// attribute. This causes all focus-driven popups (QuickOpen, tooltips, ...)
// to hide when the assistant opens. Thus, prevent it from processing the Show event here.
if ( event->type() == QEvent::Show ) {
return true;
}
return QGraphicsView::viewportEvent(event);
}
bool AssistantPopup::eventFilter(QObject* object, QEvent* event)
{
if (!m_view)
return false;
Q_ASSERT(object == m_view.data());
Q_UNUSED(object);
if (event->type() == QEvent::Resize) {
updateLayoutType();
updatePosition(m_view.data(), KTextEditor::Cursor::invalid());
} else if (event->type() == QEvent::Hide) {
executeHideAction();
} else if (event->type() == QEvent::KeyPress) {
auto keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->modifiers() == ASSISTANT_MODIFIER) {
setActive(true);
}
if (keyEvent->key() == Qt::Key_Escape) {
executeHideAction();
}
} else if (event->type() == QEvent::KeyRelease) {
auto keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->modifiers() == ASSISTANT_MODIFIER || keyEvent->key() == ASSISTANT_MOD_KEY) {
setActive(false);
}
}
return false;
}
void AssistantPopup::updatePosition(KTextEditor::View* view, const KTextEditor::Cursor& newPos)
{
static const int MARGIN = 12;
if (newPos.isValid() && newPos.line() == 0 && !m_shownAtBottom) {
// the position is not going to change; don't waste time
return;
}
auto editorGeometry = textWidgetGeometry(view);
const auto startCursorCoordinate = view->cursorToCoordinate(KTextEditor::Cursor(0, 0));
// algorithm for popup positioning:
// if we are scrolled to the top: show at bottom
// else:
// if: current cursor position is in upper half => show at bottom
// else: show at top
const bool showAtBottom = startCursorCoordinate.y() == 0 ? true :
view->cursorPositionCoordinates().y() < view->height()/2;
const QPoint targetLocation = showAtBottom ?
parentWidget()->mapFromGlobal(view->mapToGlobal(editorGeometry.bottomRight()
+ QPoint(-width() - MARGIN, -MARGIN - height()))) :
parentWidget()->mapFromGlobal(view->mapToGlobal(editorGeometry.topRight()
+ QPoint(-width() - MARGIN, MARGIN)));
if (pos() == targetLocation) {
return;
}
if ( m_reopening ) {
// When the assistant is already visible, close to no flickering will occur anyways,
// so we can avoid the full repaint of the window.
move(targetLocation);
}
else {
Sublime::HoldUpdates hold(ICore::self()->uiController()->activeMainWindow());
move(targetLocation);
}
}
IAssistant::Ptr AssistantPopup::assistant() const
{
return m_assistant;
}
void AssistantPopup::executeHideAction()
{
if ( isVisible() ) {
m_assistant->doHide();
}
}
void AssistantPopup::hideAssistant()
{
reset(nullptr, {}); // indirectly calls hide()
}
void AssistantPopup::updateLayoutType()
{
if ( !m_assistant || !m_view ) {
return;
}
// Make a rough estimate of the width the assistant will need
// and decide on whether to use vertical layout or not.
const auto& metrics = fontMetrics();
auto textWidth = 0;
textWidth += metrics.boundingRect(KDevelop::htmlToPlainText(assistant()->title())).width();
for ( const auto& action: assistant()->actions() ) {
textWidth += metrics.boundingRect(KDevelop::htmlToPlainText(action->description())).width();
textWidth += 10;
}
m_config->setUseVerticalLayout(textWidth > textWidgetGeometry(m_view).width()*0.75);
updateState();
}
void AssistantPopup::updateState()
{
if (!m_assistant || m_assistant->actions().isEmpty() || !m_view) {
hide();
return;
}
auto curShortcut = m_shortcuts.constBegin();
auto hideAction = new KAction(i18n("Hide"), this);
connect(*curShortcut, SIGNAL(activated()), hideAction, SLOT(trigger()));
connect(hideAction, SIGNAL(triggered()), this, SLOT(executeHideAction()));
QList<QObject*> items;
foreach (IAssistantAction::Ptr action, m_assistant->actions()) {
items << action->toKAction();
items.last()->setParent(this);
//For some reason, KAction's setShortcut does nothing, so we manage with QShortcut
if (++curShortcut != m_shortcuts.constEnd()) {
connect(*curShortcut, SIGNAL(activated()), items.last(), SLOT(trigger()));
}
}
items << hideAction;
auto doc = ICore::self()->documentController()->activeDocument();
m_config->setColorsFromView(doc->textDocument()->activeView());
m_config->setModel(items);
m_config->setTitle(m_assistant->title());
setActive(false);
// both changed title or actions may change the appearance of the popup
// force recomputing the size hint
resize(sizeHint());
updatePosition(m_view, KTextEditor::Cursor::invalid());
show();
}
#include "moc_assistantpopup.cpp"