Terminal: lock/unlock keyboard

We copied QShortCutMap into Qtc to allow us tight control over which shortcuts
are "enabled" while the focus is inside a terminal, and the keyboard is "locked"
to the Terminal. Locked here means that except for a select few, all key presses
are send directly to the terminal and cannot be used to activate other actions.

Change-Id: I96cddf753033c0f4e7d806b20085bb4755853117
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
Marcus Tillmanns
2023-06-06 09:37:13 +02:00
parent ad5e8392fe
commit 217b03c1ac
10 changed files with 711 additions and 45 deletions

View File

@@ -92,6 +92,8 @@ protected:
void setupContext(const Context &context, QWidget *widget);
void setZoomButtonsEnabled(bool enabled);
IContext *m_context = nullptr;
private:
virtual void updateFilter();
@@ -108,7 +110,6 @@ private:
QAction *m_filterActionCaseSensitive = nullptr;
QAction *m_invertFilterAction = nullptr;
Utils::FancyLineEdit *m_filterOutputLineEdit = nullptr;
IContext *m_context = nullptr;
bool m_filterRegexp = false;
bool m_invertFilter = false;
Qt::CaseSensitivity m_filterCaseSensitivity = Qt::CaseInsensitive;

View File

@@ -9,6 +9,7 @@ add_qtc_plugin(Terminal
scrollback.cpp scrollback.h
shellintegration.cpp shellintegration.h
shellmodel.cpp shellmodel.h
shortcutmap.cpp shortcutmap.h
terminal.qrc
terminalconstants.h
terminalicons.h

View File

@@ -0,0 +1,557 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
// COPIED FROM qshortcutmap.cpp
#include "shortcutmap.h"
#include <algorithm>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QLoggingCategory>
#include <QWindow>
Q_LOGGING_CATEGORY(lcShortcutMap, "terminal.shortcutmap", QtWarningMsg)
namespace Terminal::Internal {
/* \internal
Entry data for ShortcutMap
Contains:
Keysequence for entry
Pointer to parent owning the sequence
*/
struct ShortcutEntry
{
ShortcutEntry()
: keyseq(0)
, context(Qt::WindowShortcut)
, enabled(false)
, autorepeat(1)
, id(0)
, owner(nullptr)
, contextMatcher(nullptr)
{}
ShortcutEntry(const QKeySequence &k)
: keyseq(k)
, context(Qt::WindowShortcut)
, enabled(false)
, autorepeat(1)
, id(0)
, owner(nullptr)
, contextMatcher(nullptr)
{}
ShortcutEntry(QObject *o,
const QKeySequence &k,
Qt::ShortcutContext c,
int i,
bool a,
ShortcutMap::ContextMatcher m)
: keyseq(k)
, context(c)
, enabled(true)
, autorepeat(a)
, id(i)
, owner(o)
, contextMatcher(m)
{}
bool correctContext() const { return contextMatcher(owner, context); }
bool operator<(const ShortcutEntry &f) const { return keyseq < f.keyseq; }
QKeySequence keyseq;
Qt::ShortcutContext context;
bool enabled : 1;
bool autorepeat : 1;
signed int id;
QObject *owner;
ShortcutMap::ContextMatcher contextMatcher;
};
/* \internal
Private data for ShortcutMap
*/
class ShortcutMapPrivate
{
Q_DECLARE_PUBLIC(ShortcutMap)
public:
ShortcutMapPrivate(ShortcutMap *parent)
: q_ptr(parent)
, currentId(0)
, ambigCount(0)
, currentState(QKeySequence::NoMatch)
{
identicals.reserve(10);
currentSequences.reserve(10);
}
ShortcutMap *q_ptr; // Private's parent
QList<ShortcutEntry> sequences; // All sequences!
int currentId; // Global shortcut ID number
int ambigCount; // Index of last enabled ambiguous dispatch
QKeySequence::SequenceMatch currentState;
QList<QKeySequence> currentSequences; // Sequence for the current state
QList<QKeySequence> newEntries;
QKeySequence prevSequence; // Sequence for the previous identical match
QList<const ShortcutEntry *> identicals; // Last identical matches
};
/*! \internal
ShortcutMap constructor.
*/
ShortcutMap::ShortcutMap()
: d_ptr(new ShortcutMapPrivate(this))
{
resetState();
}
/*! \internal
ShortcutMap destructor.
*/
ShortcutMap::~ShortcutMap() {}
/*! \internal
Adds a shortcut to the global map.
Returns the id of the newly added shortcut.
*/
int ShortcutMap::addShortcut(QObject *owner,
const QKeySequence &key,
Qt::ShortcutContext context,
ContextMatcher matcher)
{
Q_ASSERT_X(owner, "ShortcutMap::addShortcut", "All shortcuts need an owner");
Q_ASSERT_X(!key.isEmpty(), "ShortcutMap::addShortcut", "Cannot add keyless shortcuts to map");
Q_D(ShortcutMap);
ShortcutEntry newEntry(owner, key, context, --(d->currentId), true, matcher);
const auto it = std::upper_bound(d->sequences.begin(), d->sequences.end(), newEntry);
d->sequences.insert(it, newEntry); // Insert sorted
qCDebug(lcShortcutMap).nospace() << "ShortcutMap::addShortcut(" << owner << ", " << key << ", "
<< context << ") added shortcut with ID " << d->currentId;
return d->currentId;
}
/*! \internal
Removes a shortcut from the global map.
If \a owner is \nullptr, all entries in the map with the key sequence specified
is removed. If \a key is null, all sequences for \a owner is removed from
the map. If \a id is 0, any identical \a key sequences owned by \a owner
are removed.
Returns the number of sequences removed from the map.
*/
int ShortcutMap::removeShortcut(int id, QObject *owner, const QKeySequence &key)
{
Q_D(ShortcutMap);
int itemsRemoved = 0;
bool allOwners = (owner == nullptr);
bool allKeys = key.isEmpty();
bool allIds = id == 0;
auto debug = qScopeGuard([&]() {
qCDebug(lcShortcutMap).nospace()
<< "ShortcutMap::removeShortcut(" << id << ", " << owner << ", " << key << ") removed "
<< itemsRemoved << " shortcuts(s)";
});
// Special case, remove everything
if (allOwners && allKeys && allIds) {
itemsRemoved = d->sequences.size();
d->sequences.clear();
return itemsRemoved;
}
int i = d->sequences.size() - 1;
while (i >= 0) {
const ShortcutEntry &entry = d->sequences.at(i);
int entryId = entry.id;
if ((allOwners || entry.owner == owner) && (allIds || entry.id == id)
&& (allKeys || entry.keyseq == key)) {
d->sequences.removeAt(i);
++itemsRemoved;
}
if (id == entryId)
return itemsRemoved;
--i;
}
return itemsRemoved;
}
/*! \internal
Resets the state of the statemachine to NoMatch
*/
void ShortcutMap::resetState()
{
Q_D(ShortcutMap);
d->currentState = QKeySequence::NoMatch;
clearSequence(d->currentSequences);
}
/*! \internal
Returns the current state of the statemachine
*/
QKeySequence::SequenceMatch ShortcutMap::state()
{
Q_D(ShortcutMap);
return d->currentState;
}
/*! \internal
Uses nextState(QKeyEvent) to check for a grabbed shortcut.
If so, it is dispatched using dispatchEvent().
Returns true if a shortcut handled the event.
\sa nextState, dispatchEvent
*/
bool ShortcutMap::tryShortcut(QKeyEvent *e)
{
Q_D(ShortcutMap);
if (e->key() == Qt::Key_unknown)
return false;
QKeySequence::SequenceMatch previousState = state();
switch (nextState(e)) {
case QKeySequence::NoMatch:
// In the case of going from a partial match to no match we handled the
// event, since we already stated that we did for the partial match. But
// in the normal case of directly going to no match we say we didn't.
return previousState == QKeySequence::PartialMatch;
case QKeySequence::PartialMatch:
// For a partial match we don't know yet if we will handle the shortcut
// but we need to say we did, so that we get the follow-up key-presses.
return true;
case QKeySequence::ExactMatch: {
// Save number of identical matches before dispatching
// to keep ShortcutMap and tryShortcut reentrant.
const int identicalMatches = d->identicals.size();
resetState();
dispatchEvent(e);
// If there are no identicals we've only found disabled shortcuts, and
// shouldn't say that we handled the event.
return identicalMatches > 0;
}
}
Q_UNREACHABLE_RETURN(false);
}
/*! \internal
Returns the next state of the statemachine
If return value is SequenceMatch::ExactMatch, then a call to matches()
will return a QObjects* list of all matching objects for the last matching
sequence.
*/
QKeySequence::SequenceMatch ShortcutMap::nextState(QKeyEvent *e)
{
Q_D(ShortcutMap);
// Modifiers can NOT be shortcuts...
if (e->key() >= Qt::Key_Shift && e->key() <= Qt::Key_ScrollLock)
return d->currentState;
QKeySequence::SequenceMatch result = QKeySequence::NoMatch;
// We start fresh each time..
d->identicals.clear();
result = find(e);
if (result == QKeySequence::NoMatch && (e->modifiers() & Qt::KeypadModifier)) {
// Try to find a match without keypad modifier
result = find(e, Qt::KeypadModifier);
}
if (result == QKeySequence::NoMatch && e->modifiers() & Qt::ShiftModifier) {
// If Shift + Key_Backtab, also try Shift + Qt::Key_Tab
if (e->key() == Qt::Key_Backtab) {
QKeyEvent pe = QKeyEvent(e->type(), Qt::Key_Tab, e->modifiers(), e->text());
result = find(&pe);
}
}
// Does the new state require us to clean up?
if (result == QKeySequence::NoMatch)
clearSequence(d->currentSequences);
d->currentState = result;
qCDebug(lcShortcutMap).nospace() << "ShortcutMap::nextState(" << e << ") = " << result;
return result;
}
/*! \internal
Determines if an enabled shortcut has a matching key sequence.
*/
bool ShortcutMap::hasShortcutForKeySequence(const QKeySequence &seq) const
{
Q_D(const ShortcutMap);
ShortcutEntry entry(seq); // needed for searching
const auto itEnd = d->sequences.cend();
auto it = std::lower_bound(d->sequences.cbegin(), itEnd, entry);
for (; it != itEnd; ++it) {
if (matches(entry.keyseq, (*it).keyseq) == QKeySequence::ExactMatch
&& (*it).correctContext() && (*it).enabled) {
return true;
}
}
//end of the loop: we didn't find anything
return false;
}
/*! \internal
Returns the next state of the statemachine, based
on the new key event \a e.
Matches are appended to the list of identicals,
which can be access through matches().
\sa matches
*/
QKeySequence::SequenceMatch ShortcutMap::find(QKeyEvent *e, int ignoredModifiers)
{
Q_D(ShortcutMap);
if (!d->sequences.size())
return QKeySequence::NoMatch;
createNewSequences(e, d->newEntries, ignoredModifiers);
qCDebug(lcShortcutMap) << "Possible shortcut key sequences:" << d->newEntries;
// Should never happen
if (d->newEntries == d->currentSequences) {
Q_ASSERT_X(e->key() != Qt::Key_unknown || e->text().size(),
"ShortcutMap::find",
"New sequence to find identical to previous");
return QKeySequence::NoMatch;
}
// Looking for new identicals, scrap old
d->identicals.clear();
bool partialFound = false;
bool identicalDisabledFound = false;
QList<QKeySequence> okEntries;
int result = QKeySequence::NoMatch;
for (int i = d->newEntries.size() - 1; i >= 0; --i) {
ShortcutEntry entry(d->newEntries.at(i)); // needed for searching
qCDebug(lcShortcutMap) << "- checking entry" << entry.id << entry.keyseq;
const auto itEnd = d->sequences.constEnd();
auto it = std::lower_bound(d->sequences.constBegin(), itEnd, entry);
int oneKSResult = QKeySequence::NoMatch;
int tempRes = QKeySequence::NoMatch;
do {
if (it == itEnd)
break;
tempRes = matches(entry.keyseq, (*it).keyseq);
oneKSResult = qMax(oneKSResult, tempRes);
qCDebug(lcShortcutMap) << " - matches returned" << tempRes << "for" << entry.keyseq
<< it->keyseq << "- correctContext()?" << it->correctContext();
if (tempRes != QKeySequence::NoMatch && (*it).correctContext()) {
if (tempRes == QKeySequence::ExactMatch) {
if ((*it).enabled)
d->identicals.append(&*it);
else
identicalDisabledFound = true;
} else if (tempRes == QKeySequence::PartialMatch) {
// We don't need partials, if we have identicals
if (d->identicals.size())
break;
// We only care about enabled partials, so we don't consume
// key events when all partials are disabled!
partialFound |= (*it).enabled;
}
}
++it;
// If we got a valid match on this run, there might still be more keys to check against,
// so we'll loop once more. If we get NoMatch, there's guaranteed no more possible
// matches in the shortcutmap.
} while (tempRes != QKeySequence::NoMatch);
// If the type of match improves (ergo, NoMatch->Partial, or Partial->Exact), clear the
// previous list. If this match is equal or better than the last match, append to the list
if (oneKSResult > result) {
okEntries.clear();
qCDebug(lcShortcutMap)
<< "Found better match (" << d->newEntries << "), clearing key sequence list";
}
if (oneKSResult && oneKSResult >= result) {
okEntries << d->newEntries.at(i);
qCDebug(lcShortcutMap) << "Added ok key sequence" << d->newEntries;
}
}
if (d->identicals.size()) {
result = QKeySequence::ExactMatch;
} else if (partialFound) {
result = QKeySequence::PartialMatch;
} else if (identicalDisabledFound) {
result = QKeySequence::ExactMatch;
} else {
clearSequence(d->currentSequences);
result = QKeySequence::NoMatch;
}
if (result != QKeySequence::NoMatch)
d->currentSequences = okEntries;
qCDebug(lcShortcutMap) << "Returning shortcut match == " << result;
return QKeySequence::SequenceMatch(result);
}
/*! \internal
Clears \a seq to an empty QKeySequence.
Same as doing (the slower)
\snippet code/src_gui_kernel_shortcutmap.cpp 0
*/
void ShortcutMap::clearSequence(QList<QKeySequence> &ksl)
{
ksl.clear();
d_func()->newEntries.clear();
}
static QList<int> extractKeyFromEvent(QKeyEvent *e)
{
QList<int> result;
if (e->key() && (e->key() != Qt::Key_unknown))
result << e->keyCombination().toCombined();
else if (!e->text().isEmpty())
result << int(e->text().at(0).unicode() + (int) e->modifiers());
return result;
}
/*! \internal
Alters \a seq to the new sequence state, based on the
current sequence state, and the new key event \a e.
*/
void ShortcutMap::createNewSequences(QKeyEvent *e, QList<QKeySequence> &ksl, int ignoredModifiers)
{
Q_D(ShortcutMap);
QList<int> possibleKeys = extractKeyFromEvent(e);
qCDebug(lcShortcutMap) << "Creating new sequences for" << e
<< "with ignoredModifiers=" << Qt::KeyboardModifiers(ignoredModifiers);
int pkTotal = possibleKeys.size();
if (!pkTotal)
return;
int ssActual = d->currentSequences.size();
int ssTotal = qMax(1, ssActual);
// Resize to possible permutations of the current sequence(s).
ksl.resize(pkTotal * ssTotal);
int index = ssActual ? d->currentSequences.at(0).count() : 0;
for (int pkNum = 0; pkNum < pkTotal; ++pkNum) {
for (int ssNum = 0; ssNum < ssTotal; ++ssNum) {
int i = (pkNum * ssTotal) + ssNum;
QKeySequence &curKsl = ksl[i];
if (ssActual) {
const QKeySequence &curSeq = d->currentSequences.at(ssNum);
curKsl = QKeySequence(curSeq[0], curSeq[1], curSeq[2], curSeq[3]);
} else {
curKsl = QKeySequence(QKeyCombination::fromCombined(0));
}
std::array<QKeyCombination, 4> cur = {curKsl[0], curKsl[1], curKsl[2], curKsl[3]};
cur[index] = QKeyCombination::fromCombined(possibleKeys.at(pkNum) & ~ignoredModifiers);
curKsl = QKeySequence(cur[0], cur[1], cur[2], cur[3]);
}
}
}
/*! \internal
Basically the same function as QKeySequence::matches(const QKeySequence &seq) const
only that is specially handles Key_hyphen as Key_Minus, as people mix these up all the time and
they conceptually the same.
*/
QKeySequence::SequenceMatch ShortcutMap::matches(const QKeySequence &seq1,
const QKeySequence &seq2) const
{
uint userN = seq1.count(), seqN = seq2.count();
if (userN > seqN)
return QKeySequence::NoMatch;
// If equal in length, we have a potential ExactMatch sequence,
// else we already know it can only be partial.
QKeySequence::SequenceMatch match = (userN == seqN ? QKeySequence::ExactMatch
: QKeySequence::PartialMatch);
for (uint i = 0; i < userN; ++i) {
int userKey = seq1[i].toCombined(), sequenceKey = seq2[i].toCombined();
if ((userKey & Qt::Key_unknown) == Qt::Key_hyphen)
userKey = (userKey & Qt::KeyboardModifierMask) | Qt::Key_Minus;
if ((sequenceKey & Qt::Key_unknown) == Qt::Key_hyphen)
sequenceKey = (sequenceKey & Qt::KeyboardModifierMask) | Qt::Key_Minus;
if (userKey != sequenceKey)
return QKeySequence::NoMatch;
}
return match;
}
/*! \internal
Returns the list of ShortcutEntry's matching the last Identical state.
*/
QList<const ShortcutEntry *> ShortcutMap::matches() const
{
Q_D(const ShortcutMap);
return d->identicals;
}
/*! \internal
Dispatches QShortcutEvents to widgets who grabbed the matched key sequence.
*/
void ShortcutMap::dispatchEvent(QKeyEvent *e)
{
Q_D(ShortcutMap);
if (!d->identicals.size())
return;
const QKeySequence &curKey = d->identicals.at(0)->keyseq;
if (d->prevSequence != curKey) {
d->ambigCount = 0;
d->prevSequence = curKey;
}
// Find next
const ShortcutEntry *current = nullptr, *next = nullptr;
int i = 0, enabledShortcuts = 0;
QList<const ShortcutEntry *> ambiguousShortcuts;
while (i < d->identicals.size()) {
current = d->identicals.at(i);
if (current->enabled || !next) {
++enabledShortcuts;
if (lcShortcutMap().isDebugEnabled())
ambiguousShortcuts.append(current);
if (enabledShortcuts > d->ambigCount + 1)
break;
next = current;
}
++i;
}
d->ambigCount = (d->identicals.size() == i ? 0 : d->ambigCount + 1);
// Don't trigger shortcut if we're autorepeating and the shortcut is
// grabbed with not accepting autorepeats.
if (!next || (e->isAutoRepeat() && !next->autorepeat))
return;
// Dispatch next enabled
if (lcShortcutMap().isDebugEnabled()) {
if (ambiguousShortcuts.size() > 1) {
qCDebug(lcShortcutMap)
<< "The following shortcuts are about to be activated ambiguously:";
for (const ShortcutEntry *entry : std::as_const(ambiguousShortcuts))
qCDebug(lcShortcutMap).nospace()
<< "- " << entry->keyseq << " (belonging to " << entry->owner << ")";
}
qCDebug(lcShortcutMap).nospace()
<< "ShortcutMap::dispatchEvent(): Sending QShortcutEvent(\"" << next->keyseq.toString()
<< "\", " << next->id << ", " << static_cast<bool>(enabledShortcuts > 1)
<< ") to object(" << next->owner << ')';
}
QShortcutEvent se(next->keyseq, next->id, enabledShortcuts > 1);
QCoreApplication::sendEvent(const_cast<QObject *>(next->owner), &se);
}
} // namespace Terminal::Internal

View File

@@ -0,0 +1,52 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
// COPIED FROM shortcutmap_p.h
#pragma once
#include <QKeySequence>
#include <QObject>
class QKeyEvent;
class QObject;
namespace Terminal::Internal {
struct ShortcutEntry;
class ShortcutMapPrivate;
class ShortcutMap
{
Q_DECLARE_PRIVATE(ShortcutMap)
public:
ShortcutMap();
~ShortcutMap();
typedef bool (*ContextMatcher)(QObject *object, Qt::ShortcutContext context);
int addShortcut(QObject *owner,
const QKeySequence &key,
Qt::ShortcutContext context,
ContextMatcher matcher);
int removeShortcut(int id, QObject *owner, const QKeySequence &key = QKeySequence());
QKeySequence::SequenceMatch state();
bool tryShortcut(QKeyEvent *e);
bool hasShortcutForKeySequence(const QKeySequence &seq) const;
private:
void resetState();
QKeySequence::SequenceMatch nextState(QKeyEvent *e);
void dispatchEvent(QKeyEvent *e);
QKeySequence::SequenceMatch find(QKeyEvent *e, int ignoredModifiers = 0);
QKeySequence::SequenceMatch matches(const QKeySequence &seq1, const QKeySequence &seq2) const;
QList<const ShortcutEntry *> matches() const;
void createNewSequences(QKeyEvent *e, QList<QKeySequence> &ksl, int ignoredModifiers);
void clearSequence(QList<QKeySequence> &ksl);
QScopedPointer<ShortcutMapPrivate> d_ptr;
};
} // namespace Terminal::Internal

View File

@@ -21,6 +21,8 @@ QtcPlugin {
"shellmodel.h",
"shellintegration.cpp",
"shellintegration.h",
"shortcutmap.cpp",
"shortcutmap.h",
"terminal.qrc",
"terminalconstants.h",
"terminalicons.h",

View File

@@ -4,6 +4,7 @@
#include "terminalpane.h"
#include "shellmodel.h"
#include "shortcutmap.h"
#include "terminalconstants.h"
#include "terminalicons.h"
#include "terminalsettings.h"
@@ -36,9 +37,9 @@ using namespace Core;
TerminalPane::TerminalPane(QObject *parent)
: IOutputPane(parent)
, m_context("Terminal.Pane", Core::Constants::C_GLOBAL_CUTOFF)
, m_selfContext("Terminal.Pane")
{
setupContext(m_context, &m_tabWidget);
setupContext(m_selfContext, &m_tabWidget);
setZoomButtonsEnabled(true);
connect(this, &IOutputPane::zoomInRequested, this, [this] {
@@ -52,6 +53,9 @@ TerminalPane::TerminalPane(QObject *parent)
initActions();
m_lockKeyboardButton = new QToolButton();
m_lockKeyboardButton->setDefaultAction(&lockKeyboard);
m_newTerminalButton = new QToolButton();
m_newTerminalButton->setDefaultAction(&newTerminal);
@@ -124,6 +128,14 @@ void TerminalPane::openTerminal(const OpenTerminalParameters &parameters)
}
const auto terminalWidget = new TerminalWidget(&m_tabWidget, parametersCopy);
using namespace Constants;
terminalWidget->unlockGlobalAction("Coreplugin.OutputPane.minmax");
terminalWidget->unlockGlobalAction(Core::Constants::LOCATE);
terminalWidget->unlockGlobalAction(NEWTERMINAL);
terminalWidget->unlockGlobalAction(NEXTTERMINAL);
terminalWidget->unlockGlobalAction(PREVTERMINAL);
m_tabWidget.setCurrentIndex(m_tabWidget.addTab(terminalWidget, Tr::tr("Terminal")));
setupTerminalWidget(terminalWidget);
@@ -229,6 +241,23 @@ void TerminalPane::initActions()
{
createShellMenu();
lockKeyboard.setCheckable(true);
lockKeyboard.setChecked(TerminalSettings::instance().lockKeyboard());
auto updateLockKeyboard = [this](bool locked) {
TerminalSettings::instance().lockKeyboard.setValue(locked);
if (locked) {
lockKeyboard.setIcon(Icons::LOCKED_TOOLBAR.icon());
lockKeyboard.setToolTip(Tr::tr("Keyboard shortcuts will be send to the Terminal"));
} else {
lockKeyboard.setIcon(Icons::UNLOCKED_TOOLBAR.icon());
lockKeyboard.setToolTip(Tr::tr("Keyboard shortcuts will be send to Qt Creator"));
}
};
updateLockKeyboard(TerminalSettings::instance().lockKeyboard());
connect(&lockKeyboard, &QAction::toggled, this, updateLockKeyboard);
newTerminal.setText(Tr::tr("New Terminal"));
newTerminal.setIcon(NEW_TERMINAL_ICON.icon());
newTerminal.setToolTip(Tr::tr("Create a new Terminal."));
@@ -242,25 +271,22 @@ void TerminalPane::initActions()
using namespace Constants;
ActionManager::registerAction(&newTerminal, NEWTERMINAL, m_context)
->setDefaultKeySequences({QKeySequence(
HostOsInfo::isMacHost() ? QLatin1String("Ctrl+T") : QLatin1String("Ctrl+Shift+T"))});
Command *cmd = ActionManager::registerAction(&newTerminal, NEWTERMINAL, m_selfContext);
cmd->setDefaultKeySequences({QKeySequence(
HostOsInfo::isMacHost() ? QLatin1String("Ctrl+T") : QLatin1String("Ctrl+Shift+T"))});
ActionManager::registerAction(&nextTerminal, NEXTTERMINAL, m_context)
ActionManager::registerAction(&nextTerminal, NEXTTERMINAL, m_selfContext)
->setDefaultKeySequences(
{QKeySequence("Alt+Tab"),
QKeySequence(HostOsInfo::isMacHost() ? QLatin1String("Ctrl+Shift+[")
: QLatin1String("Ctrl+PgUp"))});
ActionManager::registerAction(&prevTerminal, PREVTERMINAL, m_context)
ActionManager::registerAction(&prevTerminal, PREVTERMINAL, m_selfContext)
->setDefaultKeySequences(
{QKeySequence("Alt+Shift+Tab"),
QKeySequence(HostOsInfo::isMacHost() ? QLatin1String("Ctrl+Shift+]")
: QLatin1String("Ctrl+PgDown"))});
m_minMax = TerminalWidget::unlockGlobalAction("Coreplugin.OutputPane.minmax", m_context);
m_locate = TerminalWidget::unlockGlobalAction(Core::Constants::LOCATE, m_context);
connect(&newTerminal, &QAction::triggered, this, [this] { openTerminal({}); });
connect(&closeTerminal, &QAction::triggered, this, [this] {
removeTab(m_tabWidget.currentIndex());
@@ -303,10 +329,11 @@ void TerminalPane::createShellMenu()
QList<QWidget *> TerminalPane::toolBarWidgets() const
{
QList<QWidget *> widgets = IOutputPane::toolBarWidgets();
widgets.prepend(m_newTerminalButton);
widgets.prepend(m_closeTerminalButton);
return widgets << m_openSettingsButton << m_escSettingButton;
return widgets << m_openSettingsButton << m_lockKeyboardButton << m_escSettingButton;
}
QString TerminalPane::displayName() const

View File

@@ -62,18 +62,17 @@ private:
QToolButton *m_closeTerminalButton{nullptr};
QToolButton *m_openSettingsButton{nullptr};
QToolButton *m_escSettingButton{nullptr};
UnlockedGlobalAction m_minMax;
UnlockedGlobalAction m_locate;
QToolButton *m_lockKeyboardButton{nullptr};
QAction newTerminal;
QAction nextTerminal;
QAction prevTerminal;
QAction closeTerminal;
QAction lockKeyboard;
QMenu m_shellMenu;
Core::Context m_context;
Core::Context m_selfContext;
bool m_widgetInitialized{false};
bool m_isVisible{false};

View File

@@ -32,6 +32,7 @@ public:
Utils::BoolAspect sendEscapeToTerminal{this};
Utils::BoolAspect audibleBell{this};
Utils::BoolAspect lockKeyboard{this};
};
} // Terminal

View File

@@ -252,10 +252,37 @@ void TerminalWidget::setupColors()
update();
}
static RegisteredAction registerAction(Id commandId, const Context &context)
static bool contextMatcher(QObject *, Qt::ShortcutContext)
{
return true;
}
void TerminalWidget::registerShortcut(Command *cmd)
{
QTC_ASSERT(cmd, return);
auto addShortCut = [this, cmd] {
for (const auto &keySequence : cmd->keySequences()) {
m_shortcutMap.addShortcut(cmd->action(),
keySequence,
Qt::ShortcutContext::WindowShortcut,
contextMatcher);
}
};
auto removeShortCut = [this, cmd] { m_shortcutMap.removeShortcut(0, cmd->action()); };
addShortCut();
connect(cmd, &Command::keySequenceChanged, this, [addShortCut, removeShortCut]() {
removeShortCut();
addShortCut();
});
}
RegisteredAction TerminalWidget::registerAction(Id commandId, const Context &context)
{
QAction *action = new QAction;
ActionManager::registerAction(action, commandId, context);
Command *cmd = ActionManager::registerAction(action, commandId, context);
registerShortcut(cmd);
return RegisteredAction(action, [commandId](QAction *a) {
ActionManager::unregisterAction(a, commandId);
@@ -287,10 +314,10 @@ void TerminalWidget::setupActions()
this,
&TerminalWidget::moveCursorWordRight);
m_exit = unlockGlobalAction(Core::Constants::EXIT, m_context);
m_options = unlockGlobalAction(Core::Constants::OPTIONS, m_context);
m_settings = unlockGlobalAction("Preferences.Terminal.General", m_context);
m_findInDocument = unlockGlobalAction(Core::Constants::FIND_IN_DOCUMENT, m_context);
unlockGlobalAction(Core::Constants::EXIT);
unlockGlobalAction(Core::Constants::OPTIONS);
unlockGlobalAction("Preferences.Terminal.General");
unlockGlobalAction(Core::Constants::FIND_IN_DOCUMENT);
}
void TerminalWidget::closeTerminal()
@@ -1506,6 +1533,11 @@ void TerminalWidget::showEvent(QShowEvent *event)
bool TerminalWidget::event(QEvent *event)
{
if (TerminalSettings::instance().lockKeyboard() && event->type() == QEvent::ShortcutOverride) {
event->accept();
return true;
}
if (event->type() == QEvent::Paint) {
QPainter p(this);
p.fillRect(QRect(QPoint(0, 0), size()), m_currentColors[ColorIndex::Background]);
@@ -1513,12 +1545,16 @@ bool TerminalWidget::event(QEvent *event)
}
if (event->type() == QEvent::KeyPress) {
QKeyEvent *k = (QKeyEvent *) event;
auto k = static_cast<QKeyEvent *>(event);
if (TerminalSettings::instance().lockKeyboard() && m_shortcutMap.tryShortcut(k))
return true;
keyPressEvent(k);
return true;
}
if (event->type() == QEvent::KeyRelease) {
QKeyEvent *k = (QKeyEvent *) event;
auto k = static_cast<QKeyEvent *>(event);
keyReleaseEvent(k);
return true;
}
@@ -1565,21 +1601,11 @@ void TerminalWidget::initActions()
ActionManager::registerAction(&clearTerminal, Constants::CLEAR_TERMINAL, context);
}
UnlockedGlobalAction TerminalWidget::unlockGlobalAction(const Utils::Id &commandId,
const Context &context)
void TerminalWidget::unlockGlobalAction(const Utils::Id &commandId)
{
QAction *srcAction = ActionManager::command(commandId)->actionForContext(
Core::Constants::C_GLOBAL);
ProxyAction *proxy = ProxyAction::proxyActionWithIcon(srcAction, srcAction->icon());
ActionManager::registerAction(proxy, commandId, context);
UnlockedGlobalAction registeredAction(proxy, [commandId](QAction *a) {
ActionManager::unregisterAction(a, commandId);
delete a;
});
return registeredAction;
Command *cmd = ActionManager::command(commandId);
QTC_ASSERT(cmd, return);
registerShortcut(cmd);
}
} // namespace Terminal

View File

@@ -3,12 +3,14 @@
#pragma once
#include "shortcutmap.h"
#include "terminalsearch.h"
#include "terminalsurface.h"
#include <aggregation/aggregate.h>
#include <coreplugin/icontext.h>
#include <coreplugin/actionmanager/command.h>
#include <utils/link.h>
#include <utils/process.h>
@@ -24,7 +26,6 @@
namespace Terminal {
using UnlockedGlobalAction = std::unique_ptr<QAction, std::function<void(QAction *)>>;
using RegisteredAction = std::unique_ptr<QAction, std::function<void(QAction *)>>;
class TerminalWidget : public QAbstractScrollArea
@@ -91,8 +92,7 @@ public:
static void initActions();
[[nodiscard]] static UnlockedGlobalAction unlockGlobalAction(const Utils::Id &commandId,
const Core::Context &context);
void unlockGlobalAction(const Utils::Id &commandId);
signals:
void started(qint64 pid);
@@ -189,6 +189,9 @@ protected:
void updateCopyState();
RegisteredAction registerAction(Utils::Id commandId, const Core::Context &context);
void registerShortcut(Core::Command *command);
private:
Core::Context m_context;
std::unique_ptr<Utils::Process> m_process;
@@ -248,10 +251,7 @@ private:
RegisteredAction m_moveCursorWordRight;
RegisteredAction m_close;
UnlockedGlobalAction m_findInDocument;
UnlockedGlobalAction m_exit;
UnlockedGlobalAction m_options;
UnlockedGlobalAction m_settings;
Internal::ShortcutMap m_shortcutMap;
};
} // namespace Terminal