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

619 lines
16 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 <tqfile.h>
#include <tqtextstream.h>
#include <tdeapplication.h>
#include <tdecmdlineargs.h>
#include <tdeconfig.h>
#include <tdelocale.h>
#include <tdemessagebox.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;
TrayLabelMgr* TrayLabelMgr::instance()
{
if (gTrayLabelMgr) return gTrayLabelMgr;
TRACE("Creating new instance");
return (gTrayLabelMgr = new TrayLabelMgr());
}
TrayLabelMgr::TrayLabelMgr() : mReady(false), mHiddenLabelsCount(0)
{
connect(&restoreSessionTimer, TQ_SIGNAL(timeout()), this, TQ_SLOT(doRestoreSession()));
// Set ourselves up to be called from the application loop
TQTimer::singleShot(0, this, TQ_SLOT(startup()));
}
TrayLabelMgr::~TrayLabelMgr()
{
undockAll();
}
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)) || TDEApplication::kApplication()->isRestored();
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, TQ_SLOT(startup()));
return;
}
if (KMessageBox::warningContinueCancel(NULL,
state == SysTrayAbsent ? i18n("No system tray found") : i18n("System tray appears to be hidden"),
i18n("TDEDocker")) == KMessageBox::Cancel)
{
TDEApplication::kApplication()->quit();
return;
}
}
// Things are fine or user with OK with the state of system tray
mReady = true;
bool ok = false;
if (TDEApplication::kApplication()->isRestored())
{
restoreSession();
ok = true;
}
else
{
ok = processCommand(TDECmdLineArgs::parsedArgs());
}
// 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)
{
TDEApplication::kApplication()->quit();
}
}
// Initialize a TQTrayLabel after its creation
void TrayLabelMgr::manageTrayLabel(TQTrayLabel *t)
{
connect(t, TQ_SIGNAL(destroyed(TQObject *)), this, TQ_SLOT(trayLabelDestroyed(TQObject *)));
connect(t, TQ_SIGNAL(undocked(TQTrayLabel *)), t, TQ_SLOT(deleteLater()));
// All TQTrayLabels will emit this signal. We just need one of them
if (mTrayLabels.count() == 0)
connect(t, TQ_SIGNAL(sysTrayDestroyed()), this, TQ_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();
}
// Close all the windows and quit
void TrayLabelMgr::quitAll()
{
TRACE("quitAll: number of tray labels = %i", mTrayLabels.count());
TQPtrListIterator<TQTrayLabel> it(mTrayLabels);
TQTrayLabel *t;
while ((t = it.current()) != 0)
{
++it;
t->close();
}
}
// Undock all the windows
void TrayLabelMgr::undockAll()
{
TRACE("undockAll: 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 = 50;
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].local8Bit();
}
argv[argc] = NULL; // null terminate the array
return processCommand(argc, const_cast<char **>(argv));
}
// Process the command line
bool TrayLabelMgr::processCommand(TDECmdLineArgs *args)
{
TQStringList argl;
argl.append(TDECmdLineArgs::appName());
if (args->isSet("b"))
{
argl.append("-b");
}
if (args->isSet("d"))
{
argl.append("-d");
}
if (args->isSet("e"))
{
argl.append("-e");
}
if (args->isSet("f"))
{
argl.append("-f");
}
if (args->isSet("i"))
{
argl.append("-i");
argl.append(args->getOption("i"));
}
if (args->isSet("m"))
{
argl.append("-m");
}
if (args->isSet("o"))
{
argl.append("-o");
}
if (args->isSet("p"))
{
argl.append("-p");
argl.append(args->getOption("p"));
}
if (args->isSet("q"))
{
argl.append("-q");
}
if (args->isSet("t"))
{
argl.append("-t");
}
if (args->isSet("w"))
{
argl.append("-w");
argl.append(args->getOption("w"));
}
for (int i = 0; i < args->count(); ++i)
{
argl.append(args->arg(i));
}
return processCommand(argl);
}
// 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], "--restore-internal") == 0)
{
TRACE("Restoring session (new instance request)");
restoreSession();
return true;
}
int option;
Window w = None;
const char *icon = NULL;
int balloon_timeout = 4000;
bool withdraw = true, skip_taskbar = false,
dock_obscure = false, check_normality = true, enable_sm = true;
optind = 0; // initialise the getopt static
while ((option = getopt(argc, argv, "+bdefi:lmop:qtw:")) != EOF)
{
switch (option)
{
case '?':
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 '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
TQTrayLabel *t = (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->setSessionManagement(enable_sm);
t->dock();
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)
{
KMessageBox::error(NULL, err, i18n("TDEDocker"));
}
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 (KMessageBox::warningContinueCancel(NULL,
i18n("The window you are attempting to dock does not seem to be a normal window."),
i18n("TDEDocker")) == KMessageBox::Cancel)
{
return NULL;
}
}
if (!isWindowDocked(w))
{
return new TQTrayLabel(w);
}
TRACE("0x%x is alredy docked", (unsigned) w);
KMessageBox::error(NULL, i18n("This window is already docked.\n"
"Click on system tray icon to toggle docking."), i18n("TDEDocker"));
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 the forked process only.
// Using TDEApplication::kApplication()->quit() crashes the parent application.
exit(0);
return NULL;
}
}
if (pid == -1)
{
KMessageBox::error(NULL, i18n("Failed to fork: %1").arg(strerror(errno)), i18n("Ignore"));
return NULL;
}
TQStringList cmd_line;
int i = 0;
while (argv[i])
{
cmd_line.append(argv[i]);
++i;
}
TQTrayLabel *label = new TQTrayLabel(cmd_line, pid);
TDEApplication::kApplication()->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())
{
TDEApplication::kApplication()->quit();
}
else if (reconnect)
{
TRACE("Reconnecting");
connect(mTrayLabels.getFirst(), TQ_SIGNAL(sysTrayDestroyed()),
this, TQ_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, TQ_SLOT(notifySysTrayAbsence()));
}
void TrayLabelMgr::notifySysTrayAbsence()
{
SysTrayState state = sysTrayStatus(TQPaintDevice::x11AppDisplay());
if (state == SysTrayPresent)
return; // So sweet of the systray to come back so soon
KMessageBox::error(NULL, i18n("The System tray was hidden or removed. All applications "
"will be undocked."), i18n("TDEDocker"));
undockAll();
}
void TrayLabelMgr::restoreSession()
{
// After restoring a session, the TDE session manager will relaunch the applications
// that were previously docked before terminating the previous session. To avoid
// launching the same apps twice, wait for a while before restoring the tdedocker
// session, so that we give tdedocker a chance to simply docks the applications
// already launched by TDE session manager.
restoreSessionTimer.start(5000, true);
}
void TrayLabelMgr::doRestoreSession()
{
TRACE("Restoring session");
TDEConfig *config = TDEApplication::kApplication()->sessionConfig();
if (config->hasGroup("General"))
{
config->setGroup("General");
int count = config->readNumEntry("InstanceCount", 0);
for (int i = 0; i < count; ++i)
{
if (!config->hasGroup(TQString::number(i)))
{
break;
}
config->setGroup(TQString::number(i));
TQString pname = config->readEntry("Application", TQString::null);
if (!pname.isEmpty())
{
TRACE("Restoring Application[%s]", pname.ascii());
manageTrayLabel(new TQTrayLabel(TQStringList::split(" ", pname), 0));
if (!mTrayLabels.getFirst()->restoreState(config))
{
// Failed to restore the application, remove the tray label
delete mTrayLabels.take(0);
}
}
}
}
// Exit if no application could be restored
if (mTrayLabels.isEmpty())
{
TDEApplication::kApplication()->quit();
}
}
bool TrayLabelMgr::saveState(TQSessionManager &sm)
{
TRACE("Saving session");
int i = 0;
TQTrayLabel *t;
TDEConfig *config = TDEApplication::kApplication()->sessionConfig();
TQPtrListIterator<TQTrayLabel> it(mTrayLabels);
for (it.toFirst(); it.current(); ++it)
{
t = it.current();
config->setGroup(TQString::number(i));
if (t->saveState(config))
{
TRACE("Saved instance %i", i);
++i;
}
else
{
config->deleteGroup(TQString::number(i));
}
}
config->setGroup("General");
config->writeEntry("InstanceCount", i);
return true;
}
/*
* 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"