root / pykoticon / trunk / bin / pykoticon @ 149

Revision 149, 20.3 kB (checked in by jerome, 18 years ago)

Ensures that the taskbar icons is displayed at least once, otherwise it
is not visible at all when typing "pykoticon &".

  • Property svn:keywords set to Id
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4"""PyKotIcon is a generic, networked, cross-platform dialog box manager."""
5
6# PyKotIcon - Client side helper for PyKota and other applications
7#
8# (c) 2003, 2004, 2005, 2006 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22#
23#
24
25__version__ = "1.02"
26__author__ = "Jerome Alet"
27__author_email__ = "alet@librelogiciel.com"
28__license__ = "GNU GPL"
29__url__ = "http://www.pykota.com/software/pykoticon"
30__revision__ = "$Id$"
31
32import sys
33import os
34import time
35import urllib
36import urllib2
37import locale
38import gettext
39import socket
40import threading
41import xmlrpclib
42import SimpleXMLRPCServer
43try :
44    import optparse
45except ImportError :   
46    sys.stderr.write("You need Python v2.3 or higher for PyKotIcon to work.\nAborted.\n")
47    sys.exit(-1)
48
49if sys.platform == "win32" :
50    isWindows = True
51    try :
52        import win32api
53    except ImportError :   
54        raise ImportError, "Mark Hammond's Win32 Extensions are missing. Please install them."
55    else :   
56        iconsdir = os.path.split(sys.argv[0])[0]
57else :       
58    isWindows = False
59    iconsdir = "/usr/share/pykoticon"   # TODO : change this
60    import pwd
61   
62try :   
63    import wx
64    hasWxPython = True
65except ImportError :   
66    hasWxPython = False
67    raise ImportError, "wxPython is missing. Please install it."
68   
69aboutbox = """PyKotIcon v%(__version__)s (c) 2003-2006 %(__author__)s - %(__author_email__)s
70
71PyKotIcon is generic, networked, cross-platform dialog box manager.
72
73It is often used as a client side companion for PyKota, but it
74can be used from other applications if you want.
75
76This program is free software; you can redistribute it and/or modify
77it under the terms of the GNU General Public License as published by
78the Free Software Foundation; either version 2 of the License, or
79(at your option) any later version.
80
81This program is distributed in the hope that it will be useful,
82but WITHOUT ANY WARRANTY; without even the implied warranty of
83MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
84GNU General Public License for more details.
85
86You should have received a copy of the GNU General Public License
87along with this program; if not, write to the Free Software
88Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA."""
89
90       
91class MyXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer) :
92    """My own server class."""
93    allow_reuse_address = True
94    def __init__(self, frame, options, arguments) :
95        SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, \
96                                                       ('0.0.0.0', options.port), \
97                                                       SimpleXMLRPCServer.SimpleXMLRPCRequestHandler, \
98                                                       options.debug)
99        self.frame = frame
100        self.debug = options.debug
101        self.cacheduration = options.cache
102        self.cache = {}
103        self.printServers = [ socket.gethostbyname(arg) for arg in arguments ]
104        if "127.0.0.1" not in self.printServers :
105            self.printServers.append("127.0.0.1") # to allow clean shutdown
106        loop = threading.Thread(target=self.mainloop)
107        loop.start()
108       
109    def logDebug(self, message) :   
110        """Logs a debug message if debug mode is active."""
111        if self.debug :
112            sys.stderr.write("%s\n" % message)
113           
114    def getAnswerFromCache(self, key) :
115        """Tries to extract a value from the cache and returns it if still valid."""
116        cacheentry = self.cache.get(key)
117        if cacheentry is not None :
118            (birth, value) = cacheentry 
119            if (time.time() - birth) < self.cacheduration :
120                self.logDebug("Cache hit for %s" % str(key))
121                return value # NB : we don't extend the life of this entry
122            else :   
123                self.logDebug("Cache expired for %s" % str(key))
124        else :       
125            self.logDebug("Cache miss for %s" % str(key))
126        return None
127       
128    def storeAnswerInCache(self, key, value) :   
129        """Stores an entry in the cache."""
130        self.cache[key] = (time.time(), value)
131        self.logDebug("Cache store for %s" % str(key))
132       
133    def export_askDatas(self, labels, varnames, varvalues) :
134        """Asks some textual datas defined by a list of labels, a list of variables' names and a list of variables values in a mapping."""
135        values = {}
136        for (key, value) in varvalues.items() :
137            values[key] = self.frame.UTF8ToUserCharset(value.data)
138        cachekey = tuple(values.items()) 
139        retcode = self.getAnswerFromCache(cachekey)
140        if retcode is None :
141            wx.CallAfter(self.frame.askDatas, [ self.frame.UTF8ToUserCharset(label.data) for label in labels ], \
142                                              varnames, \
143                                              values)
144            # ugly, isn't it ?
145            while self.frame.dialogAnswer is None :
146                time.sleep(0.1)
147            retcode = self.frame.dialogAnswer   
148            for (key, value) in retcode.items() :
149                if key != "isValid" :
150                    retcode[key] = xmlrpclib.Binary(self.frame.userCharsetToUTF8(value))
151            self.frame.dialogAnswer = None # prepare for next call, just in case
152            self.storeAnswerInCache(cachekey, retcode)
153        return retcode
154       
155    def export_quitApplication(self) :   
156        """Makes the application quit."""
157        self.frame.quitEvent.set()
158        wx.CallAfter(self.frame.OnClose, None)
159        return True
160       
161    def export_showDialog(self, message, yesno) :
162        """Opens a notification or confirmation dialog."""
163        wx.CallAfter(self.frame.showDialog, self.frame.UTF8ToUserCharset(message.data), yesno)
164        # ugly, isn't it ?
165        while self.frame.dialogAnswer is None :
166            time.sleep(0.1)
167        retcode = self.frame.dialogAnswer   
168        self.frame.dialogAnswer = None # prepare for next call, just in case
169        return retcode
170       
171    def export_nop(self) :   
172        """Does nothing, but allows a clean shutdown from the frame itself."""
173        return True
174       
175    def _dispatch(self, method, params) :   
176        """Ensure that only export_* methods are available."""
177        return getattr(self, "export_%s" % method)(*params)
178       
179    def handle_error(self, request, client_address) :   
180        """Doesn't display an ugly traceback in case an error occurs."""
181        self.logDebug("An exception occured while handling an incoming request from %s:%s" % (client_address[0], client_address[1]))
182       
183    def verify_request(self, request, client_address) :
184        """Ensures that requests which don't come from the print server are rejected."""
185        (client, port) = client_address
186        if client in self.printServers :
187            self.logDebug("%s accepted." % client)
188            return True
189        else :
190            # Unauthorized access !
191            self.logDebug("%s rejected." % client)
192            return False
193       
194    def mainloop(self) :
195        """XML-RPC Server's main loop."""
196        self.register_function(self.export_askDatas)
197        self.register_function(self.export_showDialog)
198        self.register_function(self.export_quitApplication)
199        self.register_function(self.export_nop)
200        while not self.frame.quitEvent.isSet() :
201            self.handle_request()
202        self.server_close()   
203        sys.exit(0)
204   
205   
206class GenericInputDialog(wx.Dialog) :
207    """Generic input dialog box."""
208    def __init__(self, parent, id, labels, varnames, varvalues):
209        wx.Dialog.__init__(self, parent, id, \
210               _("PyKotIcon data input"), \
211               style = wx.CAPTION \
212                     | wx.THICK_FRAME \
213                     | wx.STAY_ON_TOP \
214                     | wx.DIALOG_MODAL)
215
216        self.variables = []
217        vsizer = wx.BoxSizer(wx.VERTICAL)
218        for i in range(len(varnames)) :
219            varname = varnames[i]
220            try :
221                label = labels[i]
222            except IndexError :   
223                label = ""
224            labelid = wx.NewId()   
225            varid = wx.NewId()
226            labelst = wx.StaticText(self, labelid, label)
227            if varname.lower().find("password") != -1 :
228                variable = wx.TextCtrl(self, varid, varvalues.get(varname, ""), style=wx.TE_PASSWORD)
229            else :
230                variable = wx.TextCtrl(self, varid, varvalues.get(varname, ""))
231            self.variables.append(variable)   
232            hsizer = wx.BoxSizer(wx.HORIZONTAL)
233            hsizer.Add(labelst, 0, wx.ALIGN_CENTER | wx.ALIGN_RIGHT | wx.ALL, 5)
234            hsizer.Add(variable, 0, wx.ALIGN_CENTER | wx.ALIGN_LEFT | wx.ALL, 5)
235            vsizer.Add(hsizer, 0, wx.ALIGN_CENTER | wx.ALL, 5)
236           
237        okbutton = wx.Button(self, wx.ID_OK, "OK")   
238        vsizer.Add(okbutton, 0, wx.ALIGN_CENTER | wx.ALL, 5)
239       
240        self.SetAutoLayout(True)
241        self.SetSizerAndFit(vsizer)
242        self.Layout()
243       
244       
245class PyKotIcon(wx.Frame):
246    """Main class."""
247    def __init__(self, parent, id):
248        self.dialogAnswer = None
249        wx.Frame.__init__(self, parent, id, \
250               _("PyKotIcon info for %s") % self.getCurrentUserName(), \
251               size = (0, 0), \
252               style = wx.FRAME_NO_TASKBAR | wx.NO_FULL_REPAINT_ON_RESIZE)
253                     
254        self.tbicon = wx.TaskBarIcon()
255        self.greenicon = wx.Icon(os.path.join(iconsdir, "pykoticon-green.ico"), \
256                                  wx.BITMAP_TYPE_ICO)
257        self.redicon = wx.Icon(os.path.join(iconsdir, "pykoticon-red.ico"), \
258                                  wx.BITMAP_TYPE_ICO)
259        self.tbicon.SetIcon(self.greenicon, "PyKotIcon")
260       
261        wx.EVT_TASKBAR_LEFT_DCLICK(self.tbicon, self.OnTaskBarActivate)
262        wx.EVT_TASKBAR_RIGHT_UP(self.tbicon, self.OnTaskBarMenu)
263       
264        self.TBMENU_ABOUT = wx.NewId()
265        self.TBMENU_RESTORE = wx.NewId()
266        self.TBMENU_CLOSE = wx.NewId()
267        wx.EVT_MENU(self.tbicon, self.TBMENU_ABOUT, \
268                                 self.OnAbout)
269        wx.EVT_MENU(self.tbicon, self.TBMENU_RESTORE, \
270                                 self.OnTaskBarActivate)
271        wx.EVT_MENU(self.tbicon, self.TBMENU_CLOSE, \
272                                 self.OnTaskBarClose)
273        self.menu = wx.Menu()
274        self.menu.Append(self.TBMENU_ABOUT, _("About"))
275        self.menu.Append(self.TBMENU_CLOSE, _("Quit"))
276       
277        wx.EVT_ICONIZE(self, self.OnIconify)
278        wx.EVT_CLOSE(self, self.OnClose)
279        self.Show(True)
280        self.Iconize()
281        self.Hide()
282       
283    def getCurrentUserName(self) :
284        """Retrieves the current user's name."""
285        if isWindows :
286            return win32api.GetUserName()
287        else :   
288            try :
289                return pwd.getpwuid(os.geteuid())[0]
290            except :
291                return "** Unknown **"
292           
293    def OnIconify(self, event) :
294        """Iconify/De-iconify the application."""
295        if not self.IsIconized() :
296            self.Iconize(True)
297        self.Hide()
298
299    def OnTaskBarActivate(self, event) :
300        """Show the application if it is minimized."""
301        if self.IsIconized() :
302            self.Iconize(False)
303        if not self.IsShown() :
304            self.Show(True)
305        self.Raise()
306
307    def OnClose(self, event) :
308        """Cleanly quit the application."""
309        if (event is None) or self.options.allowquit :
310            self.closeServer()
311            self.menu.Destroy()
312            self.tbicon.Destroy()
313            self.Destroy()
314        else :   
315            self.quitIsForbidden()
316
317    def OnTaskBarMenu(self, event) :
318        """Open the taskbar menu."""
319        self.tbicon.PopupMenu(self.menu)
320
321    def OnTaskBarClose(self, event) :
322        """React to close from the taskbar."""
323        if self.options.allowquit :
324            self.Close()
325        else :
326            self.quitIsForbidden()
327           
328    def quitIsForbidden(self) :       
329        """Displays a message indicating that quitting the application is not allowed."""
330        message = _("Sorry, this was forbidden by your system administrator.")
331        caption = _("Information")
332        style = wx.OK | wx.ICON_INFORMATION | wx.STAY_ON_TOP
333        dialog = wx.MessageDialog(self, message, caption, style)
334        dialog.ShowModal()
335        dialog.Destroy()
336       
337    def OnAbout(self, event) :   
338        """Displays the about box."""
339        dialog = wx.MessageDialog(self, aboutbox % globals(), \
340                                        _("About"), \
341                                        wx.OK | wx.ICON_INFORMATION)
342        dialog.ShowModal()
343        dialog.Destroy()
344       
345    def showDialog(self, message, yesno) :
346        """Opens a notification dialog."""
347        self.dialogAnswer = None
348        if yesno :
349            caption = _("Confirmation")
350            style = wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION
351        else :
352            caption = _("Information")
353            style = wx.OK | wx.ICON_INFORMATION
354        style |= wx.STAY_ON_TOP   
355        dialog = wx.MessageDialog(self, message, caption, style)
356        self.dialogAnswer = ((dialog.ShowModal() == wx.ID_NO) and "CANCEL") or "OK"
357        dialog.Destroy()
358       
359    def askDatas(self, labels, varnames, varvalues) :
360        """Opens a dialog box asking for data entry."""
361        # use it this way : self.askDatas(["Username", "Password", "Billing code"], ["username", "password", "billingcode"])
362        self.dialogAnswer = None
363        dialog = GenericInputDialog(self, wx.ID_ANY, labels, varnames, varvalues)
364        retvalues = {}
365        if dialog.ShowModal() == wx.ID_OK :
366            retvalues["isValid"] = True
367            for i in range(len(varnames)) :
368                retvalues[varnames[i]] = dialog.variables[i].GetValue()
369        else :       
370            retvalues["isValid"] = False
371            for k in varvalues.keys() :
372                retvalues[k] = ""
373        self.dialogAnswer = retvalues
374        dialog.Destroy()
375       
376    def closeServer(self) :   
377        """Tells the xml-rpc server to exit."""
378        if not self.quitEvent.isSet() :
379            self.quitEvent.set()
380        server = xmlrpclib.ServerProxy("http://localhost:%s" % self.options.port)   
381        try :
382            # wake the server with an empty request
383            # for it to see the event object
384            # which has just been set
385            server.nop()
386        except :   
387            # Probably already stopped
388            pass
389       
390    def postInit(self, charset, options, arguments) :   
391        """Starts the XML-RPC server."""
392        self.quitEvent = threading.Event()
393        self.charset = charset
394        self.options = options
395        self.server = MyXMLRPCServer(self, options, arguments)
396       
397    def UTF8ToUserCharset(self, text) :
398        """Converts from UTF-8 to user's charset."""
399        if text is not None :
400            try :
401                return text.decode("UTF-8").encode(self.charset, "replace") 
402            except (UnicodeError, AttributeError) :   
403                try :
404                    # Maybe already in Unicode
405                    return text.encode(self.charset, "replace") 
406                except (UnicodeError, AttributeError) :
407                    pass # Don't know what to do
408        return text
409       
410    def userCharsetToUTF8(self, text) :
411        """Converts from user's charset to UTF-8."""
412        if text is not None :
413            try :
414                # We don't necessarily trust the default charset, because
415                # xprint sends us titles in UTF-8 but CUPS gives us an ISO-8859-1 charset !
416                # So we first try to see if the text is already in UTF-8 or not, and
417                # if it is, we delete characters which can't be converted to the user's charset,
418                # then convert back to UTF-8. PostgreSQL 7.3.x used to reject some unicode characters,
419                # this is fixed by the ugly line below :
420                return text.decode("UTF-8").encode(self.charset, "replace").decode(self.charset).encode("UTF-8", "replace")
421            except (UnicodeError, AttributeError) :
422                try :
423                    return text.decode(self.charset).encode("UTF-8", "replace") 
424                except (UnicodeError, AttributeError) :   
425                    try :
426                        # Maybe already in Unicode
427                        return text.encode("UTF-8", "replace") 
428                    except (UnicodeError, AttributeError) :
429                        pass # Don't know what to do
430        return text
431       
432
433class PyKotIconApp(wx.App):
434    def OnInit(self) :
435        self.frame = PyKotIcon(None, wx.ID_ANY)
436        self.frame.Show(False)
437        self.SetTopWindow(self.frame)
438        return True
439       
440    def postInit(self, charset, options, arguments) :   
441        """Continues processing."""
442        self.frame.postInit(charset, options, arguments)
443       
444       
445def main() :
446    """Program's entry point."""
447    try :
448        locale.setlocale(locale.LC_ALL, "")
449    except (locale.Error, IOError) :
450        sys.stderr.write("Problem while setting locale.\n")
451    try :
452        gettext.install("pykoticon")
453    except :
454        gettext.NullTranslations().install()
455       
456    localecharset = None
457    try :
458        try :
459            localecharset = locale.nl_langinfo(locale.CODESET)
460        except AttributeError :   
461            try :
462                localecharset = locale.getpreferredencoding()
463            except AttributeError :   
464                try :
465                    localecharset = locale.getlocale()[1]
466                    localecharset = localecharset or locale.getdefaultlocale()[1]
467                except ValueError :   
468                    pass        # Unknown locale, strange...
469    except locale.Error :           
470        pass
471    charset = os.environ.get("CHARSET") or localecharset or "ISO-8859-15"
472   
473    parser = optparse.OptionParser(usage="usage : pykoticon [options] server1 [server2 ...]")
474    parser.add_option("-v", "--version", 
475                            action="store_true", 
476                            dest="version",
477                            help=_("show PyKotIcon's version number and exit."))
478    parser.add_option("-c", "--cache", 
479                            type="int", 
480                            default=0, 
481                            dest="cache",
482                            help=_("the duration of the cache in seconds to keep input forms' datas in memory. Defaults to 0 second, meaning no cache."))
483    parser.add_option("-d", "--debug", 
484                            action="store_true", 
485                            dest="debug",
486                            help=_("activate debug mode."))
487    parser.add_option("-p", "--port", 
488                            type="int", 
489                            default=7654, 
490                            dest="port",
491                            help=_("the TCP port PyKotIcon will listen to, default is 7654."))
492    parser.add_option("-q", "--allowquit", 
493                            action="store_true", 
494                            dest="allowquit",
495                            help=_("allow the end user to close the application."))
496    (options, arguments) = parser.parse_args()
497    if options.version :
498        print "PyKotIcon v%(__version__)s" % globals()
499    else :
500        if not (1024 <= options.port <= 65535) :
501            sys.stderr.write(_("The TCP port number specified for --port must be between 1024 and 65535.\n"))
502        elif not (0 <= options.cache <= 86400) :   
503            sys.stderr.write(_("The duration specified for --cache must be between 0 and 86400 seconds.\n"))
504        else :   
505            app = PyKotIconApp()
506            app.postInit(charset, options, arguments)
507            app.MainLoop()
508   
509   
510if __name__ == '__main__':
511    main()
512   
Note: See TracBrowser for help on using the browser.