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
RevLine 
[47]1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
[119]4"""PyKotIcon is a generic, networked, cross-platform dialog box manager."""
5
[138]6# PyKotIcon - Client side helper for PyKota and other applications
[47]7#
[89]8# (c) 2003, 2004, 2005, 2006 Jerome Alet <alet@librelogiciel.com>
[47]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
[138]25__version__ = "1.02"
[119]26__author__ = "Jerome Alet"
27__author_email__ = "alet@librelogiciel.com"
28__license__ = "GNU GPL"
[137]29__url__ = "http://www.pykota.com/software/pykoticon"
[119]30__revision__ = "$Id$"
31
[47]32import sys
[63]33import os
[119]34import time
[47]35import urllib
36import urllib2
[65]37import locale
38import gettext
[87]39import socket
40import threading
[88]41import xmlrpclib
[87]42import SimpleXMLRPCServer
[123]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)
[47]48
[65]49if sys.platform == "win32" :
[119]50    isWindows = True
[65]51    try :
52        import win32api
53    except ImportError :   
54        raise ImportError, "Mark Hammond's Win32 Extensions are missing. Please install them."
[76]55    else :   
[85]56        iconsdir = os.path.split(sys.argv[0])[0]
[65]57else :       
[119]58    isWindows = False
[76]59    iconsdir = "/usr/share/pykoticon"   # TODO : change this
[65]60    import pwd
[57]61   
[58]62try :   
63    import wx
[119]64    hasWxPython = True
[58]65except ImportError :   
[119]66    hasWxPython = False
[75]67    raise ImportError, "wxPython is missing. Please install it."
[58]68   
[119]69aboutbox = """PyKotIcon v%(__version__)s (c) 2003-2006 %(__author__)s - %(__author_email__)s
[111]70
[119]71PyKotIcon is generic, networked, cross-platform dialog box manager.
72
73It is often used as a client side companion for PyKota, but it
[111]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
[65]90       
[87]91class MyXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer) :
92    """My own server class."""
[88]93    allow_reuse_address = True
[123]94    def __init__(self, frame, options, arguments) :
[122]95        SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, \
[123]96                                                       ('0.0.0.0', options.port), \
[122]97                                                       SimpleXMLRPCServer.SimpleXMLRPCRequestHandler, \
[123]98                                                       options.debug)
[87]99        self.frame = frame
[123]100        self.debug = options.debug
[146]101        self.cacheduration = options.cache
102        self.cache = {}
[123]103        self.printServers = [ socket.gethostbyname(arg) for arg in arguments ]
[124]104        if "127.0.0.1" not in self.printServers :
105            self.printServers.append("127.0.0.1") # to allow clean shutdown
[87]106        loop = threading.Thread(target=self.mainloop)
107        loop.start()
[56]108       
[88]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           
[146]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 :
[147]120                self.logDebug("Cache hit for %s" % str(key))
[146]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
[55]127       
[146]128    def storeAnswerInCache(self, key, value) :   
129        """Stores an entry in the cache."""
130        self.cache[key] = (time.time(), value)
[147]131        self.logDebug("Cache store for %s" % str(key))
[146]132       
[103]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."""
[107]135        values = {}
[119]136        for (key, value) in varvalues.items() :
137            values[key] = self.frame.UTF8ToUserCharset(value.data)
[146]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)
[105]153        return retcode
[102]154       
[146]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       
[100]161    def export_showDialog(self, message, yesno) :
162        """Opens a notification or confirmation dialog."""
[112]163        wx.CallAfter(self.frame.showDialog, self.frame.UTF8ToUserCharset(message.data), yesno)
[92]164        # ugly, isn't it ?
[100]165        while self.frame.dialogAnswer is None :
[97]166            time.sleep(0.1)
[100]167        retcode = self.frame.dialogAnswer   
168        self.frame.dialogAnswer = None # prepare for next call, just in case
[91]169        return retcode
170       
[88]171    def export_nop(self) :   
172        """Does nothing, but allows a clean shutdown from the frame itself."""
173        return True
174       
[138]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       
[87]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
[123]186        if client in self.printServers :
[88]187            self.logDebug("%s accepted." % client)
[87]188            return True
189        else :
190            # Unauthorized access !
[88]191            self.logDebug("%s rejected." % client)
[87]192            return False
[55]193       
[87]194    def mainloop(self) :
195        """XML-RPC Server's main loop."""
[102]196        self.register_function(self.export_askDatas)
[100]197        self.register_function(self.export_showDialog)
[87]198        self.register_function(self.export_quitApplication)
[88]199        self.register_function(self.export_nop)
[87]200        while not self.frame.quitEvent.isSet() :
201            self.handle_request()
[88]202        self.server_close()   
[87]203        sys.exit(0)
[47]204   
[119]205   
[105]206class GenericInputDialog(wx.Dialog) :
[104]207    """Generic input dialog box."""
208    def __init__(self, parent, id, labels, varnames, varvalues):
[105]209        wx.Dialog.__init__(self, parent, id, \
[104]210               _("PyKotIcon data input"), \
[105]211               style = wx.CAPTION \
212                     | wx.THICK_FRAME \
[104]213                     | wx.STAY_ON_TOP \
[105]214                     | wx.DIALOG_MODAL)
215
[106]216        self.variables = []
[104]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()
[136]226            labelst = wx.StaticText(self, labelid, label)
[106]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)   
[104]232            hsizer = wx.BoxSizer(wx.HORIZONTAL)
[136]233            hsizer.Add(labelst, 0, wx.ALIGN_CENTER | wx.ALIGN_RIGHT | wx.ALL, 5)
[104]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           
[105]237        okbutton = wx.Button(self, wx.ID_OK, "OK")   
[104]238        vsizer.Add(okbutton, 0, wx.ALIGN_CENTER | wx.ALL, 5)
[105]239       
[104]240        self.SetAutoLayout(True)
241        self.SetSizerAndFit(vsizer)
242        self.Layout()
243       
[119]244       
[87]245class PyKotIcon(wx.Frame):
[63]246    """Main class."""
247    def __init__(self, parent, id):
[100]248        self.dialogAnswer = None
[104]249        wx.Frame.__init__(self, parent, id, \
[119]250               _("PyKotIcon info for %s") % self.getCurrentUserName(), \
[124]251               size = (0, 0), \
[111]252               style = wx.FRAME_NO_TASKBAR | wx.NO_FULL_REPAINT_ON_RESIZE)
[114]253                     
254        self.tbicon = wx.TaskBarIcon()
[104]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)
[143]259        self.tbicon.SetIcon(self.greenicon, "PyKotIcon")
[104]260       
[114]261        wx.EVT_TASKBAR_LEFT_DCLICK(self.tbicon, self.OnTaskBarActivate)
262        wx.EVT_TASKBAR_RIGHT_UP(self.tbicon, self.OnTaskBarMenu)
[104]263       
[114]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, \
[128]268                                 self.OnAbout)
[114]269        wx.EVT_MENU(self.tbicon, self.TBMENU_RESTORE, \
[128]270                                 self.OnTaskBarActivate)
[114]271        wx.EVT_MENU(self.tbicon, self.TBMENU_CLOSE, \
[128]272                                 self.OnTaskBarClose)
[114]273        self.menu = wx.Menu()
274        self.menu.Append(self.TBMENU_ABOUT, _("About"))
275        self.menu.Append(self.TBMENU_CLOSE, _("Quit"))
[104]276       
277        wx.EVT_ICONIZE(self, self.OnIconify)
[138]278        wx.EVT_CLOSE(self, self.OnClose)
[149]279        self.Show(True)
280        self.Iconize()
281        self.Hide()
[64]282       
[119]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           
[59]293    def OnIconify(self, event) :
[124]294        """Iconify/De-iconify the application."""
[104]295        if not self.IsIconized() :
296            self.Iconize(True)
[111]297        self.Hide()
[58]298
[59]299    def OnTaskBarActivate(self, event) :
[124]300        """Show the application if it is minimized."""
[104]301        if self.IsIconized() :
302            self.Iconize(False)
[59]303        if not self.IsShown() :
[58]304            self.Show(True)
305        self.Raise()
306
[64]307    def OnClose(self, event) :
[124]308        """Cleanly quit the application."""
[138]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()
[58]316
[91]317    def OnTaskBarMenu(self, event) :
[124]318        """Open the taskbar menu."""
[114]319        self.tbicon.PopupMenu(self.menu)
[58]320
[91]321    def OnTaskBarClose(self, event) :
[124]322        """React to close from the taskbar."""
323        if self.options.allowquit :
324            self.Close()
325        else :
326            self.quitIsForbidden()
327           
[127]328    def quitIsForbidden(self) :       
[124]329        """Displays a message indicating that quitting the application is not allowed."""
[138]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()
[91]336       
[111]337    def OnAbout(self, event) :   
338        """Displays the about box."""
[128]339        dialog = wx.MessageDialog(self, aboutbox % globals(), \
340                                        _("About"), \
341                                        wx.OK | wx.ICON_INFORMATION)
[111]342        dialog.ShowModal()
343        dialog.Destroy()
344       
[100]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()
[91]358       
[103]359    def askDatas(self, labels, varnames, varvalues) :
[102]360        """Opens a dialog box asking for data entry."""
361        # use it this way : self.askDatas(["Username", "Password", "Billing code"], ["username", "password", "billingcode"])
[105]362        self.dialogAnswer = None
363        dialog = GenericInputDialog(self, wx.ID_ANY, labels, varnames, varvalues)
[106]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
[105]374        dialog.Destroy()
[104]375       
376    def closeServer(self) :   
377        """Tells the xml-rpc server to exit."""
378        if not self.quitEvent.isSet() :
379            self.quitEvent.set()
[124]380        server = xmlrpclib.ServerProxy("http://localhost:%s" % self.options.port)   
[104]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       
[123]390    def postInit(self, charset, options, arguments) :   
[104]391        """Starts the XML-RPC server."""
392        self.quitEvent = threading.Event()
[112]393        self.charset = charset
[124]394        self.options = options
[123]395        self.server = MyXMLRPCServer(self, options, arguments)
[112]396       
397    def UTF8ToUserCharset(self, text) :
398        """Converts from UTF-8 to user's charset."""
399        if text is not None :
400            try :
[119]401                return text.decode("UTF-8").encode(self.charset, "replace") 
402            except (UnicodeError, AttributeError) :   
[112]403                try :
[119]404                    # Maybe already in Unicode
405                    return text.encode(self.charset, "replace") 
406                except (UnicodeError, AttributeError) :
407                    pass # Don't know what to do
[112]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 :
[119]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) :
[112]422                try :
[119]423                    return text.decode(self.charset).encode("UTF-8", "replace") 
424                except (UnicodeError, AttributeError) :   
[113]425                    try :
[119]426                        # Maybe already in Unicode
427                        return text.encode("UTF-8", "replace") 
428                    except (UnicodeError, AttributeError) :
429                        pass # Don't know what to do
[112]430        return text
[119]431       
[92]432
[111]433class PyKotIconApp(wx.App):
[58]434    def OnInit(self) :
[104]435        self.frame = PyKotIcon(None, wx.ID_ANY)
[111]436        self.frame.Show(False)
[114]437        self.SetTopWindow(self.frame)
[58]438        return True
439       
[123]440    def postInit(self, charset, options, arguments) :   
[81]441        """Continues processing."""
[123]442        self.frame.postInit(charset, options, arguments)
[81]443       
[119]444       
[123]445def main() :
[63]446    """Program's entry point."""
[65]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()
[112]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   
[123]473    parser = optparse.OptionParser(usage="usage : pykoticon [options] server1 [server2 ...]")
474    parser.add_option("-v", "--version", 
475                            action="store_true", 
476                            dest="version",
[146]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."))
[123]483    parser.add_option("-d", "--debug", 
484                            action="store_true", 
485                            dest="debug",
[146]486                            help=_("activate debug mode."))
[123]487    parser.add_option("-p", "--port", 
488                            type="int", 
489                            default=7654, 
490                            dest="port",
[146]491                            help=_("the TCP port PyKotIcon will listen to, default is 7654."))
[123]492    parser.add_option("-q", "--allowquit", 
493                            action="store_true", 
494                            dest="allowquit",
[146]495                            help=_("allow the end user to close the application."))
[123]496    (options, arguments) = parser.parse_args()
497    if options.version :
498        print "PyKotIcon v%(__version__)s" % globals()
499    else :
[146]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()
[63]508   
[119]509   
[58]510if __name__ == '__main__':
[123]511    main()
512   
Note: See TracBrowser for help on using the browser.