Commit 0f249ec6 authored by Pablo Rauzy's avatar Pablo Rauzy Committed by Christoph Cullmann
Browse files

KeyboardMacros Plugin v1.0

parent 6bb69ed2
......@@ -6,7 +6,9 @@
#include "keyboardmacrosplugin.h"
#include <QAction>
#include <QApplication>
#include <QCoreApplication>
#include <QDateTime>
#include <QKeyEvent>
#include <QKeySequence>
#include <QList>
......@@ -21,24 +23,19 @@
#include <KPluginFactory>
#include <KXMLGUIFactory>
#include <iostream>
#include <qevent.h>
K_PLUGIN_FACTORY_WITH_JSON(KeyboardMacrosPluginFactory, "keyboardmacrosplugin.json", registerPlugin<KeyboardMacrosPlugin>();)
KeyboardMacrosPlugin::KeyboardMacrosPlugin(QObject *parent, const QList<QVariant> &)
: KTextEditor::Plugin(parent)
{
// register "recmac" and "runmac" commands
m_recCommand = new KeyboardMacrosPluginRecordCommand(this);
m_runCommand = new KeyboardMacrosPluginRunCommand(this);
}
KeyboardMacrosPlugin::~KeyboardMacrosPlugin()
{
delete m_recCommand;
delete m_runCommand;
reset();
qDeleteAll(m_tape.begin(), m_tape.end());
m_tape.clear();
qDeleteAll(m_macro.begin(), m_macro.end());
m_macro.clear();
}
QObject *KeyboardMacrosPlugin::createView(KTextEditor::MainWindow *mainWindow)
......@@ -47,112 +44,128 @@ QObject *KeyboardMacrosPlugin::createView(KTextEditor::MainWindow *mainWindow)
return new KeyboardMacrosPluginView(this, mainWindow);
}
// https://doc.qt.io/qt-6/eventsandfilters.html
// https://doc.qt.io/qt-6/qobject.html#installEventFilter
// https://stackoverflow.com/questions/41631011/my-qt-eventfilter-doesnt-stop-events-as-it-should
// file:///usr/share/qt5/doc/qtcore/qobject.html#installEventFilter
// file:///usr/share/qt5/doc/qtcore/qcoreapplication.html#sendEvent
// also see postEvent, sendPostedEvents, etc
void KeyboardMacrosPlugin::sendMessage(const QString &text, bool error)
{
QVariantMap genericMessage;
genericMessage.insert(QStringLiteral("type"), error ? QStringLiteral("Error") : QStringLiteral("Info"));
genericMessage.insert(QStringLiteral("category"), i18n("Macros"));
genericMessage.insert(QStringLiteral("categoryIcon"), QIcon::fromTheme(QStringLiteral("input-keyboard")));
genericMessage.insert(QStringLiteral("text"), text);
Q_EMIT message(genericMessage);
}
bool KeyboardMacrosPlugin::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
QKeyEvent *keyEvent = new QKeyEvent(*static_cast<QKeyEvent *>(event));
QKeySequence s(keyEvent->key() | keyEvent->modifiers());
qDebug("KeySeq: %s", s.toString().toUtf8().data());
m_keyEvents.append(keyEvent);
return true;
// FIXME: this should let the event pass through by returning false
// but also capture keypress only once and only the relevant ones
// (e.g., if pressing ctrl then c before releasing only capture ctrl+c)
// Update which widget we filter events from if the focus has changed
m_focusWidget->removeEventFilter(this);
m_focusWidget = qApp->focusWidget();
m_focusWidget->installEventFilter(this);
// We only spy on keyboard events so we only need to check ShortcutOverride and return false
if (event->type() == QEvent::ShortcutOverride) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
// if only modifiers are pressed, we don't care
switch (keyEvent->key()) {
case Qt::Key_Shift:
case Qt::Key_Control:
case Qt::Key_Alt:
case Qt::Key_Meta:
case Qt::Key_AltGr:
return false;
}
// we don't want to record the shortcut for recording
if (m_recordAction->shortcut().matches(QKeySequence(keyEvent->key() | keyEvent->modifiers())) == QKeySequence::ExactMatch) {
return false;
}
// otherwise we add the keyboard event to the macro
m_tape.append(new QKeyEvent(QEvent::KeyPress, keyEvent->key(), keyEvent->modifiers(), keyEvent->text()));
m_tape.append(new QKeyEvent(QEvent::KeyRelease, keyEvent->key(), keyEvent->modifiers(), keyEvent->text()));
return false;
} else {
return QObject::eventFilter(obj, event);
}
}
void KeyboardMacrosPlugin::reset()
void KeyboardMacrosPlugin::record()
{
qDeleteAll(m_keyEvents.begin(), m_keyEvents.end());
m_keyEvents.clear();
// start recording
qDebug("[KeyboardMacrosPlugin] start recording");
m_focusWidget = qApp->focusWidget();
m_focusWidget->installEventFilter(this);
m_recording = true;
m_recordAction->setText(i18n("End Macro &Recording"));
m_cancelAction->setEnabled(true);
}
void KeyboardMacrosPlugin::stop(bool save)
{
// stop recording
qDebug("[KeyboardMacrosPlugin] %s recording", save ? "end" : "cancel");
m_focusWidget->removeEventFilter(this);
m_recording = false;
if (save) {
// delete current macro
qDeleteAll(m_macro.begin(), m_macro.end());
m_macro.clear();
// replace it with the tape
m_macro.swap(m_tape);
// clear tape
m_tape.clear();
m_playAction->setEnabled(!m_macro.isEmpty());
} else { // cancel
// delete tape
qDeleteAll(m_tape.begin(), m_tape.end());
m_tape.clear();
}
m_recordAction->setText(i18n("&Record Macro..."));
m_cancelAction->setEnabled(false);
}
bool KeyboardMacrosPlugin::record(KTextEditor::View *)
void KeyboardMacrosPlugin::cancel()
{
if (m_recording) { // end recording
// KTextEditor::Editor::instance()->application()->activeMainWindow()->window()->removeEventFilter(this);
QCoreApplication::instance()->removeEventFilter(this);
std::cerr << "stop recording" << std::endl;
m_recording = false;
return true; // if success
}
// first reset ...
reset();
// TODO (after first working release):
// either allow to record multiple macros with names (to pass to the runmac command)
// and/or have at least two slots to be able to cancel a recording and get the previously
// recorded macro back as the current one.
// ... then start recording
std::cerr << "start recording" << std::endl;
QCoreApplication::instance()->installEventFilter(this);
m_recording = true;
return true;
stop(false);
}
bool KeyboardMacrosPlugin::run(KTextEditor::View *view)
bool KeyboardMacrosPlugin::play()
{
if (m_recording) {
// end recording before running macro
record(view);
if (m_macro.isEmpty()) {
return false;
}
if (!m_keyEvents.isEmpty()) {
QList<QKeyEvent *>::ConstIterator it;
for (it = m_keyEvents.constBegin(); it != m_keyEvents.constEnd(); it++) {
QKeyEvent *keyEvent = *it;
QKeySequence s(keyEvent->key() | keyEvent->modifiers());
qDebug("KeySeq: %s", s.toString().toUtf8().data());
QCoreApplication::sendEvent(QCoreApplication::instance(), keyEvent);
// FIXME: the above doesn't work
}
Macro::Iterator it;
for (it = m_macro.begin(); it != m_macro.end(); it++) {
QKeyEvent *keyEvent = *it;
QKeySequence s(keyEvent->key() | keyEvent->modifiers());
keyEvent->setAccepted(false);
qApp->sendEvent(qApp->focusWidget(), keyEvent);
}
return true;
}
bool KeyboardMacrosPlugin::isRecording()
void KeyboardMacrosPlugin::slotRecord()
{
return m_recording;
if (m_recording) {
stop(true);
} else {
record();
}
}
void KeyboardMacrosPlugin::slotRecord()
void KeyboardMacrosPlugin::slotPlay()
{
if (!KTextEditor::Editor::instance()->application()->activeMainWindow()) {
return;
if (m_recording) {
stop(true);
}
KTextEditor::View *view(KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView());
if (!view) {
return;
if (!play()) {
sendMessage(i18n("Macro is empty."), false);
}
record(view);
}
void KeyboardMacrosPlugin::slotRun()
void KeyboardMacrosPlugin::slotCancel()
{
if (!KTextEditor::Editor::instance()->application()->activeMainWindow()) {
return;
}
KTextEditor::View *view(KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView());
if (!view) {
if (!m_recording) {
return;
}
run(view);
cancel();
}
// BEGIN Plugin view to add our actions to the gui
......@@ -166,18 +179,26 @@ KeyboardMacrosPluginView::KeyboardMacrosPluginView(KeyboardMacrosPlugin *plugin,
setXMLFile(QStringLiteral("ui.rc"));
// create record action
QAction *rec = actionCollection()->addAction(QStringLiteral("record_macro"));
rec->setText(i18n("Record &Macro..."));
QAction *rec = actionCollection()->addAction(QStringLiteral("keyboardmacros_record"));
rec->setText(i18n("&Record Macro..."));
actionCollection()->setDefaultShortcut(rec, Qt::CTRL | Qt::SHIFT | Qt::Key_K);
connect(rec, &QAction::triggered, plugin, &KeyboardMacrosPlugin::slotRecord);
// create run action
QAction *run = actionCollection()->addAction(QStringLiteral("run_macro"));
run->setText(i18n("&Run Macro"));
actionCollection()->setDefaultShortcut(run, Qt::CTRL | Qt::ALT | Qt::Key_K);
connect(run, &QAction::triggered, plugin, &KeyboardMacrosPlugin::slotRun);
// TODO: make an entire "Keyboard Macros" submenu with "record", "run", "save as", "run saved"
plugin->m_recordAction = rec;
// create cancel action
QAction *cancel = actionCollection()->addAction(QStringLiteral("keyboardmacros_cancel"));
cancel->setText(i18n("&Cancel Macro Recording"));
cancel->setEnabled(false);
connect(cancel, &QAction::triggered, plugin, &KeyboardMacrosPlugin::slotCancel);
plugin->m_cancelAction = cancel;
// create play action
QAction *play = actionCollection()->addAction(QStringLiteral("keyboardmacros_play"));
play->setText(i18n("&Play Macro"));
actionCollection()->setDefaultShortcut(play, Qt::CTRL | Qt::ALT | Qt::Key_K);
play->setEnabled(false);
connect(play, &QAction::triggered, plugin, &KeyboardMacrosPlugin::slotPlay);
plugin->m_playAction = play;
// register our gui elements
mainwindow->guiFactory()->addClient(this);
......@@ -191,55 +212,5 @@ KeyboardMacrosPluginView::~KeyboardMacrosPluginView()
// END
// BEGIN commands
KeyboardMacrosPluginRecordCommand::KeyboardMacrosPluginRecordCommand(KeyboardMacrosPlugin *plugin)
: KTextEditor::Command(QStringList() << QStringLiteral("recmac"), plugin)
, m_plugin(plugin)
{
}
bool KeyboardMacrosPluginRecordCommand::exec(KTextEditor::View *view, const QString &, QString &, const KTextEditor::Range &)
{
if (m_plugin->isRecording()) {
// remove from the recording the call to this command…
}
if (!m_plugin->record(view)) {
// display fail in toolview
}
return true;
}
bool KeyboardMacrosPluginRecordCommand::help(KTextEditor::View *, const QString &, QString &msg)
{
msg = i18n("<qt><p>Usage: <code>recmac</code></p><p>Start/stop recording a keyboard macro.</p></qt>");
return true;
}
KeyboardMacrosPluginRunCommand::KeyboardMacrosPluginRunCommand(KeyboardMacrosPlugin *plugin)
: KTextEditor::Command(QStringList() << QStringLiteral("runmac"), plugin)
, m_plugin(plugin)
{
}
bool KeyboardMacrosPluginRunCommand::exec(KTextEditor::View *view, const QString &, QString &, const KTextEditor::Range &)
{
if (!m_plugin->run(view)) {
// display fail in toolview
}
return true;
// TODO: allow the command to take a name as an argument to run a saved macro (default to the last recorded one)
}
bool KeyboardMacrosPluginRunCommand::help(KTextEditor::View *, const QString &, QString &msg)
{
msg = i18n("<qt><p>Usage: <code>runmac</code></p><p>Run recorded keyboard macro.</p></qt>");
return true;
}
// TODO: add a new "savemac" command
// END
// required for KeyboardMacrosPluginFactory vtable
#include "keyboardmacrosplugin.moc"
......@@ -15,13 +15,16 @@
#include <KTextEditor/Plugin>
#include <KTextEditor/View>
class KeyboardMacrosPluginRecordCommand;
class KeyboardMacrosPluginRunCommand;
class KeyboardMacrosPluginView;
typedef QList<QKeyEvent *> Macro;
class KeyboardMacrosPlugin : public KTextEditor::Plugin
{
Q_OBJECT
friend KeyboardMacrosPluginView;
public:
explicit KeyboardMacrosPlugin(QObject *parent = nullptr, const QList<QVariant> & = QList<QVariant>());
......@@ -29,25 +32,35 @@ public:
QObject *createView(KTextEditor::MainWindow *mainWindow) override;
bool eventFilter(QObject *obj, QEvent *event) override;
void sendMessage(const QString &text, bool error);
void reset();
bool record(KTextEditor::View *view);
bool run(KTextEditor::View *view);
bool isRecording();
Q_SIGNALS:
void message(const QVariantMap &message);
public:
bool eventFilter(QObject *obj, QEvent *event) override;
private:
KTextEditor::MainWindow *m_mainWindow;
QWidget *m_focusWidget;
QAction *m_recordAction;
QAction *m_cancelAction;
QAction *m_playAction;
bool m_recording = false;
QList<QKeyEvent *> m_keyEvents;
Macro m_tape;
Macro m_macro;
KeyboardMacrosPluginRecordCommand *m_recCommand;
KeyboardMacrosPluginRunCommand *m_runCommand;
void record();
void stop(bool save);
void cancel();
bool play();
public Q_SLOTS:
void slotRecord();
void slotRun();
void slotCancel();
void slotPlay();
};
/**
......@@ -65,36 +78,4 @@ private:
KTextEditor::MainWindow *m_mainWindow;
};
/**
* recmac command
*/
class KeyboardMacrosPluginRecordCommand : public KTextEditor::Command
{
Q_OBJECT
public:
KeyboardMacrosPluginRecordCommand(KeyboardMacrosPlugin *plugin);
bool exec(KTextEditor::View *view, const QString &, QString &, const KTextEditor::Range & = KTextEditor::Range::invalid()) override;
bool help(KTextEditor::View *view, const QString &, QString &msg) override;
private:
KeyboardMacrosPlugin *m_plugin;
};
/**
* runmac command
*/
class KeyboardMacrosPluginRunCommand : public KTextEditor::Command
{
Q_OBJECT
public:
KeyboardMacrosPluginRunCommand(KeyboardMacrosPlugin *plugin);
bool exec(KTextEditor::View *view, const QString &, QString &, const KTextEditor::Range & = KTextEditor::Range::invalid()) override;
bool help(KTextEditor::View *view, const QString &, QString &msg) override;
private:
KeyboardMacrosPlugin *m_plugin;
};
#endif
{
"KPlugin": {
"Description": "Record and run keyboard action sequences",
"Description": "Record and play keyboard action sequences",
"Name": "Keyboard Macros",
"ServiceTypes": [
"KTextEditor/Plugin"
......
......@@ -4,8 +4,13 @@
<MenuBar>
<Menu name="tools">
<text>&amp;Tools</text>
<Action name="record_macro" group="tools_snippets"/>
<Action name="run_macro" group="tools_snippets"/>
<Separator/>
<Menu name="keyboardmacros" noMerge="1">
<text>&amp;Keyboard Macros</text>
<Action name="keyboardmacros_record"/>
<Action name="keyboardmacros_cancel"/>
<Action name="keyboardmacros_play"/>
</Menu>
</Menu>
</MenuBar>
</gui>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment