diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt index 7d53e05f2..cb7b9eb4c 100644 --- a/autotests/integration/CMakeLists.txt +++ b/autotests/integration/CMakeLists.txt @@ -1,49 +1,51 @@ add_definitions(-DKWINBACKENDPATH="${CMAKE_BINARY_DIR}/plugins/platforms/virtual/KWinWaylandVirtualBackend.so") add_definitions(-DKWINQPAPATH="${CMAKE_BINARY_DIR}/plugins/qpa/") add_subdirectory(helper) add_library(KWinIntegrationTestFramework STATIC kwin_wayland_test.cpp test_helpers.cpp) target_link_libraries(KWinIntegrationTestFramework kwin Qt5::Test) function(integrationTest) set(oneValueArgs NAME) set(multiValueArgs SRCS LIBS) cmake_parse_arguments(ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) add_executable(${ARGS_NAME} ${ARGS_SRCS}) target_link_libraries(${ARGS_NAME} KWinIntegrationTestFramework kwin Qt5::Test ${ARGS_LIBS}) add_test(kwin-${ARGS_NAME} ${ARGS_NAME}) endfunction() integrationTest(NAME testStart SRCS start_test.cpp) integrationTest(NAME testTransientNoInput SRCS transient_no_input_test.cpp) integrationTest(NAME testQuickTiling SRCS quick_tiling_test.cpp) integrationTest(NAME testDontCrashGlxgears SRCS dont_crash_glxgears.cpp) integrationTest(NAME testLockScreen SRCS lockscreen.cpp) integrationTest(NAME testDecorationInput SRCS decoration_input_test.cpp) integrationTest(NAME testInternalWindow SRCS internal_window.cpp) integrationTest(NAME testTouchInput SRCS touch_input_test.cpp) integrationTest(NAME testInputStackingOrder SRCS input_stacking_order.cpp) integrationTest(NAME testPointerInput SRCS pointer_input.cpp) integrationTest(NAME testPlatformCursor SRCS platformcursor.cpp) integrationTest(NAME testDontCrashCancelAnimation SRCS dont_crash_cancel_animation.cpp) integrationTest(NAME testTransientPlacmenet SRCS transient_placement.cpp) integrationTest(NAME testDebugConsole SRCS debug_console_test.cpp) integrationTest(NAME testDontCrashEmptyDeco SRCS dont_crash_empty_deco.cpp) integrationTest(NAME testPlasmaSurface SRCS plasma_surface_test.cpp) integrationTest(NAME testMaximized SRCS maximize_test.cpp) integrationTest(NAME testShellClient SRCS shell_client_test.cpp) integrationTest(NAME testDontCrashNoBorder SRCS dont_crash_no_border.cpp) integrationTest(NAME testXClipboardSync SRCS xclipboardsync_test.cpp) integrationTest(NAME testSceneOpenGL SRCS scene_opengl_test.cpp) integrationTest(NAME testSceneQPainter SRCS scene_qpainter_test.cpp) integrationTest(NAME testNoXdgRuntimeDir SRCS no_xdg_runtime_dir_test.cpp) integrationTest(NAME testScreenChanges SRCS screen_changes_test.cpp) if (XCB_ICCCM_FOUND) integrationTest(NAME testMoveResize SRCS move_resize_window_test.cpp LIBS XCB::ICCCM) integrationTest(NAME testStruts SRCS struts_test.cpp LIBS XCB::ICCCM) integrationTest(NAME testShade SRCS shade_test.cpp LIBS XCB::ICCCM) integrationTest(NAME testDontCrashAuroraeDestroyDeco SRCS dont_crash_aurorae_destroy_deco.cpp LIBS XCB::ICCCM) integrationTest(NAME testPlasmaWindow SRCS plasmawindow_test.cpp LIBS XCB::ICCCM) integrationTest(NAME testScreenEdgeClientShow SRCS screenedge_client_show_test.cpp LIBS XCB::ICCCM) endif() + +add_subdirectory(scripting) diff --git a/autotests/integration/scripting/CMakeLists.txt b/autotests/integration/scripting/CMakeLists.txt new file mode 100644 index 000000000..854f7ae9f --- /dev/null +++ b/autotests/integration/scripting/CMakeLists.txt @@ -0,0 +1 @@ +integrationTest(NAME testScriptingScreenEdge SRCS screenedge_test.cpp) diff --git a/autotests/integration/scripting/screenedge_test.cpp b/autotests/integration/scripting/screenedge_test.cpp new file mode 100644 index 000000000..32a8be40a --- /dev/null +++ b/autotests/integration/scripting/screenedge_test.cpp @@ -0,0 +1,146 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "cursor.h" +#include "effectloader.h" +#include "platform.h" +#include "wayland_server.h" +#include "workspace.h" +#include "scripting/scripting.h" +#include "effect_builtins.h" + +#include + +Q_DECLARE_METATYPE(KWin::ElectricBorder) + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_scripting_screenedge-0"); + +class ScreenEdgeTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testEdge_data(); + void testEdge(); +}; + +void ScreenEdgeTest::initTestCase() +{ + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // empty config to have defaults + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + + // disable all effects to prevent them grabbing edges + KConfigGroup plugins(config, QStringLiteral("Plugins")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + + // disable electric border pushaback + config->group("Windows").writeEntry("ElectricBorderPushbackPixels", 0); + + config->sync(); + kwinApp()->setConfig(config); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + QVERIFY(Scripting::self()); +} + +void ScreenEdgeTest::init() +{ + KWin::Cursor::setPos(640, 512); + if (workspace()->showingDesktop()) { + workspace()->slotToggleShowDesktop(); + } + QVERIFY(!workspace()->showingDesktop()); +} + +void ScreenEdgeTest::cleanup() +{ + // try to unload the script + const QString scriptToLoad = QFINDTESTDATA("./scripts/screenedge.js"); + if (!scriptToLoad.isEmpty()) { + if (Scripting::self()->isScriptLoaded(scriptToLoad)) { + QVERIFY(Scripting::self()->unloadScript(scriptToLoad)); + QTRY_VERIFY(!Scripting::self()->isScriptLoaded(scriptToLoad)); + } + } +} + +void ScreenEdgeTest::testEdge_data() +{ + QTest::addColumn("edge"); + QTest::addColumn("triggerPos"); + + QTest::newRow("Top") << KWin::ElectricTop << QPoint(512, 0); + QTest::newRow("TopRight") << KWin::ElectricTopRight << QPoint(1279, 0); + QTest::newRow("Right") << KWin::ElectricRight << QPoint(1279, 512); + QTest::newRow("BottomRight") << KWin::ElectricBottomRight << QPoint(1279, 1023); + QTest::newRow("Bottom") << KWin::ElectricBottom << QPoint(512, 1023); + QTest::newRow("BottomLeft") << KWin::ElectricBottomLeft << QPoint(0, 1023); + QTest::newRow("Left") << KWin::ElectricLeft << QPoint(0, 512); + QTest::newRow("TopLeft") << KWin::ElectricTopLeft << QPoint(0, 0); +} + +void ScreenEdgeTest::testEdge() +{ + const QString scriptToLoad = QFINDTESTDATA("./scripts/screenedge.js"); + QVERIFY(!scriptToLoad.isEmpty()); + + // mock the config + auto config = kwinApp()->config(); + QFETCH(KWin::ElectricBorder, edge); + config->group(QLatin1String("Script-") + scriptToLoad).writeEntry("Edge", int(edge)); + config->sync(); + + QVERIFY(!Scripting::self()->isScriptLoaded(scriptToLoad)); + const int id = Scripting::self()->loadScript(scriptToLoad); + QVERIFY(id != -1); + QVERIFY(Scripting::self()->isScriptLoaded(scriptToLoad)); + // TODO: a way to run the script without having to call the global start + Scripting::self()->start(); + // give it some time to start - a callback would be nice + QTest::qWait(100); + + // triggering the edge will result in show desktop being triggered + QSignalSpy showDesktopSpy(workspace(), &Workspace::showingDesktopChanged); + QVERIFY(showDesktopSpy.isValid()); + + // trigger the edge + QFETCH(QPoint, triggerPos); + KWin::Cursor::setPos(triggerPos); + QCOMPARE(showDesktopSpy.count(), 1); + QVERIFY(workspace()->showingDesktop()); +} + +WAYLANDTEST_MAIN(ScreenEdgeTest) +#include "screenedge_test.moc" diff --git a/autotests/integration/scripting/scripts/screenedge.js b/autotests/integration/scripting/scripts/screenedge.js new file mode 100644 index 000000000..4d02e83b3 --- /dev/null +++ b/autotests/integration/scripting/scripts/screenedge.js @@ -0,0 +1 @@ +registerScreenEdge(readConfig("Edge", 1), function() { workspace.slotToggleShowDesktop(); }); diff --git a/scripting/scripting.h b/scripting/scripting.h index fdc3ebe79..48a3b7132 100644 --- a/scripting/scripting.h +++ b/scripting/scripting.h @@ -1,403 +1,403 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2010 Rohan Prabhu Copyright (C) 2011 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_SCRIPTING_H #define KWIN_SCRIPTING_H #include #include #include #include #include class QQmlComponent; class QQmlContext; class QQmlEngine; class QAction; class QDBusPendingCallWatcher; class QGraphicsScene; class QMenu; class QMutex; class QScriptEngine; class QScriptValue; class QQuickWindow; class KConfigGroup; /// @c true == javascript, @c false == qml typedef QList< QPair > > LoadScriptList; namespace KWin { class AbstractClient; class Client; class ScriptUnloaderAgent; class WorkspaceWrapper; class AbstractScript : public QObject { Q_OBJECT public: AbstractScript(int id, QString scriptName, QString pluginName, QObject *parent = nullptr); ~AbstractScript(); QString fileName() const { return m_scriptFile.fileName(); } const QString &pluginName() { return m_pluginName; } void printMessage(const QString &message); void registerShortcut(QAction *a, QScriptValue callback); /** * @brief Registers the given @p callback to be invoked whenever the UserActionsMenu is about * to be showed. In the callback the script can create a further sub menu or menu entry to be * added to the UserActionsMenu. * * @param callback Script method to execute when the UserActionsMenu is about to be shown. * @return void * @see actionsForUserActionMenu **/ void registerUseractionsMenuCallback(QScriptValue callback); /** * @brief Creates actions for the UserActionsMenu by invoking the registered callbacks. * * This method invokes all the callbacks previously registered with registerUseractionsMenuCallback. * The Client @p c is passed in as an argument to the invoked method. * * The invoked method is supposed to return a JavaScript object containing either the menu or * menu entry to be added. In case the callback returns a null or undefined or any other invalid * value, it is not considered for adding to the menu. * * The JavaScript object structure for a menu entry looks like the following: * @code * { * title: "My Menu Entry", * checkable: true, * checked: false, * triggered: function (action) { * // callback when the menu entry is triggered with the QAction as argument * } * } * @endcode * * To construct a complete Menu the JavaScript object looks like the following: * @code * { * title: "My Menu Title", * items: [{...}, {...}, ...] // list of menu entries as described above * } * @endcode * * The returned JavaScript object is introspected and for a menu entry a QAction is created, * while for a menu a QMenu is created and QActions for the individual entries. Of course it * is allowed to have nested structures. * * All created objects are (grand) children to the passed in @p parent menu, so that they get * deleted whenever the menu is destroyed. * * @param c The Client for which the menu is invoked, passed to the callback * @param parent The Parent for the created Menus or Actions * @return QList< QAction* > List of QActions obtained from asking the registered callbacks * @see registerUseractionsMenuCallback **/ QList actionsForUserActionMenu(AbstractClient *c, QMenu *parent); KConfigGroup config() const; const QHash &shortcutCallbacks() const { return m_shortcutCallbacks; } QHash > &screenEdgeCallbacks() { return m_screenEdgeCallbacks; } int registerCallback(QScriptValue value); public Q_SLOTS: Q_SCRIPTABLE void stop(); Q_SCRIPTABLE virtual void run() = 0; void slotPendingDBusCall(QDBusPendingCallWatcher *watcher); private Q_SLOTS: void globalShortcutTriggered(); bool borderActivated(ElectricBorder edge); /** * @brief Slot invoked when a menu action is destroyed. Used to remove the action and callback * from the map of actions. * * @param object The destroyed action **/ void actionDestroyed(QObject *object); Q_SIGNALS: Q_SCRIPTABLE void print(const QString &text); protected: QFile &scriptFile() { return m_scriptFile; } bool running() const { return m_running; } void setRunning(bool running) { m_running = running; } int scriptId() const { return m_scriptId; } private: /** * @brief Parses the @p value to either a QMenu or QAction. * * @param value The ScriptValue describing either a menu or action * @param parent The parent to use for the created menu or action * @return QAction* The parsed action or menu action, if parsing fails returns @c null. **/ QAction *scriptValueToAction(QScriptValue &value, QMenu *parent); /** * @brief Creates a new QAction from the provided data and registers it for invoking the * @p callback when the action is triggered. * * The created action is added to the map of actions and callbacks shared with the global * shortcuts. * * @param title The title of the action * @param checkable Whether the action is checkable * @param checked Whether the checkable action is checked * @param callback The callback to invoke when the action is triggered * @param parent The parent to be used for the new created action * @return QAction* The created action **/ QAction *createAction(const QString &title, bool checkable, bool checked, QScriptValue &callback, QMenu *parent); /** * @brief Parses the @p items and creates a QMenu from it. * * @param title The title of the Menu. * @param items JavaScript Array containing Menu items. * @param parent The parent to use for the new created menu * @return QAction* The menu action for the new Menu **/ QAction *createMenu(const QString &title, QScriptValue &items, QMenu *parent); int m_scriptId; QFile m_scriptFile; QString m_pluginName; bool m_running; QHash m_shortcutCallbacks; QHash > m_screenEdgeCallbacks; QHash m_callbacks; /** * @brief List of registered functions to call when the UserActionsMenu is about to show * to add further entries. **/ QList m_userActionsMenuCallbacks; }; class Script : public AbstractScript { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Scripting") public: Script(int id, QString scriptName, QString pluginName, QObject *parent = nullptr); virtual ~Script(); QScriptEngine *engine() { return m_engine; } public Q_SLOTS: Q_SCRIPTABLE void run(); Q_SIGNALS: Q_SCRIPTABLE void printError(const QString &text); private Q_SLOTS: /** * A nice clean way to handle exceptions in scripting. * TODO: Log to file, show from notifier.. */ void sigException(const QScriptValue &exception); /** * Callback for when loadScriptFromFile has finished. **/ void slotScriptLoadedFromFile(); private: void installScriptFunctions(QScriptEngine *engine); /** * Read the script from file into a byte array. * If file cannot be read an empty byte array is returned. **/ QByteArray loadScriptFromFile(); QScriptEngine *m_engine; bool m_starting; QScopedPointer m_agent; }; class ScriptUnloaderAgent : public QScriptEngineAgent { public: explicit ScriptUnloaderAgent(Script *script); virtual void scriptUnload(qint64 id); private: Script *m_script; }; class DeclarativeScript : public AbstractScript { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Scripting") public: explicit DeclarativeScript(int id, QString scriptName, QString pluginName, QObject *parent = nullptr); virtual ~DeclarativeScript(); public Q_SLOTS: Q_SCRIPTABLE void run(); private Q_SLOTS: void createComponent(); private: QQmlContext *m_context; QQmlComponent *m_component; }; class JSEngineGlobalMethodsWrapper : public QObject { Q_OBJECT Q_ENUMS(ClientAreaOption) public: //------------------------------------------------------------------ //enums copy&pasted from kwinglobals.h for exporting enum ClientAreaOption { ///< geometry where a window will be initially placed after being mapped PlacementArea, ///< window movement snapping area? ignore struts MovementArea, ///< geometry to which a window will be maximized MaximizeArea, ///< like MaximizeArea, but ignore struts - used e.g. for topmenu MaximizeFullArea, ///< area for fullscreen windows FullScreenArea, ///< whole workarea (all screens together) WorkArea, ///< whole area (all screens together), ignore struts FullArea, ///< one whole screen, ignore struts ScreenArea }; explicit JSEngineGlobalMethodsWrapper(DeclarativeScript *parent); virtual ~JSEngineGlobalMethodsWrapper(); public Q_SLOTS: QVariant readConfig(const QString &key, QVariant defaultValue = QVariant()); void registerWindow(QQuickWindow *window); private: DeclarativeScript *m_script; }; /** * The heart of KWin::Scripting. Infinite power lies beyond */ -class Scripting : public QObject +class KWIN_EXPORT Scripting : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Scripting") private: explicit Scripting(QObject *parent); QStringList scriptList; QList scripts; /** * Lock to protect the scripts member variable. **/ QScopedPointer m_scriptsLock; // Preferably call ONLY at load time void runScripts(); public: ~Scripting(); Q_SCRIPTABLE Q_INVOKABLE int loadScript(const QString &filePath, const QString &pluginName = QString()); Q_SCRIPTABLE Q_INVOKABLE int loadDeclarativeScript(const QString &filePath, const QString &pluginName = QString()); Q_SCRIPTABLE Q_INVOKABLE bool isScriptLoaded(const QString &pluginName) const; Q_SCRIPTABLE Q_INVOKABLE bool unloadScript(const QString &pluginName); /** * @brief Invokes all registered callbacks to add actions to the UserActionsMenu. * * @param c The Client for which the UserActionsMenu is about to be shown * @param parent The parent menu to which to add created child menus and items * @return QList< QAction* > List of all actions aggregated from all scripts. **/ QList actionsForUserActionMenu(AbstractClient *c, QMenu *parent); QQmlEngine *qmlEngine() const; QQmlEngine *qmlEngine(); WorkspaceWrapper *workspaceWrapper() const; static Scripting *self(); static Scripting *create(QObject *parent); public Q_SLOTS: void scriptDestroyed(QObject *object); Q_SCRIPTABLE void start(); private Q_SLOTS: void slotScriptsQueried(); private: void init(); LoadScriptList queryScriptsToLoad(); static Scripting *s_self; QQmlEngine *m_qmlEngine; WorkspaceWrapper *m_workspaceWrapper; }; inline QQmlEngine *Scripting::qmlEngine() const { return m_qmlEngine; } inline QQmlEngine *Scripting::qmlEngine() { return m_qmlEngine; } inline WorkspaceWrapper *Scripting::workspaceWrapper() const { return m_workspaceWrapper; } inline Scripting *Scripting::self() { return s_self; } } #endif