You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdedocker/src/traylabelmgr.cpp

528 lines
15 KiB

/*
* 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 <tqdir.h>
#include <tqapplication.h>
#include <tqmessagebox.h>
#include <tqtimer.h>
#include <tqfile.h>
#include <tqaction.h>
#include <tqpopupmenu.h>
#include <tqtextstream.h>
#include <tqfiledialog.h>
#include <tdelocale.h>
#include "trace.h"
#include "traylabelmgr.h"
#include "util.h"
#include <X11/Xmu/WinUtil.h>
#include <errno.h>
#include <stdlib.h>
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
TQTimer::singleShot(0, this, SLOT(startup()));
}
TrayLabelMgr::~TrayLabelMgr()
{
undockAll();
}
void TrayLabelMgr::about(void)
{
if (TQMessageBox::information(NULL, i18n("About TDEDocker"),
i18n("Bugs/wishes to Girish Ramakrishnan (gramakri@uiuc.edu)\n"
"English translation by Girish (gramakri@uiuc.edu)\n\n"
"http://tdedocker.sourceforge.net for updates"),
TQString::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)) || tqApp->isSessionRestored();
SysTrayState state = sysTrayStatus(TQPaintDevice::x11AppDisplay());
if (state != SysTrayPresent)
{
if (wait_time-- > 0 && do_wait)
{
TRACE("Will check sys tray status after 1 second");
TQTimer::singleShot(1000, this, SLOT(startup()));
return;
}
if (TQMessageBox::warning(NULL, i18n("TDEDocker"),
i18n(state == SysTrayAbsent ? "No system tray found"
: "System tray appears to be hidden"),
TQMessageBox::Abort, TQMessageBox::Ignore) == TQMessageBox::Abort)
{
tqApp->quit();
return;
}
}
// Things are fine or user with OK with the state of system tray
mReady = true;
bool ok = false;
if (tqApp->isSessionRestored()) ok = restoreSession(tqApp->sessionId());
else ok = processCommand(tqApp->argc(), tqApp->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) tqApp->quit();
}
// Initialize a TQTrayLabel after its creation
void TrayLabelMgr::manageTrayLabel(TQTrayLabel *t)
{
connect(t, SIGNAL(destroyed(TQObject *)),
this, SLOT(trayLabelDestroyed(TQObject *)));
connect(t, SIGNAL(undocked(TQTrayLabel *)), t, SLOT(deleteLater()));
// All TQTrayLabels 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 TQTrayLabel prepended. Count=%i", mTrayLabels.count());
}
void TrayLabelMgr::dockAnother()
{
TQTrayLabel *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());
TQPtrListIterator<TQTrayLabel> it(mTrayLabels);
TQTrayLabel *t;
while ((t = it.current()) != 0)
{
++it;
t->undock();
}
}
// Process the command line
bool TrayLabelMgr::processCommand(const TQStringList& 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<argc; i++)
argv[i] = args[i].latin1();
argv[argc] = NULL; // null terminate the array
return processCommand(argc, const_cast<char **>(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 TDEDocker::notifyPreviousInstance()
if (qstrcmp(argv[1], "-session") == 0)
{
TRACE("Restoring session %s (new instance request)", argv[2]);
return restoreSession(TQString(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':
tqDebug("%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(TQPaintDevice::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(TQPaintDevice::x11AppDisplay(), w))
{
tqDebug("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.
*/
TQTrayLabel *TrayLabelMgr::selectAndDock(Window w, bool checkNormality)
{
if (w == None)
{
tqDebug("%s", i18n("Select the application/window to dock with button1.").local8Bit().data());
tqDebug("%s", i18n("Click any other button to abort\n").local8Bit().data());
const char *err = NULL;
if ((w = selectWindow(TQPaintDevice::x11AppDisplay(), &err)) == None)
{
if (err) TQMessageBox::critical(NULL, i18n("TDEDocker"), i18n(err));
return NULL;
}
}
if (checkNormality && !isNormalWindow(TQPaintDevice::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 (TQMessageBox::warning(NULL, i18n("TDEDocker"),
i18n("The window you are attempting to dock does not seem to be a"
" normal window."), TQMessageBox::Abort,
TQMessageBox::Ignore) == TQMessageBox::Abort)
return NULL;
}
if (!isWindowDocked(w)) return new CustomTrayLabel(w);
TRACE("0x%x is not docked", (unsigned) w);
TQMessageBox::message(i18n("TDEDocker"),
i18n("This window is already docked.\n"
"Click on system tray icon to toggle docking."));
return NULL;
}
bool TrayLabelMgr::isWindowDocked(Window w)
{
TQPtrListIterator<TQTrayLabel> it(mTrayLabels);
for(TQTrayLabel *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)
*/
TQTrayLabel *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. TQTrayLabel ctor keeps looking out for newly created
* windows. Need to make sure that the application is actually exec'ed only
* after we TQTrayLabel 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)
{
tqDebug("%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)
{
TQMessageBox::critical(NULL, "TDEDocker",
i18n("Failed to fork: %1").arg(strerror(errno)));
return NULL;
}
TQStringList cmd_line;
for(int i=0;;i++)
if (argv[i]) cmd_line << argv[i]; else break;
TQTrayLabel *label = new CustomTrayLabel(cmd_line, pid);
tqApp->syncX();
write(filedes[1], buf, sizeof(buf));
close(filedes[0]);
close(filedes[1]);
return label;
}
/*
* Returns the number of TQTrayLabels actually created but not show in the
* System Tray
*/
int TrayLabelMgr::hiddenLabelsCount(void) const
{
TQPtrListIterator<TQTrayLabel> it(mTrayLabels);
int count = 0;
for(TQTrayLabel *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(TQObject *t)
{
bool reconnect = ((TQObject *)mTrayLabels.getLast() == t);
mTrayLabels.removeRef((TQTrayLabel*)t);
if (mTrayLabels.isEmpty()) tqApp->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.
*/
TQTimer::singleShot(3000, this, SLOT(notifySysTrayAbsence()));
}
void TrayLabelMgr::notifySysTrayAbsence()
{
SysTrayState state = sysTrayStatus(TQPaintDevice::x11AppDisplay());
if (state == SysTrayPresent)
return; // So sweet of the systray to come back so soon
if (TQMessageBox::warning(NULL, i18n("TDEDocker"),
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 TQString& sessionId)
{
TQString session_file = "tdedocker_" + sessionId;
TQSettings settings;
settings.beginGroup(TQString("/" + session_file));
for(int i = 1;; i++)
{
settings.beginGroup(TQString("/Instance") + TQString("").setNum(i));
TQString pname = settings.readEntry("/Application");
TRACE("Restoring Application[%s]", pname.latin1());
if (pname.isEmpty()) break;
if (settings.readBoolEntry("/LaunchOnStartup"))
{
TQStringList args("tdedocker");
args += TQStringList::split(" ", pname);
TRACE("Triggering AutoLaunch");
if (!processCommand(args)) continue;
}
else
manageTrayLabel(new CustomTrayLabel(TQStringList::split(" ", pname), 0));
TQTrayLabel *tl = mTrayLabels.getFirst(); // the one that was created above
tl->restoreState(settings);
settings.endGroup();
}
return true;
}
TQString TrayLabelMgr::saveSession(void)
{
TQString session_file = "tdedocker_" + tqApp->sessionId();
TQSettings settings;
settings.beginGroup(TQString("/" + session_file));
TRACE("Saving session");
TQPtrListIterator <TQTrayLabel> it(mTrayLabels);
TQTrayLabel *t;
int i = 1;
while ((t = it.current()) != 0)
{
++it;
TRACE("Saving instance %i", i);
settings.beginGroup(TQString("/Instance") + TQString("").setNum(i));
bool ok = t->saveState(settings);
settings.endGroup();
if (ok) ++i; else TRACE("Saving of instance %i was skipped", i);
}
// Aaaaaaaaaaaaaa.........
settings.removeEntry(TQString("/Instance") + TQString("").setNum(i));
return TQDir::homeDirPath() + "/.qt/" + session_file + "rc";
}
/*
* The X11 Event Filter. Pass on events to the TQTrayLabels 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 TQTrayLabel 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 TQTrayLabel objects.
* c) When a TQTrayLabel manages to find the window that is was looking for, we
* need not process the event further
*/
bool TrayLabelMgr::x11EventFilter(XEvent *ev)
{
TQPtrListIterator<TQTrayLabel> it(mTrayLabels);
bool ret = false;
// We pass on the event to all tray labels
for(TQTrayLabel *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"