/* * Copyright (C) 2004 Girish Ramakrishnan All Rights Reserved. * * This 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 software 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 software; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ // $Id: qtraylabel.cpp,v 1.31 2005/06/21 10:04:36 cs19713 Exp $ // Include all Qt includes before X #include #include #include #include #include #include #include #include #include #include "trace.h" #include "qtraylabel.h" #include #include #include #include #include #include #include #include "util.h" void QTrayLabel::initialize(void) { mDocked = false; mWithdrawn = true; mBalloonTimeout = 4000; mSkippingTaskbar = false; mDockWhenMinimized = true; mDesktop = 666; // setDockedWindow would set it a saner value // Balloon's properties are set to match a Qt tool tip (see Qt source) mBalloon = new QLabel(0, "balloon", WType_TopLevel | WStyle_StaysOnTop | WStyle_Customize | WStyle_NoBorder | WStyle_Tool | WX11BypassWM); mBalloon->setFont(QToolTip::font()); mBalloon->setPalette(QToolTip::palette()); mBalloon->setAlignment(Qt::AlignLeft | Qt::AlignTop); mBalloon->setAutoMask(FALSE); mBalloon->setAutoResize(true); setAlignment(Qt::AlignCenter); setBackgroundMode(X11ParentRelative); connect(&mRealityMonitor, SIGNAL(timeout()), this, SLOT(realityCheck())); setDockedWindow(mDockedWindow); sysTrayStatus(QPaintDevice::x11AppDisplay(), &mSysTray); // Subscribe to system tray window notifications if (mSysTray != None) subscribe(QPaintDevice::x11AppDisplay(), mSysTray, StructureNotifyMask, true); } // Describe ourselves in a few words const char *QTrayLabel::me(void) const { static char temp[100]; snprintf(temp, sizeof(temp), "(%s,PID=%i,WID=0x%x)", mProgName[0].latin1(), mPid, (unsigned) mDockedWindow); return temp; } QTrayLabel::QTrayLabel(Window w, QWidget* parent, const QString& text) :QLabel(parent, text, WStyle_Customize | WStyle_NoBorder | WStyle_Tool), mDockedWindow(w), mPid(0) { initialize(); } QTrayLabel::QTrayLabel(const QStringList& pname, pid_t pid, QWidget* parent) :QLabel(parent, "TrayLabel", WStyle_Customize | WStyle_NoBorder | WStyle_Tool), mDockedWindow(None), mProgName(pname), mPid(pid) { if (pname[0].at(0) != '/' && pname[0].find('/', 1) > 0) mProgName[0] = QFileInfo(pname[0]).absFilePath(); // convert to absolute initialize(); } QTrayLabel::~QTrayLabel() { TRACE("%s Goodbye", me()); if (mDockedWindow == None) return; // Leave the docked window is some sane state mSkippingTaskbar = false; skipTaskbar(); map(); } /* * Scans the windows in the desktop and checks if a window exists that we might * be interested in */ void QTrayLabel::scanClients() { Window r, parent, *children; unsigned nchildren = 0; Display *display = QPaintDevice::x11AppDisplay(); QString ename = QFileInfo(mProgName[0]).fileName(); // strip out the path XQueryTree(display, qt_xrootwin(), &r, &parent, &children, &nchildren); TRACE("%s nchildren=%i", me(), nchildren); for(unsigned i=0; iNormal state change code does not map them. So we make the * window go through Withdrawn->Iconify->Normal state. */ XWMHints *wm_hint = XGetWMHints(display, mDockedWindow); if (wm_hint) { wm_hint->initial_state = IconicState; XSetWMHints(display, mDockedWindow, wm_hint); XFree(wm_hint); } XMapWindow(display, mDockedWindow); mSizeHint.flags = USPosition; // Obsolete ? XSetWMNormalHints(display, mDockedWindow, &mSizeHint); // make it the active window long l[5] = { None, CurrentTime, None, 0, 0 }; sendMessage(display, qt_xrootwin(), mDockedWindow, "_NET_ACTIVE_WINDOW", 32, SubstructureNotifyMask | SubstructureRedirectMask, l, sizeof(l)); // skipTaskbar modifies _NET_WM_STATE. Make sure we dont override WMs value QTimer::singleShot(230, this, SLOT(skipTaskbar())); // disable docking when minized for some time (since we went to Iconic state) mDockWhenMinimized = !mDockWhenMinimized; QTimer::singleShot(230, this, SLOT(toggleDockWhenMinimized())); } void QTrayLabel::withdraw(void) { TRACE("%s", me()); mWithdrawn = true; if (mDockedWindow == None) return; Display *display = QPaintDevice::x11AppDisplay(); int screen = DefaultScreen(display); long dummy; XGetWMNormalHints(display, mDockedWindow, &mSizeHint, &dummy); /* * A simple call to XWithdrawWindow wont do. Here is what we do: * 1. Iconify. This will make the application hide all its other windows. For * example, xmms would take off the playlist and equalizer window. * 2. Next tell the WM, that we would like to go to withdrawn state. Withdrawn * state will remove us from the taskbar. * Reference: ICCCM 4.1.4 Changing Window State */ XIconifyWindow(display, mDockedWindow, screen); // good for effects too XUnmapWindow(display, mDockedWindow); XUnmapEvent ev; memset(&ev, 0, sizeof(ev)); ev.type = UnmapNotify; ev.display = display; ev.event = qt_xrootwin(); ev.window = mDockedWindow; ev.from_configure = false; XSendEvent(display, qt_xrootwin(), False, SubstructureRedirectMask|SubstructureNotifyMask, (XEvent *)&ev); XSync(display, False); } /* * Skipping the taskbar is a bit painful. Basically, NET_WM_STATE needs to * have _NET_WM_STATE_SKIP_TASKBAR. NET_WM_STATE needs to be updated * carefully since it is a set of states. */ void QTrayLabel::skipTaskbar(void) { Atom __attribute__ ((unused)) type; int __attribute__ ((unused)) format; unsigned long __attribute__ ((unused)) left; Atom *data = NULL; unsigned long nitems = 0, num_states = 0; Display *display = QPaintDevice::x11AppDisplay(); TRACE("%s", me()); Atom _NET_WM_STATE = XInternAtom(display, "_NET_WM_STATE", True); Atom skip_atom = XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", False); int ret = XGetWindowProperty(display, mDockedWindow, _NET_WM_STATE, 0, 20, False, AnyPropertyType, &type, &format, &nitems, &left, (unsigned char **) &data); Atom *old_states = (Atom *) data; bool append = true, replace = false; if ((ret == Success) && data) { // Search for the skip_atom. Stop when found for (num_states = 0; num_states < nitems; num_states++) if (old_states[num_states] == skip_atom) break; if (mSkippingTaskbar) append = (num_states >= nitems); else { if (num_states < nitems) { replace = true; // need to remove skip_atom for (; num_states < nitems - 1; num_states++) old_states[num_states] = old_states[num_states + 1]; } } XFree(data); } TRACE("%s SkippingTaskar=%i append=%i replace=%i", me(), mSkippingTaskbar, append, replace); if (mSkippingTaskbar) { if (append) XChangeProperty(display, mDockedWindow, _NET_WM_STATE, XA_ATOM, 32, PropModeAppend, (unsigned char *) &skip_atom, 1); } else if (replace) XChangeProperty(display, mDockedWindow, _NET_WM_STATE, XA_ATOM, 32, PropModeReplace, (unsigned char *) &old_states, nitems - 1); } void QTrayLabel::setSkipTaskbar(bool skip) { TRACE("%s Skip=%i", me(), skip); mSkippingTaskbar = skip; if (mDockedWindow != None && !mWithdrawn) skipTaskbar(); } /* * Closes a window by sending _NET_CLOSE_WINDOW. For reasons best unknown we * need to first map and then send the request. */ void QTrayLabel::close(void) { TRACE("%s", me()); Display *display = QPaintDevice::x11AppDisplay(); long l[5] = { 0, 0, 0, 0, 0 }; map(); sendMessage(display, qt_xrootwin(), mDockedWindow, "_NET_CLOSE_WINDOW", 32, SubstructureNotifyMask | SubstructureRedirectMask, l, sizeof(l)); } /* * Sets the tray icon. If the icon failed to load, we revert to application icon */ void QTrayLabel::setTrayIcon(const QString& icon) { mCustomIcon = icon; if (QPixmap(mCustomIcon).isNull()) mCustomIcon = QString::null; TRACE("%s mCustomIcon=%s", me(), mCustomIcon.latin1()); updateIcon(); } /* * Sets the docked window to w. * A) Start/stop reality timer. * B) Subscribe/Unsubscribe for root/w notifications as appropriate * C) And of course, dock the window and apply some settings */ void QTrayLabel::setDockedWindow(Window w) { TRACE("%s %s reality monitor", me(), mDockedWindow==None ? "Starting" : "Stopping"); // Check if we are allowed to dock this window (allows custom rules) if (w != None) mDockedWindow = canDockWindow(w) ? w : None; else mDockedWindow = None; if (mDockedWindow == None) mRealityMonitor.start(500); else mRealityMonitor.stop(); Display *d = QPaintDevice::x11AppDisplay(); // Subscribe for window or root window events if (w == None) subscribe(d, None, SubstructureNotifyMask, true); else { if (canUnsubscribeFromRoot()) subscribe(d, None, ~SubstructureNotifyMask, false); else subscribe(d, None, SubstructureNotifyMask, true); subscribe(d, w, StructureNotifyMask | PropertyChangeMask | VisibilityChangeMask | FocusChangeMask, true); } if (mDocked && w!=None) { // store the desktop on which the window is being shown getCardinalProperty(d, mDockedWindow, XInternAtom(d, "_NET_WM_DESKTOP", True), &mDesktop); if (mWithdrawn) // show the window for sometime before docking QTimer::singleShot(1000, this, SLOT(withdraw())); else map(); dock(); } } /* * Balloon text. Overload this if you dont like the way things are ballooned */ void QTrayLabel::balloonText() { TRACE("%s BalloonText=%s ToolTipText=%s", me(), mBalloon->text().latin1(), QToolTip::textFor(this).latin1()); if (mBalloon->text() == QToolTip::textFor(this)) return; #if 0 // I_GOT_NETWM_BALLOONING_TO_WORK // if you can get NET WM ballooning to work let me know static int id = 1; long l[5] = { CurrentTime, SYSTEM_TRAY_BEGIN_MESSAGE, 2000, mTitle.length(), id++ }; sendMessage(display, mSystemTray, winId(), "_NET_SYSTEM_TRAY_OPCODE", 32, SubstructureNotifyMask | SubstructureRedirectMask, l, sizeof(l)); int length = mTitle.length(); const char *data = mTitle.latin1(); while (length > 0) { sendMessage(display, mSystemTray, winId(), "_NET_SYSTEM_TRAY_MESSAGE_DATA", 8, SubstructureNotifyMask | SubstructureRedirectMask, (void *) data, length > 20 ? 20 : length); length -= 20; data += 20; } #else // Manually do ballooning. See the Qt ToolTip code QString oldText = mBalloon->text(); mBalloon->setText(QToolTip::textFor(this)); if (oldText.isEmpty()) return; // dont tool tip the first time QPoint p = mapToGlobal(QPoint(0, -1 - mBalloon->height())); if (p.x() + mBalloon->width() > QApplication::desktop()->width()) p.setX(p.x() + width() - mBalloon->width()); if (p.y() < 0) p.setY(height() + 1); mBalloon->move(p); mBalloon->show(); QTimer::singleShot(mBalloonTimeout, mBalloon, SLOT(hide())); #endif } /* * Update the title in the menu. Balloon the title change if necessary */ void QTrayLabel::handleTitleChange(void) { Display *display = QPaintDevice::x11AppDisplay(); char *window_name = NULL; XFetchName(display, mDockedWindow, &window_name); mTitle = window_name; TRACE("%s has title [%s]", me(), mTitle.latin1()); if (window_name) XFree(window_name); XClassHint ch; if (XGetClassHint(display, mDockedWindow, &ch)) { if (ch.res_class) mClass = QString(ch.res_class); else if (ch.res_name) mClass = QString(ch.res_name); if (ch.res_class) XFree(ch.res_class); if (ch.res_name) XFree(ch.res_name); } updateTitle(); if (mBalloonTimeout) balloonText(); } /* * Overload this if you want a tool tip format that is different from the one * below i.e "Title [Class]". */ void QTrayLabel::updateTitle() { TRACE("%s", me()); QString text = mTitle + " [" + mClass + "]"; QToolTip::remove(this); QToolTip::add(this, text); if (mBalloonTimeout) balloonText(); } void QTrayLabel::handleIconChange(void) { char **window_icon = NULL; TRACE("%s", me()); if (mDockedWindow == None) return; Display *display = QPaintDevice::x11AppDisplay(); XWMHints *wm_hints = XGetWMHints(display, mDockedWindow); if (wm_hints != NULL) { if (!(wm_hints->flags & IconMaskHint)) wm_hints->icon_mask = None; /* * We act paranoid here. Progams like KSnake has a bug where * IconPixmapHint is set but no pixmap (Actually this happens with * quite a few KDE programs) X-( */ if ((wm_hints->flags & IconPixmapHint) && (wm_hints->icon_pixmap)) XpmCreateDataFromPixmap(display, &window_icon, wm_hints->icon_pixmap, wm_hints->icon_mask, NULL); XFree(wm_hints); } QImage image; if (!window_icon) { if (!image.load(QString(ICONS_PATH) + "/question.png")) image.load(qApp->applicationDirPath() + "/icons/question.png"); } else image = QPixmap((const char **) window_icon).convertToImage(); if (window_icon) XpmFree(window_icon); mAppIcon = image.smoothScale(24, 24); // why? setMinimumSize(mAppIcon.size()); setMaximumSize(mAppIcon.size()); updateIcon(); } /* * Overload this to possibly do operations on the pixmap before it is set */ void QTrayLabel::updateIcon() { TRACE("%s", me()); setPixmap(mCustomIcon.isEmpty() ? mAppIcon : mCustomIcon); erase(); QPaintEvent pe(rect()); paintEvent(&pe); } /* * Mouse activity on our label. RightClick = Menu. LeftClick = Toggle Map */ void QTrayLabel::mouseReleaseEvent(QMouseEvent * ev) { emit clicked(ev->button(), ev->globalPos()); } /* * Track drag event */ void QTrayLabel::dragEnterEvent(QDragEnterEvent *ev) { ev->accept(); map(); } /* * Event dispatcher */ bool QTrayLabel::x11EventFilter(XEvent *ev) { XAnyEvent *event = (XAnyEvent *)ev; if (event->window == mSysTray) { if (event->type != DestroyNotify) return FALSE; // not interested in others emit sysTrayDestroyed(); mSysTray = None; TRACE("%s SystemTray disappeared. Starting timer", me()); mRealityMonitor.start(500); return true; } else if (event->window == mDockedWindow) { if (event->type == DestroyNotify) destroyEvent(); else if (event->type == PropertyNotify) propertyChangeEvent(((XPropertyEvent *)event)->atom); else if (event->type == VisibilityNotify) { if (((XVisibilityEvent *) event)->state == VisibilityFullyObscured) obscureEvent(); } else if (event->type == MapNotify) { mWithdrawn = false; mapEvent(); } else if (event->type == UnmapNotify) { mWithdrawn = true; unmapEvent(); } else if (event->type == FocusOut) { focusLostEvent(); } return true; // Dont process this again } if (mDockedWindow != None || event->type != MapNotify) return FALSE; TRACE("%s Will analyze window 0x%x", me(), (int)((XMapEvent *)event)->window); // Check if this window is the soulmate we are looking for Display *display = QPaintDevice::x11AppDisplay(); Window w = XmuClientWindow(display, ((XMapEvent *) event)->window); if (!isNormalWindow(display, w)) return FALSE; if (!analyzeWindow(display, w, mPid, QFileInfo(mProgName[0]).fileName().latin1())) return FALSE; // All right. Lets dock this baby setDockedWindow(w); return true; } void QTrayLabel::minimizeEvent(void) { TRACE("minimizeEvent"); if (mDockWhenMinimized) withdraw(); } void QTrayLabel::destroyEvent(void) { TRACE("%s destroyEvent", me()); setDockedWindow(None); if (!mPid) undock(); } void QTrayLabel::propertyChangeEvent(Atom property) { Display *display = QPaintDevice::x11AppDisplay(); static Atom WM_NAME = XInternAtom(display, "WM_NAME", True); static Atom WM_ICON = XInternAtom(display, "WM_ICON", True); static Atom WM_STATE = XInternAtom(display, "WM_STATE", True); static Atom _NET_WM_STATE = XInternAtom(display, "_NET_WM_STATE", True); static Atom _NET_WM_DESKTOP = XInternAtom(display, "_NET_WM_DESKTOP", True); if (property == WM_NAME) handleTitleChange(); else if (property == WM_ICON) handleIconChange(); else if (property == _NET_WM_STATE) ; // skipTaskbar(); else if (property == _NET_WM_DESKTOP) { TRACE("_NET_WM_DESKTOP changed"); getCardinalProperty(display, mDockedWindow, _NET_WM_DESKTOP, &mDesktop); } else if (property == WM_STATE) { Atom type = None; int format; unsigned long nitems, after; unsigned char *data = NULL; int r = XGetWindowProperty(display, mDockedWindow, WM_STATE, 0, 1, False, AnyPropertyType, &type, &format, &nitems, &after, &data); if ((r == Success) && data && (*(long *) data == IconicState)) { minimizeEvent(); XFree(data); } } } // Session Management bool QTrayLabel::saveState(QSettings &settings) { TRACE("%s saving state", me()); settings.writeEntry("/Application", mProgName.join(" ")); settings.writeEntry("/CustomIcon", mCustomIcon); settings.writeEntry("/BalloonTimeout", mBalloonTimeout); settings.writeEntry("/DockWhenMinimized", mDockWhenMinimized); settings.writeEntry("/SkipTaskbar", mSkippingTaskbar); settings.writeEntry("/Withdraw", mWithdrawn); return true; } bool QTrayLabel::restoreState(QSettings &settings) { TRACE("%s restoring state", me()); mCustomIcon = settings.readEntry("/CustomIcon"); setBalloonTimeout(settings.readNumEntry("/BalloonTimeout")); setDockWhenMinimized(settings.readBoolEntry("/DockWhenMinimized")); setSkipTaskbar(settings.readBoolEntry("/SkipTaskbar")); mWithdrawn = settings.readBoolEntry("/Withdraw"); dock(); /* * Since we are getting restored, it is likely that the application that we * are interested in has already been started (if we didnt launch it). * So we scan the list of windows and grab the first one that satisfies us * This implicitly assumes that if mPid!=0 then we launched it. Wait till * the application really shows itself up before we do a scan (the reason * why we have 2s */ if (!mPid) QTimer::singleShot(2000, this, SLOT(scanClients())); return true; } // End kicking butt