/* * 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: traylabelmgr.cpp,v 1.10 2005/02/09 03:38:43 cs19713 Exp $ #include #include #include #include #include #include #include #include #include #include #include "trace.h" #include "traylabelmgr.h" #include "util.h" #include #include #include TrayLabelMgr* TrayLabelMgr::gTrayLabelMgr = NULL; const char *TrayLabelMgr::mOptionString = "+abdefi:lmop:qtw:"; TrayLabelMgr* TrayLabelMgr::instance(void) { if (gTrayLabelMgr) return gTrayLabelMgr; TRACE("Creating new instance"); return (gTrayLabelMgr = new TrayLabelMgr()); } TrayLabelMgr::TrayLabelMgr() : mReady(false), mHiddenLabelsCount(0) { // Set ourselves up to be called from the application loop QTimer::singleShot(0, this, SLOT(startup())); } TrayLabelMgr::~TrayLabelMgr() { undockAll(); } void TrayLabelMgr::about(void) { if (QMessageBox::information(NULL, i18n("About KDocker"), i18n("Bugs/wishes to Girish Ramakrishnan (gramakri@uiuc.edu)\n" "English translation by Girish (gramakri@uiuc.edu)\n\n" "http://kdocker.sourceforge.net for updates"), QString::null, SHOW_TRACE_TEXT) == 1) SHOW_TRACE(); } void TrayLabelMgr::startup(void) { const int WAIT_TIME = 10; static int wait_time = WAIT_TIME; /* * If it appears that we were launched from some startup script (check whether * stdout is a tty) OR if we are getting restored, wait for WAIT_TIME until * the system tray shows up (before informing the user) */ static bool do_wait = !isatty(fileno(stdout)) || qApp->isSessionRestored(); SysTrayState state = sysTrayStatus(QPaintDevice::x11AppDisplay()); if (state != SysTrayPresent) { if (wait_time-- > 0 && do_wait) { TRACE("Will check sys tray status after 1 second"); QTimer::singleShot(1000, this, SLOT(startup())); return; } if (QMessageBox::warning(NULL, i18n("KDocker"), i18n(state == SysTrayAbsent ? "No system tray found" : "System tray appears to be hidden"), QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort) { qApp->quit(); return; } } // Things are fine or user with OK with the state of system tray mReady = true; bool ok = false; if (qApp->isSessionRestored()) ok = restoreSession(qApp->sessionId()); else ok = processCommand(qApp->argc(), qApp->argv()); // Process the request Q from previous instances TRACE("Request queue has %i requests", mRequestQ.count()); for(unsigned i=0; i < mRequestQ.count(); i++) ok |= processCommand(mRequestQ[i]); if (!ok) qApp->quit(); } // Initialize a QTrayLabel after its creation void TrayLabelMgr::manageTrayLabel(QTrayLabel *t) { connect(t, SIGNAL(destroyed(QObject *)), this, SLOT(trayLabelDestroyed(QObject *))); connect(t, SIGNAL(undocked(QTrayLabel *)), t, SLOT(deleteLater())); // All QTrayLabels will emit this signal. We just need one of them if (mTrayLabels.count() == 0) connect(t, SIGNAL(sysTrayDestroyed()), this, SLOT(sysTrayDestroyed())); mTrayLabels.prepend(t); TRACE("New QTrayLabel prepended. Count=%i", mTrayLabels.count()); } void TrayLabelMgr::dockAnother() { QTrayLabel *t = selectAndDock(); if (t == NULL) return; manageTrayLabel(t); t->withdraw(); t->dock(); } // Undock all the windows void TrayLabelMgr::undockAll() { TRACE("Number of tray labels = %i", mTrayLabels.count()); QPtrListIterator it(mTrayLabels); QTrayLabel *t; while ((t = it.current()) != 0) { ++it; t->undock(); } } // Process the command line bool TrayLabelMgr::processCommand(const QStringList& args) { if (!mReady) { // If we are still looking for system tray, just add it to the Q mRequestQ.append(args); return true; } const int MAX_ARGS = 20; const char *argv[MAX_ARGS]; int argc = args.count(); if (argc >= MAX_ARGS) argc = MAX_ARGS - 1; for(int i =0 ; i(argv)); } // Process the command line bool TrayLabelMgr::processCommand(int argc, char** argv) { TRACE("CommandLine arguments"); for(int i = 0; i < argc; i++) TRACE("\t%s", argv[i]); if (argc < 1) return false; // Restore session (See the comments in KDocker::notifyPreviousInstance() if (qstrcmp(argv[1], "-session") == 0) { TRACE("Restoring session %s (new instance request)", argv[2]); return restoreSession(QString(argv[2])); } int option; Window w = None; const char *icon = NULL; int balloon_timeout = 4000; bool withdraw = true, skip_taskbar = false, auto_launch = false, dock_obscure = false, check_normality = true, enable_sm = true; optind = 0; // initialise the getopt static while ((option = getopt(argc, argv, mOptionString)) != EOF) { switch (option) { case '?': return false; case 'a': qDebug("%s", i18n("Girish Ramakrishnan (gramakri@uiuc.edu)").local8Bit().data()); return false; case 'b': check_normality = false; break; case 'd': enable_sm = false; break; case 'e': enable_sm = true; break; case 'f': w = activeWindow(QPaintDevice::x11AppDisplay()); TRACE("Active window is %i", (unsigned) w); break; case 'i': icon = optarg; break; case 'l': auto_launch = true; break; case 'm': withdraw = false; break; case 'o': dock_obscure = true; break; case 'p': balloon_timeout = atoi(optarg) * 1000; // convert to ms break; case 'q': balloon_timeout = 0; // same as '-p 0' break; case 't': skip_taskbar = true; break; case 'w': if ((optarg[1] == 'x') || (optarg[1] == 'X')) sscanf(optarg, "%x", (unsigned *) &w); else w = (Window) atoi(optarg); if (!isValidWindowId(QPaintDevice::x11AppDisplay(), w)) { qDebug("Window 0x%x invalid", (unsigned) w); return false; } break; } // switch (option) } // while (getopt) // Launch an application if present in command line. else request from user CustomTrayLabel *t = (CustomTrayLabel *) // this should be dynamic_cast ((optind < argc) ? dockApplication(&argv[optind]) : selectAndDock(w, check_normality)); if (t == NULL) return false; // apply settings and add to tray manageTrayLabel(t); if (icon) t->setTrayIcon(icon); t->setSkipTaskbar(skip_taskbar); t->setBalloonTimeout(balloon_timeout); t->setDockWhenObscured(dock_obscure); if (withdraw) t->withdraw(); else t->map(); t->enableSessionManagement(enable_sm); t->dock(); t->setLaunchOnStartup(auto_launch); return true; } /* * Request user to make a window selection if necessary. Dock the window. */ QTrayLabel *TrayLabelMgr::selectAndDock(Window w, bool checkNormality) { if (w == None) { qDebug("%s", i18n("Select the application/window to dock with button1.").local8Bit().data()); qDebug("%s", i18n("Click any other button to abort\n").local8Bit().data()); const char *err = NULL; if ((w = selectWindow(QPaintDevice::x11AppDisplay(), &err)) == None) { if (err) QMessageBox::critical(NULL, i18n("KDocker"), i18n(err)); return NULL; } } if (checkNormality && !isNormalWindow(QPaintDevice::x11AppDisplay(), w)) { /* * Abort should be the only option here really. "Ignore" is provided here * for the curious user who wants to screw himself very badly */ if (QMessageBox::warning(NULL, i18n("KDocker"), i18n("The window you are attempting to dock does not seem to be a" " normal window."), QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort) return NULL; } if (!isWindowDocked(w)) return new CustomTrayLabel(w); TRACE("0x%x is not docked", (unsigned) w); QMessageBox::message(i18n("KDocker"), i18n("This window is already docked.\n" "Click on system tray icon to toggle docking.")); return NULL; } bool TrayLabelMgr::isWindowDocked(Window w) { QPtrListIterator it(mTrayLabels); for(QTrayLabel *t; (t = it.current()); ++it) if (t->dockedWindow() == w) return true; return false; } /* * Forks application specified by argv. Requests root window SubstructreNotify * notifications (for MapEvent on children). We will monitor these new windows * to make a pid to wid mapping (see HACKING for more details) */ QTrayLabel *TrayLabelMgr::dockApplication(char *argv[]) { pid_t pid = -1; int filedes[2]; char buf[4] = { 't', 'e', 'e', 'e' }; // teeeeeee :x :-* /* * The pipe created here serves as a synchronization mechanism between the * parent and the child. QTrayLabel ctor keeps looking out for newly created * windows. Need to make sure that the application is actually exec'ed only * after we QTrayLabel is created (it requires pid of child) */ pipe(filedes); if ((pid = fork()) == 0) { close(filedes[1]); read(filedes[0], buf, sizeof(buf)); close(filedes[0]); if (execvp(argv[0], argv) == -1) { qDebug("%s", i18n("Failed to exec [%1]: %2").arg(argv[0]).arg(strerror(errno)).local8Bit().data()); ::exit(0); // will become a zombie in some systems :( return NULL; } } if (pid == -1) { QMessageBox::critical(NULL, "KDocker", i18n("Failed to fork: %1").arg(strerror(errno))); return NULL; } QStringList cmd_line; for(int i=0;;i++) if (argv[i]) cmd_line << argv[i]; else break; QTrayLabel *label = new CustomTrayLabel(cmd_line, pid); qApp->syncX(); write(filedes[1], buf, sizeof(buf)); close(filedes[0]); close(filedes[1]); return label; } /* * Returns the number of QTrayLabels actually created but not show in the * System Tray */ int TrayLabelMgr::hiddenLabelsCount(void) const { QPtrListIterator it(mTrayLabels); int count = 0; for(QTrayLabel *t; (t=it.current()); ++it) if (t->dockedWindow() == None) ++count; return count; } // The number of labes that are docked in the system tray int TrayLabelMgr::dockedLabelsCount(void) const { return mTrayLabels.count() - hiddenLabelsCount(); } void TrayLabelMgr::trayLabelDestroyed(QObject *t) { bool reconnect = ((QObject *)mTrayLabels.getLast() == t); mTrayLabels.removeRef((QTrayLabel*)t); if (mTrayLabels.isEmpty()) qApp->quit(); else if (reconnect) { TRACE("Reconnecting"); connect(mTrayLabels.getFirst(), SIGNAL(sysTrayDestroyed()), this, SLOT(sysTrayDestroyed())); } } void TrayLabelMgr::sysTrayDestroyed(void) { /* * The system tray got destroyed. This could happen when it was * hidden/removed or killed/crashed/exited. Now we must be genteel enough * to not pop up a box when the user is logging out. So, we set ourselves * up to notify user after 3 seconds. */ QTimer::singleShot(3000, this, SLOT(notifySysTrayAbsence())); } void TrayLabelMgr::notifySysTrayAbsence() { SysTrayState state = sysTrayStatus(QPaintDevice::x11AppDisplay()); if (state == SysTrayPresent) return; // So sweet of the systray to come back so soon if (QMessageBox::warning(NULL, i18n("KDocker"), i18n("The System tray was hidden or removed"), i18n("Undock All"), i18n("Ignore")) == 0) undockAll(); } /* * Session Management. Always return "true". Atleast, for now */ bool TrayLabelMgr::restoreSession(const QString& sessionId) { QString session_file = "kdocker_" + sessionId; QSettings settings; settings.beginGroup(QString("/" + session_file)); for(int i = 1;; i++) { settings.beginGroup(QString("/Instance") + QString("").setNum(i)); QString pname = settings.readEntry("/Application"); TRACE("Restoring Application[%s]", pname.latin1()); if (pname.isEmpty()) break; if (settings.readBoolEntry("/LaunchOnStartup")) { QStringList args("kdocker"); args += QStringList::split(" ", pname); TRACE("Triggering AutoLaunch"); if (!processCommand(args)) continue; } else manageTrayLabel(new CustomTrayLabel(QStringList::split(" ", pname), 0)); QTrayLabel *tl = mTrayLabels.getFirst(); // the one that was created above tl->restoreState(settings); settings.endGroup(); } return true; } QString TrayLabelMgr::saveSession(void) { QString session_file = "kdocker_" + qApp->sessionId(); QSettings settings; settings.beginGroup(QString("/" + session_file)); TRACE("Saving session"); QPtrListIterator it(mTrayLabels); QTrayLabel *t; int i = 1; while ((t = it.current()) != 0) { ++it; TRACE("Saving instance %i", i); settings.beginGroup(QString("/Instance") + QString("").setNum(i)); bool ok = t->saveState(settings); settings.endGroup(); if (ok) ++i; else TRACE("Saving of instance %i was skipped", i); } // Aaaaaaaaaaaaaa......... settings.removeEntry(QString("/Instance") + QString("").setNum(i)); return QDir::homeDirPath() + "/.qt/" + session_file + "rc"; } /* * The X11 Event Filter. Pass on events to the QTrayLabels that we created. * The logic and the code below is a bit fuzzy. * a) Events about windows that are being docked need to be processed only by * the QTrayLabel object that is docking that window. * b) Events about windows that are not docked but of interest (like * SystemTray) need to be passed on to all QTrayLabel objects. * c) When a QTrayLabel manages to find the window that is was looking for, we * need not process the event further */ bool TrayLabelMgr::x11EventFilter(XEvent *ev) { QPtrListIterator it(mTrayLabels); bool ret = false; // We pass on the event to all tray labels for(QTrayLabel *t; (t = it.current()); ++it) { Window w = t->dockedWindow(); bool res = t->x11EventFilter(ev); if (w == (((XAnyEvent *)ev)->window)) return res; if (w != None) ret |= res; else if (res) return TRUE; } return ret; } #include "traylabelmgr.moc"