root / pykoticon / trunk / bin / pykoticon @ 176

Revision 176, 19.7 kB (checked in by jerome, 17 years ago)

Changed copyright years.

  • 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, 2007 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.03"
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) or (not retcode["isValid"]) :
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        if self.variables :
240            self.variables[0].SetFocus()
241        self.SetAutoLayout(True)
242        self.SetSizerAndFit(vsizer)
243        self.Layout()
244       
245       
246class PyKotIcon(wx.Frame):
247    """Main class."""
248    def __init__(self, parent, id):
249        self.dialogAnswer = None
250        wx.Frame.__init__(self, parent, id, \
251               _("PyKotIcon info for %s") % self.getCurrentUserName(), \
252               size = (0, 0), \
253               style = wx.FRAME_NO_TASKBAR | wx.NO_FULL_REPAINT_ON_RESIZE)
254                     
255    def getCurrentUserName(self) :
256        """Retrieves the current user's name."""
257        if isWindows :
258            return win32api.GetUserName()
259        else :   
260            try :
261                return pwd.getpwuid(os.geteuid())[0]
262            except :
263                return "** Unknown **"
264           
265    def OnIconify(self, event) :
266        """Iconify/De-iconify the application."""
267        if not self.IsIconized() :
268            self.Iconize(True)
269        self.Hide()
270
271    def OnTaskBarActivate(self, event) :
272        """Show the application if it is minimized."""
273        if self.IsIconized() :
274            self.Iconize(False)
275        if not self.IsShown() :
276            self.Show(True)
277        self.Raise()
278
279    def OnClose(self, event) :
280        """Cleanly quit the application."""
281        if (event is None) \
282           or self.options.allowquit :
283            self.closeServer()
284            self.menu.Destroy()
285            self.tbicon.Destroy()
286            self.Destroy()
287            return True
288        else :   
289            # self.quitIsForbidden()
290            return False
291
292    def OnTaskBarMenu(self, event) :
293        """Open the taskbar menu."""
294        self.tbicon.PopupMenu(self.menu)
295
296    def OnTaskBarClose(self, event) :
297        """React to close from the taskbar."""
298        self.Close()
299           
300    def quitIsForbidden(self) :       
301        """Displays a message indicating that quitting the application is not allowed."""
302        message = _("Sorry, this was forbidden by your system administrator.")
303        caption = _("Information")
304        style = wx.OK | wx.ICON_INFORMATION | wx.STAY_ON_TOP
305        dialog = wx.MessageDialog(self, message, caption, style)
306        dialog.ShowModal()
307        dialog.Destroy()
308       
309    def OnAbout(self, event) :   
310        """Displays the about box."""
311        dialog = wx.MessageDialog(self, aboutbox % globals(), \
312                                        _("About"), \
313                                        wx.OK | wx.ICON_INFORMATION)
314        dialog.Raise()                                       
315        dialog.ShowModal()
316        dialog.Destroy()
317       
318    def showDialog(self, message, yesno) :
319        """Opens a notification dialog."""
320        self.dialogAnswer = None
321        if yesno :
322            caption = _("Confirmation")
323            style = wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION
324        else :
325            caption = _("Information")
326            style = wx.OK | wx.ICON_INFORMATION
327        style |= wx.STAY_ON_TOP   
328        dialog = wx.MessageDialog(self, message, caption, style)
329        dialog.Raise()
330        self.dialogAnswer = ((dialog.ShowModal() == wx.ID_NO) and "CANCEL") or "OK"
331        dialog.Destroy()
332       
333    def askDatas(self, labels, varnames, varvalues) :
334        """Opens a dialog box asking for data entry."""
335        self.dialogAnswer = None
336        dialog = GenericInputDialog(self, wx.ID_ANY, labels, varnames, varvalues)
337        dialog.Raise()
338        retvalues = {}
339        if dialog.ShowModal() == wx.ID_OK :
340            retvalues["isValid"] = True
341            for i in range(len(varnames)) :
342                retvalues[varnames[i]] = dialog.variables[i].GetValue()
343        else :       
344            retvalues["isValid"] = False
345            for k in varvalues.keys() :
346                retvalues[k] = ""
347        self.dialogAnswer = retvalues
348        dialog.Destroy()
349       
350    def closeServer(self) :   
351        """Tells the xml-rpc server to exit."""
352        if not self.quitEvent.isSet() :
353            self.quitEvent.set()
354        server = xmlrpclib.ServerProxy("http://localhost:%s" % self.options.port)   
355        try :
356            # wake the server with an empty request
357            # for it to see the event object
358            # which has just been set
359            server.nop()
360        except :   
361            # Probably already stopped
362            pass
363       
364    def postInit(self, charset, options, arguments) :   
365        """Starts the XML-RPC server."""
366        self.charset = charset
367        self.options = options
368       
369        self.tbicon = wx.TaskBarIcon()
370        self.greenicon = wx.Icon(os.path.join(iconsdir, "pykoticon-green.ico"), \
371                                  wx.BITMAP_TYPE_ICO)
372        self.redicon = wx.Icon(os.path.join(iconsdir, "pykoticon-red.ico"), \
373                                  wx.BITMAP_TYPE_ICO)
374        self.tbicon.SetIcon(self.greenicon, "PyKotIcon")
375       
376        wx.EVT_TASKBAR_LEFT_DCLICK(self.tbicon, self.OnTaskBarActivate)
377        wx.EVT_TASKBAR_RIGHT_UP(self.tbicon, self.OnTaskBarMenu)
378       
379        self.TBMENU_ABOUT = wx.NewId()
380        self.TBMENU_RESTORE = wx.NewId()
381        self.TBMENU_CLOSE = wx.NewId()
382        wx.EVT_MENU(self.tbicon, self.TBMENU_ABOUT, \
383                                 self.OnAbout)
384        wx.EVT_MENU(self.tbicon, self.TBMENU_RESTORE, \
385                                 self.OnTaskBarActivate)
386        wx.EVT_MENU(self.tbicon, self.TBMENU_CLOSE, \
387                                 self.OnTaskBarClose)
388        self.menu = wx.Menu()
389        self.menu.Append(self.TBMENU_ABOUT, _("About"), _("About this software"))
390        if options.allowquit :
391            self.menu.Append(self.TBMENU_CLOSE, _("Quit"), \
392                                                _("Exit the application"))
393        wx.EVT_ICONIZE(self, self.OnIconify)
394        wx.EVT_CLOSE(self, self.OnClose)
395        self.Show(True)
396        self.Hide()
397       
398        self.quitEvent = threading.Event()
399        self.server = MyXMLRPCServer(self, options, arguments)
400       
401    def UTF8ToUserCharset(self, text) :
402        """Converts from UTF-8 to user's charset."""
403        if text is not None :
404            try :
405                return text.decode("UTF-8").encode(self.charset, "replace") 
406            except (UnicodeError, AttributeError) :   
407                try :
408                    # Maybe already in Unicode
409                    return text.encode(self.charset, "replace") 
410                except (UnicodeError, AttributeError) :
411                    pass # Don't know what to do
412        return text
413       
414    def userCharsetToUTF8(self, text) :
415        """Converts from user's charset to UTF-8."""
416        if text is not None :
417            try :
418                return text.decode(self.charset).encode("UTF-8")
419            except (UnicodeError, AttributeError) :
420                try :
421                    return text.decode(self.charset, "replace").encode("UTF-8") 
422                except (UnicodeError, AttributeError) :   
423                    try :
424                        # Maybe already in Unicode
425                        return text.encode("UTF-8", "replace") 
426                    except (UnicodeError, AttributeError) :
427                        pass # Don't know what to do
428        return text
429       
430
431class PyKotIconApp(wx.App):
432    def OnInit(self) :
433        self.frame = PyKotIcon(None, wx.ID_ANY)
434        self.frame.Show(False)
435        self.SetTopWindow(self.frame)
436        return True
437       
438    def postInit(self, charset, options, arguments) :   
439        """Continues processing."""
440        self.frame.postInit(charset, options, arguments)
441       
442       
443def main() :
444    """Program's entry point."""
445    # locale stuff
446    try :
447        locale.setlocale(locale.LC_ALL, ("", None))
448    except (locale.Error, IOError) :
449        locale.setlocale(locale.LC_ALL, None)
450    (language, charset) = locale.getlocale()
451    language = language or "C"
452    charset = ((sys.platform != "win32") and charset) or locale.getpreferredencoding()
453   
454    # translation stuff
455    try :
456        try :
457            trans = gettext.translation("pykoticon", languages=["%s.%s" % (language, charset)], codeset=charset)
458        except TypeError : # Python <2.4
459            trans = gettext.translation("pykoticon", languages=["%s.%s" % (language, charset)])
460        trans.install()
461    except :
462        gettext.NullTranslations().install()
463   
464   
465    parser = optparse.OptionParser(usage="pykoticon [options] server1 [server2 ...]")
466    parser.add_option("-v", "--version", 
467                            action="store_true", 
468                            dest="version",
469                            help=_("show PyKotIcon's version number and exit."))
470    parser.add_option("-c", "--cache", 
471                            type="int", 
472                            default=0, 
473                            dest="cache",
474                            help=_("the duration of the cache in seconds to keep input forms' datas in memory. Defaults to 0 second, meaning no cache."))
475    parser.add_option("-d", "--debug", 
476                            action="store_true", 
477                            dest="debug",
478                            help=_("activate debug mode."))
479    parser.add_option("-p", "--port", 
480                            type="int", 
481                            default=7654, 
482                            dest="port",
483                            help=_("the TCP port PyKotIcon will listen to, default is 7654."))
484    parser.add_option("-q", "--allowquit", 
485                            action="store_true", 
486                            dest="allowquit",
487                            help=_("allow the end user to close the application."))
488    (options, arguments) = parser.parse_args()
489    if options.version :
490        print "PyKotIcon v%(__version__)s" % globals()
491    else :
492        if not (1024 <= options.port <= 65535) :
493            sys.stderr.write(_("The TCP port number specified for --port must be between 1024 and 65535.\n"))
494        elif not (0 <= options.cache <= 86400) :   
495            sys.stderr.write(_("The duration specified for --cache must be between 0 and 86400 seconds.\n"))
496        else :   
497            app = PyKotIconApp()
498            app.postInit(charset, options, arguments)
499            app.MainLoop()
500   
501   
502if __name__ == '__main__':
503    main()
504   
Note: See TracBrowser for help on using the browser.