QQuick item for live updating window thumbnails

New qquick item in PlasmaCore to render a live updating window
thumbnail. The implementation uses XCB to redirect the specified
window using the composite extension. This means a running compositor
is not required. Through the damage extension the item tracks changes
to the window and triggers updates of the texture. Furthermore the
item tracks geometry changes of the window to recreate the window
pixmap.

If the pixmap of the window is valid, a texture is generated from it
using the glx texture from pixmap extension. For this a new optional
dependency for glx is added. On platform where glx is not available
(e.g. Windows, Linux with OpenGL ES) this will not get compiled and
the window's icon is used instead as a fallback.

REVIEW: 112142
This commit is contained in:
Martin Gräßlin 2013-08-18 10:01:44 +02:00
parent eba2125d6e
commit 91d306d2e8
6 changed files with 554 additions and 1 deletions

View File

@ -74,7 +74,7 @@ find_package(KDE4Support REQUIRED NO_MODULE)
#optional features
find_package(X11 MODULE)
find_package(XCB MODULE COMPONENTS XCB)
find_package(XCB MODULE COMPONENTS XCB COMPOSITE DAMAGE)
set_package_properties(XCB PROPERTIES DESCRIPTION "X protocol C-language Binding"
URL "http://xcb.freedesktop.org"
TYPE RECOMMENDED
@ -131,6 +131,17 @@ set_package_properties(LibAttica PROPERTIES DESCRIPTION "Support for Get Hot New
TYPE REQUIRED
)
find_package(OpenGL)
set_package_properties(OpenGL PROPERTIES DESCRIPTION "The OpenGL libraries"
URL "http://www.opengl.org"
TYPE OPTIONAL
)
if(OPENGL_FOUND)
set(HAVE_GLX HAVE_X11)
else()
set(HAVE_GLX 0)
endif()
#########################################################################
#add_definitions(${KDE4_DEFINITIONS})

View File

@ -10,6 +10,14 @@ INCLUDE_DIRECTORIES(
)
add_definitions(-DHAVE_X11=${HAVE_X11})
if(XCB_XCB_FOUND AND XCB_COMPOSITE_FOUND AND XCB_DAMAGE_FOUND)
add_definitions(-DHAVE_XCB_COMPOSITE=1)
include_directories(
${XCB_XCB_INCLUDE_DIR}
${XCB_DAMAGE_INCLUDE_DIR}
${XCB_COMPOSITE_INCLUDE_DIR}
)
endif()
set(corebindings_SRCS
corebindingsplugin.cpp
@ -26,6 +34,7 @@ set(corebindings_SRCS
serviceoperationstatus.cpp
dataenginebindings.cpp
iconitem.cpp
windowthumbnail.cpp
)
add_library(corebindingsplugin SHARED ${corebindings_SRCS})
@ -43,6 +52,17 @@ target_link_libraries(corebindingsplugin
if(X11_FOUND)
target_link_libraries(corebindingsplugin ${X11_LIBRARIES} ${XCB_XCB_LIBRARY} )
target_link_libraries(corebindingsplugin Qt5::X11Extras)
if(XCB_COMPOSITE_FOUND AND XCB_DAMAGE_FOUND)
target_link_libraries(corebindingsplugin
${XCB_DAMAGE_LIBRARY}
${XCB_COMPOSITE_LIBRARY}
)
endif()
if(OPENGL_FOUND)
target_link_libraries(corebindingsplugin ${OPENGL_gl_LIBRARY})
endif()
endif(X11_FOUND)
install(TARGETS corebindingsplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/plasma/core)

View File

@ -43,6 +43,7 @@
#include "serviceoperationstatus.h"
#include "tooltip.h"
#include "windowthumbnail.h"
// #include "dataenginebindings_p.h"
@ -103,6 +104,8 @@ void CoreBindingsPlugin::registerTypes(const char *uri)
qmlRegisterInterface<Plasma::DataSource>("DataSource");
qRegisterMetaType<Plasma::DataSource*>("DataSource");
qmlRegisterType<Plasma::WindowThumbnail>(uri, 2, 0, "WindowThumbnail");
}

View File

@ -0,0 +1,394 @@
/*
* Copyright 2013 by Martin Gräßlin <mgraesslin@kde.org>
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2, 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 Library General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "windowthumbnail.h"
// KF5
#include <KWindowSystem>
// Qt
#include <QGuiApplication>
#include <QIcon>
#include <QOpenGLContext>
#include <QQuickWindow>
// X11
#if HAVE_XCB_COMPOSITE
#include <QX11Info>
#include <xcb/composite.h>
#if HAVE_GLX
#include <GL/glx.h>
typedef void (*glXBindTexImageEXT_func)(Display* dpy, GLXDrawable drawable,
int buffer, const int* attrib_list);
typedef void (*glXReleaseTexImageEXT_func)(Display* dpy, GLXDrawable drawable, int buffer);
#endif
#endif
namespace Plasma {
WindowTextureNode::WindowTextureNode()
: QSGSimpleTextureNode()
{
}
WindowTextureNode::~WindowTextureNode()
{
}
void WindowTextureNode::reset(QSGTexture *texture)
{
setTexture(texture);
m_texture.reset(texture);
}
WindowThumbnail::WindowThumbnail(QQuickItem* parent)
: QQuickItem(parent)
, QAbstractNativeEventFilter()
, m_xcb(false)
, m_winId(0)
, m_damaged(false)
#if HAVE_XCB_COMPOSITE
, m_openGLFunctionsResolved(false)
, m_damageEventBase(0)
, m_damage(XCB_NONE)
, m_pixmap(XCB_PIXMAP_NONE)
, m_texture(0)
#if HAVE_GLX
, m_glxPixmap(XCB_PIXMAP_NONE)
#endif
#endif
{
setFlag(ItemHasContents);
if (QGuiApplication *gui = dynamic_cast<QGuiApplication*>(QCoreApplication::instance())) {
m_xcb = (gui->platformName() == QStringLiteral("xcb"));
if (m_xcb) {
gui->installNativeEventFilter(this);
#if HAVE_XCB_COMPOSITE
xcb_connection_t *c = QX11Info::connection();
xcb_prefetch_extension_data(c, &xcb_damage_id);
const auto *reply = xcb_get_extension_data(c, &xcb_damage_id);
m_damageEventBase = reply->first_event;
if (reply->present) {
xcb_damage_query_version_unchecked(c, XCB_DAMAGE_MAJOR_VERSION, XCB_DAMAGE_MINOR_VERSION);
}
#endif
}
}
}
WindowThumbnail::~WindowThumbnail()
{
if (m_xcb) {
QCoreApplication::instance()->removeNativeEventFilter(this);
stopRedirecting();
discardPixmap();
}
}
uint32_t WindowThumbnail::winId() const
{
return m_winId;
}
void WindowThumbnail::setWinId(uint32_t winId)
{
if (m_winId == winId) {
return;
}
if (!KWindowSystem::self()->hasWId(winId)) {
// invalid Id, don't updated
return;
}
stopRedirecting();
m_winId = winId;
startRedirecting();
emit winIdChanged();
}
QSGNode *WindowThumbnail::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData)
{
Q_UNUSED(updatePaintNodeData)
auto *node = static_cast<WindowTextureNode*>(oldNode);
if (!node) {
node = new WindowTextureNode();
node->setFiltering(QSGTexture::Linear);
}
if (!m_xcb || m_winId == 0) {
iconToTexture(node);
} else {
windowToTexture(node);
}
node->setRect(boundingRect());
const QSize size(node->texture()->textureSize().scaled(boundingRect().size().toSize(), Qt::KeepAspectRatio));
const qreal x = boundingRect().x() + (boundingRect().width() - size.width())/2;
const qreal y = boundingRect().y() + (boundingRect().height() - size.height())/2;
node->setRect(QRectF(QPointF(x, y), size));
return node;
}
bool WindowThumbnail::nativeEventFilter(const QByteArray &eventType, void *message, long int *result)
{
Q_UNUSED(result)
if (!m_xcb || eventType != QByteArrayLiteral("xcb_generic_event_t")) {
// currently we are only interested in XCB events
return false;
}
#if HAVE_XCB_COMPOSITE
xcb_generic_event_t *event = static_cast<xcb_generic_event_t*>(message);
const uint8_t responseType = event->response_type & ~0x80;
if (responseType == m_damageEventBase + XCB_DAMAGE_NOTIFY) {
if (reinterpret_cast<xcb_damage_notify_event_t*>(event)->drawable == m_winId) {
m_damaged = true;
update();
}
} else if (responseType == XCB_CONFIGURE_NOTIFY) {
if (reinterpret_cast<xcb_configure_notify_event_t*>(event)->window == m_winId) {
// TODO: only discard if size changes
discardPixmap();
}
}
#endif
// do not filter out any events, there might be further WindowThumbnails for the same window
return false;
}
void WindowThumbnail::iconToTexture(WindowTextureNode *textureNode)
{
QIcon icon;
if (KWindowSystem::self()->hasWId(m_winId)) {
icon = KWindowSystem::self()->icon(m_winId);
} else {
// fallback to plasma icon
icon = QIcon::fromTheme("plasma");
}
QImage image = icon.pixmap(boundingRect().size().toSize()).toImage();
textureNode->reset(window()->createTextureFromImage(image));
}
void WindowThumbnail::windowToTexture(WindowTextureNode *textureNode)
{
if (!m_damaged && textureNode->texture()) {
return;
}
#if HAVE_XCB_COMPOSITE
xcb_connection_t *c = QX11Info::connection();
if (m_pixmap == XCB_PIXMAP_NONE) {
m_pixmap = pixmapForWindow();
}
if (m_pixmap == XCB_PIXMAP_NONE) {
// create above failed
iconToTexture(textureNode);
return;
}
#if HAVE_GLX
if (glXGetCurrentContext()) {
if (!m_openGLFunctionsResolved) {
resolveGLXFunctions();
}
if (!m_bindTexImage || !m_releaseTexImage) {
iconToTexture(textureNode);
return;
}
if (m_glxPixmap == XCB_PIXMAP_NONE) {
auto geometryCookie = xcb_get_geometry_unchecked(c, m_pixmap);
if (!loadGLXTexture()) {
iconToTexture(textureNode);
return;
}
QScopedPointer<xcb_get_geometry_reply_t, QScopedPointerPodDeleter> geo(xcb_get_geometry_reply(c, geometryCookie, Q_NULLPTR));
QSize size;
if (!geo.isNull()) {
size.setWidth(geo->width);
size.setHeight(geo->height);
}
textureNode->reset(window()->createTextureFromId(m_texture, size));
}
textureNode->texture()->bind();
bindGLXTexture();
} else {
#else
{
#endif
// just for safety to not crash
iconToTexture(textureNode);
}
// TODO: add an egl variant
textureNode->markDirty(QSGNode::DirtyForceUpdate);
#endif
}
#if HAVE_XCB_COMPOSITE
xcb_pixmap_t WindowThumbnail::pixmapForWindow()
{
xcb_connection_t *c = QX11Info::connection();
xcb_pixmap_t pix = xcb_generate_id(c);
auto cookie = xcb_composite_name_window_pixmap_checked(c, m_winId, pix);
QScopedPointer<xcb_generic_error_t, QScopedPointerPodDeleter> error(xcb_request_check(c, cookie));
if (error) {
return XCB_PIXMAP_NONE;
}
return pix;
}
#if HAVE_GLX
void WindowThumbnail::resolveGLXFunctions()
{
auto *context = window()->openglContext();
QList<QByteArray> extensions = QByteArray(glXQueryExtensionsString(QX11Info::display(), QX11Info::appScreen())).split(' ');
if (extensions.contains(QByteArrayLiteral("GLX_EXT_texture_from_pixmap"))) {
qDebug() << "Have texture from pixmap";
m_bindTexImage = context->getProcAddress(QByteArrayLiteral("glXBindTexImageEXT"));
m_releaseTexImage = context->getProcAddress(QByteArrayLiteral("glXReleaseTexImageEXT"));
}
m_openGLFunctionsResolved = true;
}
void WindowThumbnail::bindGLXTexture()
{
Display *d = QX11Info::display();
((glXReleaseTexImageEXT_func)(m_releaseTexImage))(d, m_glxPixmap, GLX_FRONT_LEFT_EXT);
((glXBindTexImageEXT_func)(m_bindTexImage))(d, m_glxPixmap, GLX_FRONT_LEFT_EXT, NULL);
resetDamaged();
}
bool WindowThumbnail::loadGLXTexture()
{
GLXContext glxContext = glXGetCurrentContext();
if (!glxContext) {
return false;
}
Display *d = QX11Info::display();
glGenTextures(1, &m_texture);
int fbConfig = 0;
glXQueryContext(d, glxContext, GLX_FBCONFIG_ID, &fbConfig);
int fbAttribs[] = {
GLX_FBCONFIG_ID, fbConfig, XCB_NONE
};
int count = 0;
GLXFBConfig *fbConfigs = glXChooseFBConfig(d, QX11Info::appScreen(), fbAttribs, &count);
if (count == 0) {
qDebug() << "Didn't get our FBConfig";
return false;
}
int bindRgb, bindRgba;
glXGetFBConfigAttrib(d, fbConfigs[0], GLX_BIND_TO_TEXTURE_RGBA_EXT, &bindRgba);
glXGetFBConfigAttrib(d, fbConfigs[0], GLX_BIND_TO_TEXTURE_RGB_EXT, &bindRgb);
XVisualInfo *vi = glXGetVisualFromFBConfig(d, fbConfigs[0]);
int textureFormat;
if (window()->openglContext()->format().hasAlpha())
textureFormat = bindRgba ? GLX_TEXTURE_FORMAT_RGBA_EXT : GLX_TEXTURE_FORMAT_RGB_EXT;
else
textureFormat = bindRgb ? GLX_TEXTURE_FORMAT_RGB_EXT : GLX_TEXTURE_FORMAT_RGBA_EXT;
XFree(vi);
// we assume that Texture_2D is supported as we have a QtQuick OpenGL context
int attrs[] = {
GLX_TEXTURE_FORMAT_EXT, textureFormat,
GLX_MIPMAP_TEXTURE_EXT, false,
GLX_TEXTURE_TARGET_EXT, GLX_TEXTURE_2D_EXT, XCB_NONE
};
m_glxPixmap = glXCreatePixmap(d, fbConfigs[0], m_pixmap, attrs);
return true;
}
#endif
#endif
void WindowThumbnail::resetDamaged()
{
m_damaged = false;
#if HAVE_XCB_COMPOSITE
xcb_damage_subtract(QX11Info::connection(), m_damage, XCB_NONE, XCB_NONE);
#endif
}
void WindowThumbnail::stopRedirecting()
{
if (!m_xcb) {
return;
}
#if HAVE_XCB_COMPOSITE
xcb_connection_t *c = QX11Info::connection();
if (m_pixmap != XCB_PIXMAP_NONE) {
xcb_free_pixmap(c, m_pixmap);
m_pixmap = XCB_PIXMAP_NONE;
}
if (m_winId == XCB_WINDOW_NONE) {
return;
}
xcb_composite_unredirect_window(c, m_winId, XCB_COMPOSITE_REDIRECT_AUTOMATIC);
if (m_damage == XCB_NONE) {
return;
}
xcb_damage_destroy(c, m_damage);
m_damage = XCB_NONE;
const uint32_t values[] = {XCB_EVENT_MASK_NO_EVENT};
xcb_change_window_attributes(c, m_winId, XCB_CW_EVENT_MASK, values);
#endif
}
void WindowThumbnail::startRedirecting()
{
if (!m_xcb) {
return;
}
#if HAVE_XCB_COMPOSITE
xcb_connection_t *c = QX11Info::connection();
// redirect the window
xcb_composite_redirect_window(c, m_winId, XCB_COMPOSITE_REDIRECT_AUTOMATIC);
// generate the damage handle
m_damage = xcb_generate_id(c);
xcb_damage_create(c, m_damage, m_winId, XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY);
const uint32_t values[] = {XCB_EVENT_MASK_STRUCTURE_NOTIFY};
xcb_change_window_attributes(c, m_winId, XCB_CW_EVENT_MASK, values);
// force to update the texture
m_damaged = true;
#endif
}
void WindowThumbnail::discardPixmap()
{
if (!m_xcb) {
return;
}
#if HAVE_XCB_COMPOSITE
#if HAVE_GLX
if (m_glxPixmap != XCB_PIXMAP_NONE) {
Display *d = QX11Info::display();
((glXReleaseTexImageEXT_func)(m_releaseTexImage))(d, m_glxPixmap, GLX_FRONT_LEFT_EXT);
glXDestroyPixmap(d, m_glxPixmap);
m_glxPixmap = XCB_PIXMAP_NONE;
glDeleteTextures(1, &m_texture);
}
#endif
if (m_pixmap != XCB_WINDOW_NONE) {
xcb_free_pixmap(QX11Info::connection(), m_pixmap);
m_pixmap = XCB_PIXMAP_NONE;
}
m_damaged = true;
update();
#endif
}
} // namespace

View File

@ -0,0 +1,124 @@
/*
* Copyright 2013 by Martin Gräßlin <mgraesslin@kde.org>
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2, 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 Library General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef PLASMA_WINDOWTHUMBNAIL_H
#define PLASMA_WINDOWTHUMBNAIL_H
#include <config-plasma.h>
// Qt
#include <QAbstractNativeEventFilter>
#include <QSGSimpleTextureNode>
#include <QQuickItem>
// xcb
#if HAVE_XCB_COMPOSITE
#include <xcb/damage.h>
#endif
class KWindowInfo;
namespace Plasma {
class WindowTextureNode;
/**
* @brief Renders a thumbnail for the window specified by the @c winId property.
*
* This declarative item is able to render a live updating thumbnail for the
* window specified by the given @c winId property. If it is not possible to get
* the thumbnail, the window's icon is rendered instead or in case that the window
* Id is invalid a generic fallback icon is used.
*
* The thumbnail does not necessarily fill out the complete geometry as the
* thumbnail gets scaled keeping the aspect ratio. This means the thumbnail gets
* rendered into the center of the item's geometry.
*
* Note: live updating thumbnails are only implemented on the X11 platform. On X11
* a running compositor is not required as this item takes care of redirecting the
* window. For technical reasons the window's frame is not included on X11.
*
* If the window closes, the thumbnail does not get destroyed, which allows to have
* a window close animation.
*
* Example usage:
* @code
* WindowThumbnail {
* winId: 102760466
* }
* @endcode
*
*/
class WindowThumbnail : public QQuickItem, public QAbstractNativeEventFilter
{
Q_OBJECT
Q_PROPERTY(uint winId READ winId WRITE setWinId NOTIFY winIdChanged)
public:
WindowThumbnail(QQuickItem *parent = 0);
virtual ~WindowThumbnail();
virtual bool nativeEventFilter(const QByteArray &eventType, void *message, long int *result);
virtual QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData);
uint32_t winId() const;
void setWinId(uint32_t winId);
Q_SIGNALS:
void winIdChanged();
private:
void iconToTexture(WindowTextureNode *textureNode);
void windowToTexture(WindowTextureNode *textureNode);
void startRedirecting();
void stopRedirecting();
void resetDamaged();
void discardPixmap();
bool m_xcb;
uint32_t m_winId;
bool m_damaged;
#if HAVE_XCB_COMPOSITE
xcb_pixmap_t pixmapForWindow();
bool m_openGLFunctionsResolved;
uint8_t m_damageEventBase;
xcb_damage_damage_t m_damage;
xcb_pixmap_t m_pixmap;
uint m_texture;
#if HAVE_GLX
void resolveGLXFunctions();
bool loadGLXTexture();
void bindGLXTexture();
QFunctionPointer m_bindTexImage;
QFunctionPointer m_releaseTexImage;
xcb_pixmap_t m_glxPixmap;
#endif
#endif
};
/**
* @brief SimpleTextureNode which cleans up the texture
*
*/
class WindowTextureNode : public QSGSimpleTextureNode
{
public:
WindowTextureNode();
virtual ~WindowTextureNode();
void reset(QSGTexture *texture);
private:
QScopedPointer<QSGTexture> m_texture;
};
}
#endif // PLASMA_WINDOWTHUMBNAIL_H

View File

@ -4,6 +4,7 @@
#cmakedefine01 PLASMA_NO_KUTILS
#cmakedefine01 PLASMA_NO_GLOBAL_SHORTCUTS
#cmakedefine01 HAVE_X11
#cmakedefine01 HAVE_GLX
/*FIXME: Only used in CMakeLists.txt, to be removed ?*/
#cmakedefine01 PLASMA_NO_KNEWSTUFF