diff options
author | Fabien Proriol <fabien.proriol@kazoe.org> | 2025-05-25 17:58:09 +0200 |
---|---|---|
committer | Fabien Proriol <fabien.proriol@kazoe.org> | 2025-05-26 18:01:49 +0200 |
commit | 49daa163530ceabc9eaa8911ab96b5f799cfb552 (patch) | |
tree | 080d0b31eafd138cd8d47d5c2a52b75d3cfa6f28 |
Initial Commit
31 files changed, 2051 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7f8a12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build +CMakeLists.txt.user diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c388041 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,67 @@ +cmake_minimum_required(VERSION 3.16) + +project(KzDesktop VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 COMPONENTS Quick DBus WaylandCompositor REQUIRED) + +qt_standard_project_setup(REQUIRES 6.5) + +find_package(KzXdg REQUIRED) +include_directories(${KZXDG_INCLUDE_DIR}) + +set(DBUS_APIS + api/org.kazoe.desktop.applications.xml +) + +set(PROJECT_QMLS + qml/Main.qml + qml/HomePage.qml + qml/DesktopGridView.qml + qml/StatusBar.qml + qml/SystrayView.qml + qml/VKeyboard.qml +) + +set(PROJECT_SOURCES + src/main.cpp + src/applicationmanager.h src/applicationmanager.cpp + src/waylandentries.h src/waylandentries.cpp + src/waylandentriesfiltered.h src/waylandentriesfiltered.cpp + src/debug.cpp src/debug.h +) + +set(OTHER_FILES + conf/kzdesktop.sh + conf/kzdesktop.service + ${WAYLAND_PROTOCOLS} + ${DBUS_APIS} +) + +set(CLI_SOURCES + tools/cli/main.cpp +) + +qt_add_dbus_adaptor(ApplicationManager_SRCS api/org.kazoe.desktop.applications.xml src/applicationmanager.h ApplicationManager) +qt_add_dbus_interface(ApplicationManagerIface_SRCS api/org.kazoe.desktop.applications.xml ApplicationManagerIface) +qt_add_executable(kzdesktop ${PROJECT_SOURCES} ${PROJECT_QMLS} ${OTHER_FILES} ${ApplicationManager_SRCS} ${APPMANAGER_SRCS}) +qt_add_executable(desktopcli ${CLI_SOURCES} ${ApplicationManagerIface_SRCS} ${DBUS_APIS}) +qt_add_qml_module(kzdesktop URI org.kazoe.desktop VERSION 1.0 QML_FILES ${PROJECT_QMLS} ) + +target_compile_definitions(kzdesktop PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>) +target_link_libraries(kzdesktop PRIVATE ${KZXDG_LIBRARIES} Qt6::Quick Qt6::DBus Qt6::WaylandCompositor systemd ) +target_link_libraries(desktopcli PRIVATE Qt6::Core Qt6::DBus) + + +include(GNUInstallDirs) +install(TARGETS kzdesktop DESTINATION ${CMAKE_INSTALL_BIN}) +install(TARGETS desktopcli DESTINATION ${CMAKE_INSTALL_BIN}) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/conf/kzdesktop.service DESTINATION /usr/lib/systemd/user) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/conf/kzdesktop.sh DESTINATION /etc/profile.d/) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/conf/kzdesktop.conf DESTINATION /etc/system.conf.d/) +install(FILES ${DBUS_APIS} DESTINATION /usr/share/dbus-1/interfaces/) @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Proriol Fabien + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd564d3 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# KaZoe Desktop +This project provide a desktop environement base on "Qt Wayland Compositor". + +It combine features: + +- Wayland compositor +- Window manager +- customizable Task bar +- customizable systray +- homepage provided by an external application +- VNC server managed by a plugin + + +# Build and run +```sh +git clone https://kazoe.org/cgit/kazoe/kazoe-desktop.git +cd kazoe-desktop +mkdir build +cmake .. +make +./kzdesktop +``` + +DesktopManager use [standard XDG paths](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). +This paths can be override by setting [XDG_DATA_HOME and XDG_DATA_DIRS](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). + + +## Design + + + + + +## Protocol +https://specifications.freedesktop.org/menu-spec/menu-spec-1.0.html#desktop-entry-extensions-examples diff --git a/api/org.kazoe.desktop.applications.xml b/api/org.kazoe.desktop.applications.xml new file mode 100644 index 0000000..b32a062 --- /dev/null +++ b/api/org.kazoe.desktop.applications.xml @@ -0,0 +1,31 @@ +<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> +<node> + <interface name="org.kazoe.desktop.applications"> + <method name="backHome" /> + <method name="count"> + <arg type="i" direction="out"/> + </method> + <method name="currentApplicationPid"> + <arg type="i" direction="out"/> + </method> + <method name="applicationName"> + <arg type="s" direction="out"/> + <arg name="id" type="i" direction="in"/> + </method> + <method name="applicationPid"> + <arg type="i" direction="out"/> + <arg name="id" type="i" direction="in"/> + </method> + <method name="applicationFocus"> + <arg type="i" direction="out"/> + <arg name="id" type="i" direction="in"/> + </method> + <method name="hideApplicationPid"> + <arg name="pid" type="x" direction="in"/> + </method> + <method name="raiseApplicationPid"> + <arg name="pid" type="x" direction="in"/> + </method> + </interface> +</node> diff --git a/conf/kms.conf b/conf/kms.conf new file mode 100644 index 0000000..6629859 --- /dev/null +++ b/conf/kms.conf @@ -0,0 +1,11 @@ +{ + "hwcursor" : false, + "pbuffers": true, + "device": "/dev/dri/card1", + "outputs": [ + { + "name": "DP1", + "format":"argb8888" + } + ] +} diff --git a/conf/kzdesktop.conf b/conf/kzdesktop.conf new file mode 100644 index 0000000..be38d38 --- /dev/null +++ b/conf/kzdesktop.conf @@ -0,0 +1,3 @@ +[desktop] +orientation="horizontal" +theme="light" diff --git a/conf/kzdesktop.service b/conf/kzdesktop.service new file mode 100644 index 0000000..dc7c6bb --- /dev/null +++ b/conf/kzdesktop.service @@ -0,0 +1,21 @@ +[Unit] +Description=KaZoe desktop compositor +Wants=dbus.service + +[Service] +Restart=always +RestartSec=1s +Type=notify +NotifyAccess=main +ExecStart=/usr/bin/kzdesktop +StandardOutput=journal+console +WatchdogSec=5 + +Environment="QT_QPA_PLATFORM=eglfs" +Environment="QT_WAYLAND_SERVER_BUFFER_INTEGRATION=dmabuf-server" +Environment="QT_WAYLAND_CLIENT_BUFFER_INTEGRATION=linux-dmabuf-unstable-v1" +Environment="QT_IM_MODULE=qtvirtualkeyboard" +Environment="XDG_DATA_DIRS=/var/volatile:/usr/share" + +[Install] +WantedBy=default.target diff --git a/conf/kzdesktop.sh b/conf/kzdesktop.sh new file mode 100644 index 0000000..9463922 --- /dev/null +++ b/conf/kzdesktop.sh @@ -0,0 +1,3 @@ +# Profile for desktopmgr /etc/profile.d/desktopmgr.sh +export QT_QPA_PLATFORM=wayland-egl +export WAYLAND_DISPLAY=wayland-0 diff --git a/protocols/vnc-keyboard-unstable-v1.xml b/protocols/vnc-keyboard-unstable-v1.xml new file mode 100644 index 0000000..e870731 --- /dev/null +++ b/protocols/vnc-keyboard-unstable-v1.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="vnc_keyboard_unstable_v1"> + + <interface name="vnc_keyboard_v1" version="1"> + <description summary="vnc keyboard"> + The vnc keyboard provides an application with requests which maps + wit the vnc protocol. It's not a keyboard interface in that it + doesn't know anything about keymaps. + + </description> + + <request name="key"> + <description summary="key event"> + A key was pressed or released. + The time argument is a timestamp with millisecond granularity, with an + undefined base. All requests regarding a single object must share the + same clock. + State carries a value from the key_state enumeration. + </description> + <arg name="time" type="uint" summary="timestamp with millisecond granularity"/> + <arg name="keysym" type="uint" summary="key symbol"/> + <arg name="pressed" type="uint" summary="physical state of the key"/> + </request> + + <request name="keycode"> + <description summary="key code event"> + A key was pressed or released. + The time argument is a timestamp with millisecond granularity, with an + undefined base. All requests regarding a single object must share the + same clock. + State carries a value from the key_state enumeration. + </description> + <arg name="time" type="uint" summary="timestamp with millisecond granularity"/> + <arg name="code" type="uint" summary="key code"/> + <arg name="pressed" type="uint" summary="physical state of the key"/> + </request> + + <request name="destroy" type="destructor" since="1"> + <description summary="destroy the virtual keyboard keyboard object"/> + </request> + </interface> + + <interface name="vnc_virtual_keyboard_manager_v1" version="1"> + <description summary="virtual keyboard manager"> + A virtual keyboard manager allows an application to provide key + input events (using RFB key encoding) + </description> + + <enum name="error"> + <entry name="unauthorized" value="0" summary="client not authorized to use the interface"/> + </enum> + + <request name="create_virtual_keyboard"> + <description summary="Create a new virtual keyboard"> + Creates a new vnc keyboard. + </description> + <arg name="id" type="new_id" interface="vnc_keyboard_v1"/> + </request> + </interface> +</protocol> diff --git a/protocols/wlr-screencopy-unstable-v1.xml b/protocols/wlr-screencopy-unstable-v1.xml new file mode 100644 index 0000000..50b1b7d --- /dev/null +++ b/protocols/wlr-screencopy-unstable-v1.xml @@ -0,0 +1,232 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="wlr_screencopy_unstable_v1"> + <copyright> + Copyright © 2018 Simon Ser + Copyright © 2019 Andri Yngvason + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + </copyright> + + <description summary="screen content capturing on client buffers"> + This protocol allows clients to ask the compositor to copy part of the + screen content to a client buffer. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + </description> + + <interface name="zwlr_screencopy_manager_v1" version="3"> + <description summary="manager to inform clients and begin capturing"> + This object is a manager which offers requests to start capturing from a + source. + </description> + + <request name="capture_output"> + <description summary="capture an output"> + Capture the next frame of an entire output. + </description> + <arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/> + <arg name="overlay_cursor" type="int" + summary="composite cursor onto the frame"/> + <arg name="output" type="object" interface="wl_output"/> + </request> + + <request name="capture_output_region"> + <description summary="capture an output's region"> + Capture the next frame of an output's region. + + The region is given in output logical coordinates, see + xdg_output.logical_size. The region will be clipped to the output's + extents. + </description> + <arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/> + <arg name="overlay_cursor" type="int" + summary="composite cursor onto the frame"/> + <arg name="output" type="object" interface="wl_output"/> + <arg name="x" type="int"/> + <arg name="y" type="int"/> + <arg name="width" type="int"/> + <arg name="height" type="int"/> + </request> + + <request name="destroy" type="destructor"> + <description summary="destroy the manager"> + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + </description> + </request> + </interface> + + <interface name="zwlr_screencopy_frame_v1" version="3"> + <description summary="a frame ready for copy"> + This object represents a single frame. + + When created, a series of buffer events will be sent, each representing a + supported buffer type. The "buffer_done" event is sent afterwards to + indicate that all supported buffer types have been enumerated. The client + will then be able to send a "copy" request. If the capture is successful, + the compositor will send a "flags" followed by a "ready" event. + + For objects version 2 or lower, wl_shm buffers are always supported, ie. + the "buffer" event is guaranteed to be sent. + + If the capture failed, the "failed" event is sent. This can happen anytime + before the "ready" event. + + Once either a "ready" or a "failed" event is received, the client should + destroy the frame. + </description> + + <event name="buffer"> + <description summary="wl_shm buffer information"> + Provides information about wl_shm buffer parameters that need to be + used for this frame. This event is sent once after the frame is created + if wl_shm buffers are supported. + </description> + <arg name="format" type="uint" enum="wl_shm.format" summary="buffer format"/> + <arg name="width" type="uint" summary="buffer width"/> + <arg name="height" type="uint" summary="buffer height"/> + <arg name="stride" type="uint" summary="buffer stride"/> + </event> + + <request name="copy"> + <description summary="copy the frame"> + Copy the frame to the supplied buffer. The buffer must have a the + correct size, see zwlr_screencopy_frame_v1.buffer and + zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a + supported format. + + If the frame is successfully copied, a "flags" and a "ready" events are + sent. Otherwise, a "failed" event is sent. + </description> + <arg name="buffer" type="object" interface="wl_buffer"/> + </request> + + <enum name="error"> + <entry name="already_used" value="0" + summary="the object has already been used to copy a wl_buffer"/> + <entry name="invalid_buffer" value="1" + summary="buffer attributes are invalid"/> + </enum> + + <enum name="flags" bitfield="true"> + <entry name="y_invert" value="1" summary="contents are y-inverted"/> + </enum> + + <event name="flags"> + <description summary="frame flags"> + Provides flags about the frame. This event is sent once before the + "ready" event. + </description> + <arg name="flags" type="uint" enum="flags" summary="frame flags"/> + </event> + + <event name="ready"> + <description summary="indicates frame is available for reading"> + Called as soon as the frame is copied, indicating it is available + for reading. This event includes the time at which presentation happened + at. + + The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, + each component being an unsigned 32-bit value. Whole seconds are in + tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, + and the additional fractional part in tv_nsec as nanoseconds. Hence, + for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part + may have an arbitrary offset at start. + + After receiving this event, the client should destroy the object. + </description> + <arg name="tv_sec_hi" type="uint" + summary="high 32 bits of the seconds part of the timestamp"/> + <arg name="tv_sec_lo" type="uint" + summary="low 32 bits of the seconds part of the timestamp"/> + <arg name="tv_nsec" type="uint" + summary="nanoseconds part of the timestamp"/> + </event> + + <event name="failed"> + <description summary="frame copy failed"> + This event indicates that the attempted frame copy has failed. + + After receiving this event, the client should destroy the object. + </description> + </event> + + <request name="destroy" type="destructor"> + <description summary="delete this object, used or not"> + Destroys the frame. This request can be sent at any time by the client. + </description> + </request> + + <!-- Version 2 additions --> + <request name="copy_with_damage" since="2"> + <description summary="copy the frame when it's damaged"> + Same as copy, except it waits until there is damage to copy. + </description> + <arg name="buffer" type="object" interface="wl_buffer"/> + </request> + + <event name="damage" since="2"> + <description summary="carries the coordinates of the damaged region"> + This event is sent right before the ready event when copy_with_damage is + requested. It may be generated multiple times for each copy_with_damage + request. + + The arguments describe a box around an area that has changed since the + last copy request that was derived from the current screencopy manager + instance. + + The union of all regions received between the call to copy_with_damage + and a ready event is the total damage since the prior ready event. + </description> + <arg name="x" type="uint" summary="damaged x coordinates"/> + <arg name="y" type="uint" summary="damaged y coordinates"/> + <arg name="width" type="uint" summary="current width"/> + <arg name="height" type="uint" summary="current height"/> + </event> + + <!-- Version 3 additions --> + <event name="linux_dmabuf" since="3"> + <description summary="linux-dmabuf buffer information"> + Provides information about linux-dmabuf buffer parameters that need to + be used for this frame. This event is sent once after the frame is + created if linux-dmabuf buffers are supported. + </description> + <arg name="format" type="uint" summary="fourcc pixel format"/> + <arg name="width" type="uint" summary="buffer width"/> + <arg name="height" type="uint" summary="buffer height"/> + </event> + + <event name="buffer_done" since="3"> + <description summary="all buffer types reported"> + This event is sent once after all buffer events have been sent. + + The client should proceed to create a buffer of one of the supported + types, and send a "copy" request. + </description> + </event> + </interface> +</protocol> diff --git a/protocols/wlr-virtual-pointer-unstable-v1.xml b/protocols/wlr-virtual-pointer-unstable-v1.xml new file mode 100644 index 0000000..ea243e7 --- /dev/null +++ b/protocols/wlr-virtual-pointer-unstable-v1.xml @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="wlr_virtual_pointer_unstable_v1"> + <copyright> + Copyright © 2019 Josef Gajdusek + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + </copyright> + + <interface name="zwlr_virtual_pointer_v1" version="2"> + <description summary="virtual pointer"> + This protocol allows clients to emulate a physical pointer device. The + requests are mostly mirror opposites of those specified in wl_pointer. + </description> + + <enum name="error"> + <entry name="invalid_axis" value="0" + summary="client sent invalid axis enumeration value" /> + <entry name="invalid_axis_source" value="1" + summary="client sent invalid axis source enumeration value" /> + </enum> + + <request name="motion"> + <description summary="pointer relative motion event"> + The pointer has moved by a relative amount to the previous request. + + Values are in the global compositor space. + </description> + <arg name="time" type="uint" summary="timestamp with millisecond granularity"/> + <arg name="dx" type="fixed" summary="displacement on the x-axis"/> + <arg name="dy" type="fixed" summary="displacement on the y-axis"/> + </request> + + <request name="motion_absolute"> + <description summary="pointer absolute motion event"> + The pointer has moved in an absolute coordinate frame. + + Value of x can range from 0 to x_extent, value of y can range from 0 + to y_extent. + </description> + <arg name="time" type="uint" summary="timestamp with millisecond granularity"/> + <arg name="x" type="uint" summary="position on the x-axis"/> + <arg name="y" type="uint" summary="position on the y-axis"/> + <arg name="x_extent" type="uint" summary="extent of the x-axis"/> + <arg name="y_extent" type="uint" summary="extent of the y-axis"/> + </request> + + <request name="button"> + <description summary="button event"> + A button was pressed or released. + </description> + <arg name="time" type="uint" summary="timestamp with millisecond granularity"/> + <arg name="button" type="uint" summary="button that produced the event"/> + <arg name="state" type="uint" enum="wl_pointer.button_state" summary="physical state of the button"/> + </request> + + <request name="axis"> + <description summary="axis event"> + Scroll and other axis requests. + </description> + <arg name="time" type="uint" summary="timestamp with millisecond granularity"/> + <arg name="axis" type="uint" enum="wl_pointer.axis" summary="axis type"/> + <arg name="value" type="fixed" summary="length of vector in touchpad coordinates"/> + </request> + + <request name="frame"> + <description summary="end of a pointer event sequence"> + Indicates the set of events that logically belong together. + </description> + </request> + + <request name="axis_source"> + <description summary="axis source event"> + Source information for scroll and other axis. + </description> + <arg name="axis_source" type="uint" enum="wl_pointer.axis_source" summary="source of the axis event"/> + </request> + + <request name="axis_stop"> + <description summary="axis stop event"> + Stop notification for scroll and other axes. + </description> + <arg name="time" type="uint" summary="timestamp with millisecond granularity"/> + <arg name="axis" type="uint" enum="wl_pointer.axis" summary="the axis stopped with this event"/> + </request> + + <request name="axis_discrete"> + <description summary="axis click event"> + Discrete step information for scroll and other axes. + + This event allows the client to extend data normally sent using the axis + event with discrete value. + </description> + <arg name="time" type="uint" summary="timestamp with millisecond granularity"/> + <arg name="axis" type="uint" enum="wl_pointer.axis" summary="axis type"/> + <arg name="value" type="fixed" summary="length of vector in touchpad coordinates"/> + <arg name="discrete" type="int" summary="number of steps"/> + </request> + + <request name="destroy" type="destructor" since="1"> + <description summary="destroy the virtual pointer object"/> + </request> + </interface> + + <interface name="zwlr_virtual_pointer_manager_v1" version="2"> + <description summary="virtual pointer manager"> + This object allows clients to create individual virtual pointer objects. + </description> + + <request name="create_virtual_pointer"> + <description summary="Create a new virtual pointer"> + Creates a new virtual pointer. The optional seat is a suggestion to the + compositor. + </description> + <arg name="seat" type="object" interface="wl_seat" allow-null="true"/> + <arg name="id" type="new_id" interface="zwlr_virtual_pointer_v1"/> + </request> + + <request name="destroy" type="destructor" since="1"> + <description summary="destroy the virtual pointer manager"/> + </request> + + <!-- Version 2 additions --> + <request name="create_virtual_pointer_with_output" since="2"> + <description summary="Create a new virtual pointer"> + Creates a new virtual pointer. The seat and the output arguments are + optional. If the seat argument is set, the compositor should assign the + input device to the requested seat. If the output argument is set, the + compositor should map the input device to the requested output. + </description> + <arg name="seat" type="object" interface="wl_seat" allow-null="true"/> + <arg name="output" type="object" interface="wl_output" allow-null="true"/> + <arg name="id" type="new_id" interface="zwlr_virtual_pointer_v1"/> + </request> + </interface> +</protocol> diff --git a/qml/DesktopGridView.qml b/qml/DesktopGridView.qml new file mode 100644 index 0000000..c5bf588 --- /dev/null +++ b/qml/DesktopGridView.qml @@ -0,0 +1,116 @@ +import QtQuick +import QtQuick.Controls +import org.kazoe.xdg + +ListView { + id: desktopgridview + property string category + property int nbrow + clip: true + orientation: ListView.Horizontal + anchors.margins: 5 + height: 150 + spacing: 15 + + + model: DesktopFilter { + id: filtermodel + model: xdgEntries + category: desktopgridview.category + onRowsInserted: { + desktopgridview.nbrow = rowCount() + } + + onRowsRemoved: { + desktopgridview.nbrow = rowCount() + } + + Component.onCompleted: { + desktopgridview.nbrow = rowCount() + } + } + + delegate: Rectangle { + property string applicationIcon: appIcon + property string applicationTitle: name + property string xdgPath: path + property alias highlighted: btn.highlighted + id: entryItem + width: desktopgridview.height * 0.8 + height: desktopgridview.height * 0.8 + highlighted: keywords.includes("running") + color: (btn.highlighted)?(currentTheme.highlight):(currentTheme.window) + radius: 4 + + Button { + id: btn + icon.source: "file://" + entryItem.applicationIcon + icon.name: entryItem.applicationIcon + icon.width: btn.width + icon.height: btn.height + + font.pixelSize: 12 + font.bold: true + height: entryItem.height * 0.5 + width: entryItem.width * 0.5 + anchors.top: parent.top + anchors.topMargin: 10 + anchors.horizontalCenter: parent.horizontalCenter + + background: Rectangle { + color: "transparent" + } + } + + Text { + anchors.top: btn.bottom + anchors.left: parent.left + anchors.right: parent.right + height: entryItem.height * 0.4 + text: entryItem.applicationTitle + font: btn.font + fontSizeMode: Text.Fit + opacity: enabled ? 1.0 : 0.3 + color: (btn.highlighted)?("white"):("grey") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + MouseArea { + id: zone + anchors.fill: parent + enabled: parent.enabled + property bool locked: false + + onClicked: { + if(zone.locked) + zone.locked = false + else + xdgEntries.start(entryItem.xdgPath) + } + + + Timer { + id: longPressTimer + + interval: 1000 + repeat: false + running: false + + onTriggered: { + xdgEntries.stop(entryItem.xdgPath) + zone.locked = true + } + } + + onPressedChanged: { + if ( pressed ) { + longPressTimer.running = true; + } else { + longPressTimer.running = false; + } + } + } + } +} diff --git a/qml/HomePage.qml b/qml/HomePage.qml new file mode 100644 index 0000000..fbcfd70 --- /dev/null +++ b/qml/HomePage.qml @@ -0,0 +1,55 @@ +import QtQuick + +Rectangle { + id: homepage + clip: true + property string selected: "tests" + + ListView { + id: category + anchors.fill: parent + model: xdgEntries.categories + delegate: Rectangle { + Component.onCompleted: { + console.debug("MANAGE CATEGORY [" + modelData + "]") + } + + property bool isshow: modelData !== "System" && modelData !== "Debug" + width: homepage.width + height: (isshow) ? (entriesView.nbrow > 0 ? entriesView.height + categoryTitle.height : 0) : 0 + color: currentTheme.base + visible: isshow && entriesView.nbrow > 0 + + Rectangle { + id: categoryTitle + height: 20 + width: parent.width - 2 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 4 + anchors.topMargin: 4 + color: currentTheme.mid + + + Text { + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + text: modelData + font.pixelSize: 15 + font.bold: true + color: currentTheme.windowText + } + } + + DesktopGridView { + id: entriesView + category: modelData + anchors.top: categoryTitle.bottom + anchors.left: parent.left + anchors.right: parent.right + } + } + } +} diff --git a/qml/Main.qml b/qml/Main.qml new file mode 100644 index 0000000..c3470fc --- /dev/null +++ b/qml/Main.qml @@ -0,0 +1,167 @@ +import QtQuick +import QtWayland.Compositor +import QtWayland.Compositor.XdgShell +import QtQuick.Window +import QtQuick.Controls +import org.kazoe.desktop +import org.kazoe.xdg + +WaylandCompositor { + id: comp + property string defaultTitle: "" + property int appHeight: win.height - statusBar.height + property int appWidth: win.width + property bool showHomepage: true + property string statusBarModel: "StatusBar" + property alias statusBarHeight: statusBar.height + + SystemPalette { + id: currentTheme + colorGroup: SystemPalette.Active + } + + DesktopEntries { + id: xdgEntries + + onRaiseProcess: { + manager.currentSurface = waylandEntries.getSurface(pid) + } + } + + AutoStart { + id: autostarter + Component.onCompleted: { + autostarter.start("Desktop Entry/X-DESKTOPMGR-AUTOSTART", "true") + } + } + + DesktopFilter { + id: sysbar + section: "X-DESKTOPMGR-SYSBAR" + showAll: true + model: xdgEntries + } + + DesktopFilter { + id: systray + section: "X-DESKTOPMGR-SYSTRAY" + showAll: true + model: xdgEntries + } + + WaylandEntries { + id: entries + } + + Connections { + target: manager + function onCurrentSurfaceChanged() { + comp.showHomepage = false + } + + function onGotoHome() { + comp.showHomepage = true + } + } + + + WaylandOutput { + id: output + sizeFollowsWindow: true + transform: WaylandOutput.Transform90 + scaleFactor: 4 + window: Window { + id: win + visible: true + width: 800 + height: 480 + + VKeyboard { + id: keyboard + anchors.fill: parent + } + + Flickable { + anchors.fill: parent + id: appZoneView + clip: true + contentWidth: win.width + contentHeight: win.height + flickableDirection: Flickable.VerticalFlick + interactive: false + + property int textInputY: keyboard.textInputY + property int textInputHeight: keyboard.textInputHeight + property bool keyboardActive: keyboard.active + property int keyboardHeight: keyboard.implicitHeight + property int wantOffset: (appZoneView.height - keyboardHeight) / 2 + + onTextInputYChanged: { + if(appZoneView.keyboardActive && appZoneView.contentY == 0) + { + if(textInputY > (appZoneView.height - keyboardHeight)) + { + appZoneView.contentY = textInputY - textInputHeight - wantOffset + } + } + } + + onKeyboardActiveChanged: { + appZoneView.contentY = 0 + } + + StatusBar { + id: statusBar + width: parent.width + z: 20 + } + + HomePage { + id: homepageview + anchors.top: statusBar.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + visible: comp.showHomepage + } + + ShellSurfaceItem { + id: appZone + visible: !comp.showHomepage + anchors.top: statusBar.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + shellSurface: manager.currentSurface + autoCreatePopupItems: true + + onWidthChanged: { + if(manager.currentSurface) + manager.currentSurface.toplevel.sendResizing(Qt.size(appZone.width, appZone.height)) + } + + onHeightChanged: { + if(manager.currentSurface) + manager.currentSurface.toplevel.sendResizing(Qt.size(appZone.width, appZone.height)) + } + } + } + } + } + + XdgShell { + onToplevelCreated: function(toplevel, xdgSurface) { + toplevel.sendResizing(Qt.size(appZone.width, appZone.height)); + entries.addSurface(xdgSurface) + manager.currentSurface = xdgSurface + } + } + + XdgDecorationManagerV1 { + preferredMode: XdgToplevel.ServerSideDecoration + } + + // Extension for Input Method (QT_IM_MODULE) support at compositor-side + TextInputManager {} +} diff --git a/qml/StatusBar.qml b/qml/StatusBar.qml new file mode 100644 index 0000000..3758657 --- /dev/null +++ b/qml/StatusBar.qml @@ -0,0 +1,149 @@ +import QtQuick +import QtQuick.Controls +import org.kazoe.desktop +import org.kazoe.xdg + +Rectangle { + id: statusBar + height: 46 + property int batteryLevelWarning: 15 + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + gradient: Gradient { + GradientStop { position: 0.0; color: currentTheme.window } + GradientStop { position: 1.0; color: currentTheme.window } + } + + Rectangle { + id: bottomline + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 1 + width: undefined + color: "black" + } + + Button { + id: backHome + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + height: comp.statusBarHeight + anchors.margins: 4 + palette.buttonText: "white" + font.pointSize: 9 + highlighted: comp.showHomepage + display: AbstractButton.IconOnly + + icon.name: "home" + + background: Rectangle { + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + width: backHome.width + height: backHome.height + border.color: backHome.highlighted ? currentTheme.highlight : currentTheme.alternateBase + border.width: backHome.highlighted ? 2 : 1 + radius: 4 + color: currentTheme.mid + } + + onClicked: { + comp.showHomepage = true + } + } + + ListView { + id: taskbar + anchors.left: backHome.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: systrayview.left + anchors.leftMargin: 4 + anchors.topMargin: 4 + clip: true + model: entries + orientation: Qt.Horizontal + spacing: 4 + + delegate: Button { + id: taskapp + palette.buttonText: "white" + font.pointSize: 9 + highlighted: (comp.showHomepage === false) && (manager.currentSurface === wl_surface) + + height: taskbar.height - 4 + display: AbstractButton.TextBesideIcon + /* + * TODO: Find app icon with wayland + icon.name: wl_surface.surface + icon.color: "#0aa4da" + */ + Entry { + id: xdgapp + desktopFile: "/usr/share/applications/" + appId + ".desktop" + } + icon.name: xdgapp.icon + icon.color: "transparent" + + background: Rectangle { + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + width: taskapp.width + height: taskapp.height + border.color: taskapp.highlighted ? currentTheme.highlight : currentTheme.alternateBase + border.width: taskapp.highlighted ? 2 : 1 + radius: 4 + color: currentTheme.mid + } + text: title + + onClicked: { + manager.currentSurface = wl_surface + comp.showHomepage = false + } + + Timer { + id: longPressTimer + + interval: 1000 + repeat: false + running: false + + onTriggered: { + if(manager.currentSurface == wl_surface) + { + comp.showHomepage = true + } + + entries.stop(pid) + } + } + + onPressedChanged: { + if ( pressed ) { + longPressTimer.running = true; + } else { + longPressTimer.running = false; + } + } + } + } + + SystrayView { + id: systrayview + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.topMargin: 2 + anchors.bottomMargin: 2 + anchors.rightMargin: 2 + + width: systrayview.contentWidth + spacing: 1 + } +} diff --git a/qml/SystrayView.qml b/qml/SystrayView.qml new file mode 100644 index 0000000..3b21152 --- /dev/null +++ b/qml/SystrayView.qml @@ -0,0 +1,32 @@ +import QtQuick +import QtQuick.Controls +import QtWayland.Compositor +import org.kazoe.desktop +import org.kazoe.xdg + +ListView { + id: systrayView + width: systrayView.contentWidth + orientation: ListView.Horizontal + model: systray + delegate: Item { + id: trayitem + + Component.onCompleted: { + var qmlcode + var c + if(exec.startsWith("qml://")) { + var ur = exec.split("/") + qmlcode = "import " + ur[2] + " " + ur[3] + "; " + ur[4] + " {}" + c = Qt.createQmlObject(qmlcode, trayitem, ur[4]) + } + else + { + console.debug("Item not supported for systray:" + exec) + } + + trayitem.width = Qt.binding(() => {return c.width}) + trayitem.height = Qt.binding(() => {return c.height}) + } + } +} diff --git a/qml/VKeyboard.qml b/qml/VKeyboard.qml new file mode 100644 index 0000000..c97e120 --- /dev/null +++ b/qml/VKeyboard.qml @@ -0,0 +1,14 @@ +import QtQuick +import QtQuick.VirtualKeyboard +import QtQuick.VirtualKeyboard.Settings +import QtQuick.Window + +InputPanel { + id: panel + visible: active + y: active ? 0 : parent.height + anchors.left: parent.left + anchors.right: parent.right + property int textInputHeight: InputContext.cursorRectangle.height + property int textInputY: InputContext.cursorRectangle.y +} diff --git a/src/applicationmanager.cpp b/src/applicationmanager.cpp new file mode 100644 index 0000000..13158f3 --- /dev/null +++ b/src/applicationmanager.cpp @@ -0,0 +1,127 @@ +#include "applicationmanager.h" +#include "waylandentries.h" +#include <QWaylandXdgSurface> +#include <QWaylandXdgToplevel> + +ApplicationManager::ApplicationManager(QObject *parent) + : QObject(parent) +{ + +} + +bool ApplicationManager::isHome() const +{ + return m_isHome; +} + +QObject *ApplicationManager::currentSurface() const +{ + return m_currentSurface; +} + +void ApplicationManager::setCurrentSurface(QObject *newCurrentSurface) +{ + if (m_currentSurface == newCurrentSurface) + { + emit currentSurfaceChanged(); + return; + } + m_currentSurface = newCurrentSurface; + emit currentSurfaceChanged(); +} + +int ApplicationManager::count() +{ + WaylandEntries *entries = WaylandEntries::getInstance(); + if(!entries) + return 0; + return entries->rowCount(); +} + +QString ApplicationManager::applicationName(int id) +{ + WaylandEntries *entries = WaylandEntries::getInstance(); + if(!entries) + return ""; + return entries->data(entries->index(id), WaylandEntries::TitleRole).toString(); +} + +int ApplicationManager::applicationPid(int id) +{ + WaylandEntries *entries = WaylandEntries::getInstance(); + if(!entries) + return 0; + return entries->data(entries->index(id), WaylandEntries::PidRole).toInt(); +} + +int ApplicationManager::applicationFocus(int id) +{ + WaylandEntries *entries = WaylandEntries::getInstance(); + if(!entries) + return -1; + if(id >= entries->rowCount()) + return -2; + QVariant d = entries->data(entries->index(id), WaylandEntries::SurfaceRole); + if(!d.isValid()) + return -3; + QWaylandXdgSurface *surface = qvariant_cast<QWaylandXdgSurface *>(d); + if(surface) + { + setCurrentSurface(surface); + return 0; + } + return -4; +} + +int ApplicationManager::currentApplicationPid() +{ + if(isHome()) + return -1; + WaylandEntries *entries = WaylandEntries::getInstance(); + for(int i = 0; i < entries->rowCount(); i++) + { + QVariant d = entries->data(entries->index(i), WaylandEntries::SurfaceRole); + QWaylandXdgSurface *surface = qvariant_cast<QWaylandXdgSurface *>(d); + QWaylandXdgSurface *current = qobject_cast<QWaylandXdgSurface*>(currentSurface()); + if(surface != current) + continue; + return entries->data(entries->index(i), WaylandEntries::PidRole).toInt(); + } + return -2; +} + +void ApplicationManager::hideApplicationPid(qint64 pid) +{ + WaylandEntries *entries = WaylandEntries::getInstance(); + entries->addHidePid(pid); +} + +void ApplicationManager::raiseApplicationPid(qint64 pid) +{ + WaylandEntries *entries = WaylandEntries::getInstance(); + QObject *d = entries->getSurface(pid); + QWaylandXdgSurface *surface = qobject_cast<QWaylandXdgSurface *>(d); + if(surface) + { + setCurrentSurface(surface); + } +} + +void ApplicationManager::backHome() +{ + emit gotoHome(); +} + +void ApplicationManager::setIsHome(bool isHome) +{ + if (m_isHome == isHome) + return; + + m_isHome = isHome; + emit isHomeChanged(m_isHome); +} + +QString ApplicationManager::virtualkeyboard() +{ + return qgetenv("QT_IM_MODULE"); +} diff --git a/src/applicationmanager.h b/src/applicationmanager.h new file mode 100644 index 0000000..b4568f0 --- /dev/null +++ b/src/applicationmanager.h @@ -0,0 +1,43 @@ +#ifndef APPLICATIONMANAGER_H +#define APPLICATIONMANAGER_H + +#include <QObject> + +class ApplicationManager : public QObject +{ + Q_OBJECT + Q_PROPERTY(QObject *currentSurface READ currentSurface WRITE setCurrentSurface NOTIFY currentSurfaceChanged) + Q_PROPERTY(bool isHome READ isHome WRITE setIsHome NOTIFY isHomeChanged) + Q_PROPERTY(QString virtualkeyboard READ virtualkeyboard CONSTANT) + + QObject *m_currentSurface {nullptr}; + bool m_isHome; + +public: + explicit ApplicationManager(QObject *parent = nullptr); + + bool isHome() const; + +public slots: + QObject *currentSurface() const; + void setCurrentSurface(QObject *newCurrentSurface); + + int count(); + QString applicationName(int id); + int applicationPid(int id); + int applicationFocus(int id); + int currentApplicationPid(); + void hideApplicationPid(qint64 pid); + void raiseApplicationPid(qint64 pid); + void backHome(); + void setIsHome(bool isHome); + + QString virtualkeyboard(); + +signals: + void currentSurfaceChanged(); + void gotoHome(); + void isHomeChanged(bool isHome); +}; + +#endif // APPLICATIONMANAGER_H diff --git a/src/debug.cpp b/src/debug.cpp new file mode 100644 index 0000000..a07d815 --- /dev/null +++ b/src/debug.cpp @@ -0,0 +1,6 @@ +#include "debug.h" + +Q_LOGGING_CATEGORY(DESKTOP_MGR_CORE, "desktopmgr.core") +Q_LOGGING_CATEGORY(DESKTOP_MGR_SCREEN_COPY, "desktopmgr.screencopy") +Q_LOGGING_CATEGORY(DESKTOP_MGR_VNC_KEYBOARD, "desktopmgr.vnckeyboard") +Q_LOGGING_CATEGORY(DESKTOP_MGR_VIRTUAL_POINTER, "desktopmgr.virtualpointer") diff --git a/src/debug.h b/src/debug.h new file mode 100644 index 0000000..803192d --- /dev/null +++ b/src/debug.h @@ -0,0 +1,11 @@ +#ifndef DEBUG_H +#define DEBUG_H + +#include <QLoggingCategory> + +Q_DECLARE_LOGGING_CATEGORY(DESKTOP_MGR_CORE) +Q_DECLARE_LOGGING_CATEGORY(DESKTOP_MGR_SCREEN_COPY) +Q_DECLARE_LOGGING_CATEGORY(DESKTOP_MGR_VNC_KEYBOARD) +Q_DECLARE_LOGGING_CATEGORY(DESKTOP_MGR_VIRTUAL_POINTER) + +#endif // DEBUG_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ca19b05 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,70 @@ +#include <QGuiApplication> +#include <QQmlApplicationEngine> +#include <QQmlContext> +#include <QDBusConnection> +#include <QIcon> +#include <QTimer> +#include <QThread> +#include <systemd/sd-daemon.h> +#include <QDir> +#include <QFileInfo> +#include <xdgstatusnotifierwatcher.h> +#include "applicationmanager.h" +#include "applicationsadaptor.h" +#include "waylandentries.h" +#include "waylandentriesfiltered.h" + +int main(int argc, char *argv[]) +{ + qputenv("QT_IM_MODULE", "qtvirtualkeyboard"); + QDBusConnection bus = QDBusConnection::sessionBus(); + QGuiApplication app(argc, argv); + ApplicationManager manager; + QTimer watchdog; + uint64_t watchdogPeriodUSec; + + QFileInfo volatilapps("/var/volatile/applications"); + if(!volatilapps.exists()) + { + QDir().mkdir("/var/volatile/applications"); + } + + qmlRegisterType<WaylandEntries>("org.kazoe.desktop", 1, 0, "WaylandEntries"); + qmlRegisterType<WaylandEntriesFiltered>("org.kazoe.desktop", 1, 0, "WaylandEntriesFiltered"); + + if (!bus.isConnected()) { + qWarning("Cannot connect to the D-Bus session bus.\n" + "Please check your system settings and try again.\n"); + return 1; + } + + QQmlApplicationEngine engine; + new ApplicationsAdaptor(&manager); + bus.registerObject("/applications", &manager); + + + bool conres = bus.registerService("org.kazoe.desktop"); + if(!conres) qWarning() << "Can't register dbus service org.kazoe.desktop"; + + KaZoe::StatusNotifierWatcher notifierWatcher; + + engine.rootContext()->setContextProperty("manager", &manager); + engine.loadFromModule("org.kazoe.desktop", "Main"); + + // signal systemd that the app the ready + QTimer::singleShot(0, [] () { + sd_notify(0, "READY=1"); + } + ); + + // setup a timer to refresh the systemd'd watchdog if needed + if (sd_watchdog_enabled(0,&watchdogPeriodUSec) > 0) { + QObject::connect(&watchdog, &QTimer::timeout, [] () { + sd_notify(0, "WATCHDOG=1"); + } + ); + } + // configure the period of the QT timer for a half period of the watchdog timer + watchdog.start(watchdogPeriodUSec / 2000); + return app.exec(); +} diff --git a/src/org.freedesktop.DBus.xml b/src/org.freedesktop.DBus.xml new file mode 100644 index 0000000..68d5262 --- /dev/null +++ b/src/org.freedesktop.DBus.xml @@ -0,0 +1,17 @@ +<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> +<node> + <interface name="org.freedesktop.DBus.Properties"> + <method name="GetAll"> + <arg name="interface_name" type="s" direction="in"/> + <arg name="properties" type="a{sv}" direction="out"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/> + </method> + <signal name="PropertiesChanged"> + <arg name="interface_name" type="s" direction="out"/> + <arg name="changed_properties" type="a{sv}" direction="out"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/> + <arg name="invalidated_properties" type="as" direction="out"/> + </signal> + </interface> +</node> diff --git a/src/vnc-keyboard-unstable-v1.cpp b/src/vnc-keyboard-unstable-v1.cpp new file mode 100644 index 0000000..5dc8dd7 --- /dev/null +++ b/src/vnc-keyboard-unstable-v1.cpp @@ -0,0 +1,146 @@ +#include <QDebug> +#include <QGuiApplication> +#include <wayland-server-protocol.h> +#include <linux/input-event-codes.h> +#include "vnc-keyboard-unstable-v1.h" + + +void toQtKey(uint32_t key, uint32_t *pkeycode, uint32_t *punicode) +{ + static const QMap<int, int> keyMap { + { 0xff08, Qt::Key_Backspace }, + { 0xff09, Qt::Key_Tab }, + { 0xff0d, Qt::Key_Return }, + { 0xff1b, Qt::Key_Escape }, + { 0xff63, Qt::Key_Insert }, + { 0xffff, Qt::Key_Delete }, + { 0xff50, Qt::Key_Home }, + { 0xff57, Qt::Key_End }, + { 0xff55, Qt::Key_PageUp }, + { 0xff56, Qt::Key_PageDown }, + { 0xff51, Qt::Key_Left }, + { 0xff52, Qt::Key_Up }, + { 0xff53, Qt::Key_Right }, + { 0xff54, Qt::Key_Down }, + { 0xffbe, Qt::Key_F1 }, + { 0xffbf, Qt::Key_F2 }, + { 0xffc0, Qt::Key_F3 }, + { 0xffc1, Qt::Key_F4 }, + { 0xffc2, Qt::Key_F5 }, + { 0xffc3, Qt::Key_F6 }, + { 0xffc4, Qt::Key_F7 }, + { 0xffc5, Qt::Key_F8 }, + { 0xffc6, Qt::Key_F9 }, + { 0xffc7, Qt::Key_F10 }, + { 0xffc8, Qt::Key_F11 }, + { 0xffc9, Qt::Key_F12 }, + { 0xffe1, Qt::Key_Shift }, + { 0xffe2, Qt::Key_Shift }, + { 0xffe3, Qt::Key_Control }, + { 0xffe4, Qt::Key_Control }, + { 0xffe7, Qt::Key_Meta }, + { 0xffe8, Qt::Key_Meta }, + { 0xffe9, Qt::Key_Alt }, + { 0xffea, Qt::Key_Alt }, + + { 0xffb0, Qt::Key_0 }, + { 0xffb1, Qt::Key_1 }, + { 0xffb2, Qt::Key_2 }, + { 0xffb3, Qt::Key_3 }, + { 0xffb4, Qt::Key_4 }, + { 0xffb5, Qt::Key_5 }, + { 0xffb6, Qt::Key_6 }, + { 0xffb7, Qt::Key_7 }, + { 0xffb8, Qt::Key_8 }, + { 0xffb9, Qt::Key_9 }, + + { 0xff8d, Qt::Key_Return }, + { 0xffaa, Qt::Key_Asterisk }, + { 0xffab, Qt::Key_Plus }, + { 0xffad, Qt::Key_Minus }, + { 0xffae, Qt::Key_Period }, + { 0xffaf, Qt::Key_Slash }, + + { 0xff95, Qt::Key_Home }, + { 0xff96, Qt::Key_Left }, + { 0xff97, Qt::Key_Up }, + { 0xff98, Qt::Key_Right }, + { 0xff99, Qt::Key_Down }, + { 0xff9a, Qt::Key_PageUp }, + { 0xff9b, Qt::Key_PageDown }, + { 0xff9c, Qt::Key_End }, + { 0xff9e, Qt::Key_Insert }, + { 0xff9f, Qt::Key_Delete }, + }; + uint32_t unicode = 0; + uint32_t keycode = keyMap.value(key, 0); + + if (keycode >= ' ' && keycode <= '~') + unicode = keycode; + + if (!keycode) { + if (key <= 0xff) { + unicode = key; + if (key >= 'a' && key <= 'z') + keycode = Qt::Key_A + key - 'a'; + else if (key >= ' ' && key <= '~') + keycode = Qt::Key_Space + key - ' '; + } + } + *pkeycode = keycode; + *punicode = unicode; +} + + +VncKeyboardManager::VncKeyboardManager(QWaylandCompositor *compositor) + :QWaylandCompositorExtensionTemplate(compositor) +{ +} + +void VncKeyboardManager::initialize() +{ + QWaylandCompositorExtensionTemplate::initialize(); + QWaylandCompositor *compositor = static_cast<QWaylandCompositor *>(extensionContainer()); + init(compositor->display(), 1); +} + +void VncKeyboardManager::vnc_virtual_keyboard_manager_v1_create_virtual_keyboard(Resource *resource, uint32_t id) +{ + new VncKeyboard(m_window, resource->client(), id, 1); +} + +VncKeyboard::VncKeyboard(QQuickWindow *window, struct ::wl_client *client, int id, int version): + QtWaylandServer::vnc_keyboard_v1(client, id, version), + m_window(window), + m_keymod(Qt::NoModifier) +{ +} + +void VncKeyboard::vnc_keyboard_v1_key(Resource *resource, uint32_t time, uint32_t key, uint32_t state) +{ + bool down = !!state; + uint32_t keycode, unicode; + + toQtKey(key, &keycode, &unicode); + + if (keycode == Qt::Key_Shift) + m_keymod = down ? m_keymod | Qt::ShiftModifier : + m_keymod & ~Qt::ShiftModifier; + else if (keycode == Qt::Key_Control) + m_keymod = down ? m_keymod | Qt::ControlModifier : + m_keymod & ~Qt::ControlModifier; + else if (keycode == Qt::Key_Alt) + m_keymod = down ? m_keymod | Qt::AltModifier : + m_keymod & ~Qt::AltModifier; + if (unicode || keycode) { + QKeyEvent event(down ? QEvent::KeyPress : QEvent::KeyRelease, keycode, m_keymod, QString(QChar(unicode))); + QGuiApplication::sendEvent(m_window, &event); + } else { + qDebug() << __func__ << " no unicode or keycode. keysym is " << key << ". state is " << state; + } +} + +void VncKeyboard::vnc_keyboard_v1_destroy(Resource *) +{ + delete this; +} diff --git a/src/vnc-keyboard-unstable-v1.h b/src/vnc-keyboard-unstable-v1.h new file mode 100644 index 0000000..3728b3f --- /dev/null +++ b/src/vnc-keyboard-unstable-v1.h @@ -0,0 +1,46 @@ +#ifndef VIRTUALKEYBOARD_H +#define VIRTUALKEYBOARD_H + +#include "wayland-util.h" +#include <QQuickWindow> +#include <QtWaylandCompositor/QWaylandCompositorExtensionTemplate> +#include <QtWaylandCompositor/QWaylandQuickExtension> +#include <QtWaylandCompositor/QWaylandCompositor> +#include "qwayland-server-vnc-keyboard-unstable-v1.h" + +class VncKeyboardManager : public QWaylandCompositorExtensionTemplate<VncKeyboardManager> + , public QtWaylandServer::vnc_virtual_keyboard_manager_v1 +{ + Q_OBJECT + Q_PROPERTY(QQuickWindow *window MEMBER m_window); + +public: + VncKeyboardManager(QWaylandCompositor *compositor = nullptr); + void initialize() override; + +protected: + void vnc_virtual_keyboard_manager_v1_create_virtual_keyboard(Resource *resource, uint32_t id) override; + +private: + QQuickWindow *m_window; +}; + +class VncKeyboard : public QWaylandCompositorExtensionTemplate<VncKeyboard> + , public QtWaylandServer::vnc_keyboard_v1 +{ + Q_OBJECT +public: + VncKeyboard(QQuickWindow *window, struct ::wl_client *client, int id, int version); + +protected: + void vnc_keyboard_v1_key(Resource *resource, uint32_t time, uint32_t key, uint32_t state) override; + void vnc_keyboard_v1_destroy(Resource *resource) override; + +private: + QQuickWindow *m_window; + Qt::KeyboardModifiers m_keymod; +}; + +Q_COMPOSITOR_DECLARE_QUICK_EXTENSION_CLASS(VncKeyboardManager) + +#endif // VIRTUALKEYBOARD_H diff --git a/src/waylandentries.cpp b/src/waylandentries.cpp new file mode 100644 index 0000000..42808bb --- /dev/null +++ b/src/waylandentries.cpp @@ -0,0 +1,159 @@ +#include "waylandentries.h" +#include <QWaylandXdgSurface> +#include <QWaylandSurface> +#include <QMutexLocker> +#include <qdebug.h> + +WaylandEntries *m_instance = nullptr; + +WaylandEntries::WaylandEntries(QObject *parent) + : QAbstractListModel(parent) +{ + if(m_instance != nullptr) + { + qWarning() << "Many instance of WaylandEntries"; + } + m_instance = this; +} + +int WaylandEntries::rowCount(const QModelIndex &parent) const +{ + QMutexLocker locker(&m_lock); + return m_surfaces.count(); +} + +QVariant WaylandEntries::data(const QModelIndex &index, int role) const +{ + QMutexLocker locker(&m_lock); + if(index.row() > m_surfaces.count()) + return QVariant(); + + QWaylandXdgSurface *surface = m_surfaces[index.row()]; + if(surface == nullptr) + return QVariant(); + + switch(role) { + case Qt::DisplayRole: + case TitleRole: + return surface->toplevel()->title(); + case AppIdRole: + return surface->toplevel()->appId(); + case SurfaceRole: + return QVariant::fromValue<QWaylandXdgSurface*>(surface); + case PidRole: + return surface->surface()->client()->processId(); + } + + return QVariant(); +} + +QHash<int, QByteArray> WaylandEntries::roleNames() const +{ + QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); + roles[SurfaceRole] = "wl_surface"; + roles[TitleRole] = "title"; + roles[PidRole] = "pid"; + roles[AppIdRole] = "appId"; + return roles; +} + +// Need only for application manager interface for cli +WaylandEntries *WaylandEntries::getInstance() +{ + return m_instance; +} + +void WaylandEntries::addSurface(QObject *obj) +{ + QMutexLocker locker(&m_lock); + QWaylandXdgSurface *s = qobject_cast<QWaylandXdgSurface*>(obj); + if(s) + { + QObject::connect(s, &QWaylandXdgSurface::destroyed, this, &WaylandEntries::surfaceDestroyed); + if(m_hidden.contains(s->surface()->client()->processId())) + { + m_surfaces_hidden.append(s); + } + else + { + int id = m_surfaces.count(); + beginInsertRows(QModelIndex(), m_surfaces.count(), m_surfaces.count()); + QObject::connect(s->toplevel(), &QWaylandXdgToplevel::titleChanged, this, [this, id](){ + const QVector<int> roles = {TitleRole}; + emit dataChanged(index(id), index(id), roles); + }); + QObject::connect(s->toplevel(), &QWaylandXdgToplevel::appIdChanged, this, [this, id, s](){ + const QVector<int> roles = {AppIdRole}; + + emit topWindowRegistered(s, s->toplevel()->appId()); + emit dataChanged(index(id), index(id), roles); + }); + m_surfaces.append(s); + endInsertRows(); + emit nbAppsChanged(); + } + } +} + +void WaylandEntries::addHidePid(qint64 pid) +{ + QMutexLocker locker(&m_lock); + m_hidden.append(pid); + QWaylandXdgSurface *surfaceToHide = nullptr; + + for(QWaylandXdgSurface *surface: m_surfaces) + { + if(surface->surface()->client()->processId() == pid) + { + surfaceToHide = surface; + break; + } + } + if(surfaceToHide) + { + beginRemoveRows(QModelIndex(), m_surfaces.indexOf(surfaceToHide), m_surfaces.indexOf(surfaceToHide)); + m_surfaces.removeAll(surfaceToHide); + endRemoveRows(); + m_surfaces_hidden.append(surfaceToHide); + emit nbAppsChanged(); + } +} + +void WaylandEntries::surfaceDestroyed() +{ + QObject *obj = QObject::sender(); + QWaylandXdgSurface *s = static_cast<QWaylandXdgSurface*>(obj); + emit topWindowDestroyed(s, s->toplevel()->appId()); + QMutexLocker locker(&m_lock); + beginRemoveRows(QModelIndex(), m_surfaces.indexOf(s), m_surfaces.indexOf(s)); + m_surfaces.removeAll(s); + endRemoveRows(); + emit nbAppsChanged(); +} + +void WaylandEntries::stop(int pid) +{ + kill(pid, SIGTERM); +} + +QObject *WaylandEntries::getSurface(int pid) +{ + QMutexLocker locker(&m_lock); + for(QWaylandXdgSurface *surface: m_surfaces) + { + if(surface->surface()->client()->processId() == pid) + return surface; + } + for(QWaylandXdgSurface *surface: m_surfaces_hidden) + { + if(surface->surface()->client()->processId() == pid) + return surface; + } + return nullptr; +} + +int WaylandEntries::nbApps() const +{ + QMutexLocker locker(&m_lock); + return m_surfaces.count(); +} diff --git a/src/waylandentries.h b/src/waylandentries.h new file mode 100644 index 0000000..ce98ade --- /dev/null +++ b/src/waylandentries.h @@ -0,0 +1,53 @@ +#ifndef WAYLANDENTRIES_H +#define WAYLANDENTRIES_H + +#include <QAbstractListModel> +#include <QRecursiveMutex> +#include <QObject> + +class QWaylandXdgSurface; + +class WaylandEntries : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int nbApps READ nbApps NOTIFY nbAppsChanged) + + + QList<QWaylandXdgSurface *> m_surfaces; + QList<QWaylandXdgSurface *> m_surfaces_hidden; + QList<qint64> m_hidden; + mutable QRecursiveMutex m_lock; + +public: + explicit WaylandEntries(QObject *parent = nullptr); + + enum WaylandEntryRoles { + SurfaceRole = Qt::UserRole + 1, + TitleRole, + PidRole, + AppIdRole + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + static WaylandEntries* getInstance(); + + int nbApps() const; + void setNbApps(int newNbApps); + +public slots: + void addSurface(QObject *obj); + void addHidePid(qint64 pid); + void surfaceDestroyed(); + void stop(int pid); + QObject *getSurface(int pid); + +signals: + void nbAppsChanged(); + void topWindowRegistered(QObject *surface, QString appid); + void topWindowDestroyed(QObject *surface, QString appid); +}; + +#endif // WAYLANDENTRIES_H diff --git a/src/waylandentriesfiltered.cpp b/src/waylandentriesfiltered.cpp new file mode 100644 index 0000000..f1f2ebc --- /dev/null +++ b/src/waylandentriesfiltered.cpp @@ -0,0 +1,63 @@ +#include "waylandentriesfiltered.h" +#include "waylandentries.h" +#include "xdgdesktopentries.h" +#include <QDebug> + +WaylandEntriesFiltered::WaylandEntriesFiltered(QObject *parent) + : QSortFilterProxyModel{parent} +{ + m_entries = new KaZoe::DesktopEntries(this); + QObject::connect(this, &WaylandEntriesFiltered::rowsInserted, this, &WaylandEntriesFiltered::lengthChanged); + QObject::connect(this, &WaylandEntriesFiltered::rowsRemoved, this, &WaylandEntriesFiltered::lengthChanged); +} + +QString WaylandEntriesFiltered::roleFilter() const +{ + return m_roleFilter.join(";"); +} + +void WaylandEntriesFiltered::setRoleFilter(const QString &roles) +{ + QStringList newlist = roles.split(";"); + if (m_roleFilter == newlist) + return; + m_roleFilter = newlist; + emit roleFilterChanged(); +} + +bool WaylandEntriesFiltered::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + QVariant appid = sourceModel()->data(index, WaylandEntries::AppIdRole); + QString role = m_entries->appIdData(appid.toString(), "Desktop Entry/X-DESKTOPMGR-ROLE"); + + if(appid.isNull()) + return false; + + if(m_roleFilter.contains(role)) + { + return false; + } + return true; +} + +QObject *WaylandEntriesFiltered::model() const +{ + return static_cast<QObject*>(m_model); +} + +void WaylandEntriesFiltered::setModel(QObject *newModel) +{ + WaylandEntries *entries = qobject_cast<WaylandEntries*>(newModel); + if (entries) + { + m_model = entries; + setSourceModel(m_model); + emit modelChanged(m_model); + } +} + +int WaylandEntriesFiltered::length() +{ + return rowCount(); +} diff --git a/src/waylandentriesfiltered.h b/src/waylandentriesfiltered.h new file mode 100644 index 0000000..e562a7d --- /dev/null +++ b/src/waylandentriesfiltered.h @@ -0,0 +1,41 @@ +#ifndef WAYLANDENTIRESFILTERED_H +#define WAYLANDENTIRESFILTERED_H + +#include <QSortFilterProxyModel> +#include <QObject> + +class WaylandEntries; +namespace KaZoe { + class DesktopEntries; +} + +class WaylandEntriesFiltered : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QObject *model READ model WRITE setModel NOTIFY modelChanged) + Q_PROPERTY(QString roleFilter READ roleFilter WRITE setRoleFilter NOTIFY roleFilterChanged) + Q_PROPERTY(int length READ length NOTIFY lengthChanged) + KaZoe::DesktopEntries *m_entries {nullptr}; + +public: + explicit WaylandEntriesFiltered(QObject *parent = nullptr); + QString roleFilter() const; + void setRoleFilter(const QString &roles); + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + + QObject *model() const; + void setModel(QObject *newModel); + + int length(); + +signals: + void roleFilterChanged(); + void modelChanged(QObject * model); + void lengthChanged(); + +private: + QStringList m_roleFilter; + WaylandEntries *m_model = nullptr; +}; + +#endif // WAYLANDENTIRESFILTERED_H diff --git a/tools/cli/main.cpp b/tools/cli/main.cpp new file mode 100644 index 0000000..caf4463 --- /dev/null +++ b/tools/cli/main.cpp @@ -0,0 +1,98 @@ +#include <QCoreApplication> +#include <QDBusConnection> +#include <QTimer> +#include "ApplicationManagerIface.h" + +static int query(OrgKazoeDesktopApplicationsInterface &iface, int argc, char *argv[]) +{ + if(argc < 2 || QString(argv[1]) == "list") + { + int current = iface.currentApplicationPid().value(); + printf(" ______________________________________________________\n"); + printf("| ID | Name | PID |\n"); + printf("|--------|----------------------------------|----------|\n"); + if(current == -1) + { + printf("|> HOME | Homepage | |\n"); + } + else + { + printf("| HOME | Homepage | |\n"); + } + + for(int i = 0; i < iface.count(); i++) + { + int pid = iface.applicationPid(i).value(); + if(current == pid) + { + printf("|> @%04i | ", i); + } + else + { + printf("| @%04i | ", i); + } + printf("%-32s |", QString(iface.applicationName(i)).mid(0,32).toStdString().c_str()); + if(pid) printf("% 9i | ", iface.applicationPid(i).value()); + else printf(" |"); + printf("\n"); + } + printf("--------------------------------------------------------\n"); + return 0; + } + + if(QString(argv[1]) == "focus" && argc == 3) + { + int id = -1; + QString ids = QString(argv[2]); + if(ids.startsWith("@")) + { + id = ids.mid(1).toInt(); + return iface.applicationFocus(id).value(); + } + for(int i = 0; i < iface.count(); i++) + { + QString name = iface.applicationName(i); + if(name == ids) + { + return iface.applicationFocus(i).value(); + } + } + return -1; + } + + if(QString(argv[1]) == "home") + { + iface.backHome(); + return 0; + } + + return -1; +} + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + QDBusConnection connection = QDBusConnection::sessionBus(); + + if (!connection.isConnected()) { + qWarning("Cannot connect to the D-Bus session bus.\n" + "Please check your system settings and try again.\n"); + return 1; + } + const QString service = "org.kazoe.desktop.applications"; + const QString path = "/applications"; + OrgKazoeDesktopApplicationsInterface iface(service, path, connection); + + QTimer::singleShot(0, [argc, argv, &iface](){ + if(query(iface, argc, argv) != 0) + { + qInfo() << "Usage:"; + qInfo() << "> desktopcli list -> List all views"; + qInfo() << "> desktopcli home -> Go to home view"; + qInfo() << "> desktopcli focus <ID | Name> -> Go to application view, by ID or Name"; + } + exit(0); + }); + + return app.exec(); +} |