root / pykoticon / trunk / bin / pykoticon @ 123

Revision 123, 17.5 kB (checked in by jerome, 18 years ago)

Now accepts an unlimited number of print servers.
Now uses optparse to parse the command line.
IMPORTANT : the command line's syntax has changed.

  • 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
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.01"
26__author__ = "Jerome Alet"
27__author_email__ = "alet@librelogiciel.com"
28__license__ = "GNU GPL"
29__url__ = "http://www.librelogiciel.com/software/"
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.printServers = [ socket.gethostbyname(arg) for arg in arguments ]
102        loop = threading.Thread(target=self.mainloop)
103        loop.start()
104       
105    def logDebug(self, message) :   
106        """Logs a debug message if debug mode is active."""
107        if self.debug :
108            sys.stderr.write("%s\n" % message)
109           
110    def export_quitApplication(self) :   
111        """Makes the application quit."""
112        self.frame.quitEvent.set()
113        wx.CallAfter(self.frame.OnClose, None)
114        return True
115       
116    def export_askDatas(self, labels, varnames, varvalues) :
117        """Asks some textual datas defined by a list of labels, a list of variables' names and a list of variables values in a mapping."""
118        values = {}
119        for (key, value) in varvalues.items() :
120            values[key] = self.frame.UTF8ToUserCharset(value.data)
121        wx.CallAfter(self.frame.askDatas, [ self.frame.UTF8ToUserCharset(label.data) for label in labels ], \
122                                          varnames, \
123                                          values)
124        # ugly, isn't it ?
125        while self.frame.dialogAnswer is None :
126            time.sleep(0.1)
127        retcode = self.frame.dialogAnswer   
128        for (key, value) in retcode.items() :
129            if key != "isValid" :
130                retcode[key] = xmlrpclib.Binary(self.frame.userCharsetToUTF8(value))
131        self.frame.dialogAnswer = None # prepare for next call, just in case
132        return retcode
133       
134    def export_showDialog(self, message, yesno) :
135        """Opens a notification or confirmation dialog."""
136        wx.CallAfter(self.frame.showDialog, self.frame.UTF8ToUserCharset(message.data), yesno)
137        # ugly, isn't it ?
138        while self.frame.dialogAnswer is None :
139            time.sleep(0.1)
140        retcode = self.frame.dialogAnswer   
141        self.frame.dialogAnswer = None # prepare for next call, just in case
142        return retcode
143       
144    def export_nop(self) :   
145        """Does nothing, but allows a clean shutdown from the frame itself."""
146        return True
147       
148    def verify_request(self, request, client_address) :
149        """Ensures that requests which don't come from the print server are rejected."""
150        (client, port) = client_address
151        if client in self.printServers :
152            self.logDebug("%s accepted." % client)
153            return True
154        else :
155            # Unauthorized access !
156            self.logDebug("%s rejected." % client)
157            return False
158       
159    def _dispatch(self, method, params) :   
160        """Ensure that only export_* methods are available."""
161        return getattr(self, "export_%s" % method)(*params)
162       
163    def mainloop(self) :
164        """XML-RPC Server's main loop."""
165        self.register_function(self.export_askDatas)
166        self.register_function(self.export_showDialog)
167        self.register_function(self.export_quitApplication)
168        self.register_function(self.export_nop)
169        while not self.frame.quitEvent.isSet() :
170            self.handle_request()
171        self.server_close()   
172        sys.exit(0)
173   
174   
175class GenericInputDialog(wx.Dialog) :
176    """Generic input dialog box."""
177    def __init__(self, parent, id, labels, varnames, varvalues):
178        wx.Dialog.__init__(self, parent, id, \
179               _("PyKotIcon data input"), \
180               style = wx.CAPTION \
181                     | wx.THICK_FRAME \
182                     | wx.STAY_ON_TOP \
183                     | wx.DIALOG_MODAL)
184
185        self.variables = []
186        vsizer = wx.BoxSizer(wx.VERTICAL)
187        for i in range(len(varnames)) :
188            varname = varnames[i]
189            try :
190                label = labels[i]
191            except IndexError :   
192                label = ""
193            labelid = wx.NewId()   
194            varid = wx.NewId()
195            label = wx.StaticText(self, labelid, label)
196            if varname.lower().find("password") != -1 :
197                variable = wx.TextCtrl(self, varid, varvalues.get(varname, ""), style=wx.TE_PASSWORD)
198            else :
199                variable = wx.TextCtrl(self, varid, varvalues.get(varname, ""))
200            self.variables.append(variable)   
201            hsizer = wx.BoxSizer(wx.HORIZONTAL)
202            hsizer.Add(label, 0, wx.ALIGN_CENTER | wx.ALIGN_RIGHT | wx.ALL, 5)
203            hsizer.Add(variable, 0, wx.ALIGN_CENTER | wx.ALIGN_LEFT | wx.ALL, 5)
204            vsizer.Add(hsizer, 0, wx.ALIGN_CENTER | wx.ALL, 5)
205           
206        okbutton = wx.Button(self, wx.ID_OK, "OK")   
207        vsizer.Add(okbutton, 0, wx.ALIGN_CENTER | wx.ALL, 5)
208       
209        self.SetAutoLayout(True)
210        self.SetSizerAndFit(vsizer)
211        self.Layout()
212       
213       
214class PyKotIcon(wx.Frame):
215    """Main class."""
216    def __init__(self, parent, id):
217        self.dialogAnswer = None
218        wx.Frame.__init__(self, parent, id, \
219               _("PyKotIcon info for %s") % self.getCurrentUserName(), \
220               size = (1, 1), \
221               style = wx.FRAME_NO_TASKBAR | wx.NO_FULL_REPAINT_ON_RESIZE)
222                     
223        self.tbicon = wx.TaskBarIcon()
224        self.greenicon = wx.Icon(os.path.join(iconsdir, "pykoticon-green.ico"), \
225                                  wx.BITMAP_TYPE_ICO)
226        self.redicon = wx.Icon(os.path.join(iconsdir, "pykoticon-red.ico"), \
227                                  wx.BITMAP_TYPE_ICO)
228        self.tbicon.SetIcon(self.greenicon, "PyKotIcon")
229       
230        wx.EVT_TASKBAR_LEFT_DCLICK(self.tbicon, self.OnTaskBarActivate)
231        wx.EVT_TASKBAR_RIGHT_UP(self.tbicon, self.OnTaskBarMenu)
232       
233        self.TBMENU_ABOUT = wx.NewId()
234        self.TBMENU_RESTORE = wx.NewId()
235        self.TBMENU_CLOSE = wx.NewId()
236        wx.EVT_MENU(self.tbicon, self.TBMENU_ABOUT, \
237                                          self.OnAbout)
238        wx.EVT_MENU(self.tbicon, self.TBMENU_RESTORE, \
239                                          self.OnTaskBarActivate)
240        wx.EVT_MENU(self.tbicon, self.TBMENU_CLOSE, \
241                                          self.OnTaskBarClose)
242        self.menu = wx.Menu()
243        self.menu.Append(self.TBMENU_ABOUT, _("About"))
244        self.menu.Append(self.TBMENU_CLOSE, _("Quit"))
245       
246        wx.EVT_ICONIZE(self, self.OnIconify)
247        wx.EVT_CLOSE(self, self.OnClose)
248        self.Show(True)
249       
250    def getCurrentUserName(self) :
251        """Retrieves the current user's name."""
252        if isWindows :
253            return win32api.GetUserName()
254        else :   
255            try :
256                return pwd.getpwuid(os.geteuid())[0]
257            except :
258                return "** Unknown **"
259           
260    def OnIconify(self, event) :
261        if not self.IsIconized() :
262            self.Iconize(True)
263        self.Hide()
264
265    def OnTaskBarActivate(self, event) :
266        if self.IsIconized() :
267            self.Iconize(False)
268        if not self.IsShown() :
269            self.Show(True)
270        self.Raise()
271
272    def OnClose(self, event) :
273        self.closeServer()
274        self.menu.Destroy()
275        self.tbicon.Destroy()
276        self.Destroy()
277
278    def OnTaskBarMenu(self, event) :
279        self.tbicon.PopupMenu(self.menu)
280
281    def OnTaskBarClose(self, event) :
282        self.Close()
283       
284    def OnAbout(self, event) :   
285        """Displays the about box."""
286        dialog = wx.MessageDialog(self, aboutbox % globals(), _("About"), wx.OK | wx.ICON_INFORMATION)
287        dialog.ShowModal()
288        dialog.Destroy()
289       
290    def showDialog(self, message, yesno) :
291        """Opens a notification dialog."""
292        self.dialogAnswer = None
293        if yesno :
294            caption = _("Confirmation")
295            style = wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION
296        else :
297            caption = _("Information")
298            style = wx.OK | wx.ICON_INFORMATION
299        style |= wx.STAY_ON_TOP   
300        dialog = wx.MessageDialog(self, message, caption, style)
301        self.dialogAnswer = ((dialog.ShowModal() == wx.ID_NO) and "CANCEL") or "OK"
302        dialog.Destroy()
303       
304    def askDatas(self, labels, varnames, varvalues) :
305        """Opens a dialog box asking for data entry."""
306        # use it this way : self.askDatas(["Username", "Password", "Billing code"], ["username", "password", "billingcode"])
307        self.dialogAnswer = None
308        dialog = GenericInputDialog(self, wx.ID_ANY, labels, varnames, varvalues)
309        retvalues = {}
310        if dialog.ShowModal() == wx.ID_OK :
311            retvalues["isValid"] = True
312            for i in range(len(varnames)) :
313                retvalues[varnames[i]] = dialog.variables[i].GetValue()
314        else :       
315            retvalues["isValid"] = False
316            for k in varvalues.keys() :
317                retvalues[k] = ""
318        self.dialogAnswer = retvalues
319        dialog.Destroy()
320       
321    def closeServer(self) :   
322        """Tells the xml-rpc server to exit."""
323        if not self.quitEvent.isSet() :
324            self.quitEvent.set()
325        server = xmlrpclib.ServerProxy("http://localhost:%s" % self.port)   
326        try :
327            # wake the server with an empty request
328            # for it to see the event object
329            # which has just been set
330            server.nop()
331        except :   
332            # Probably already stopped
333            pass
334       
335    def postInit(self, charset, options, arguments) :   
336        """Starts the XML-RPC server."""
337        self.quitEvent = threading.Event()
338        self.charset = charset
339        self.port = options.port
340        self.server = MyXMLRPCServer(self, options, arguments)
341       
342    def UTF8ToUserCharset(self, text) :
343        """Converts from UTF-8 to user's charset."""
344        if text is not None :
345            try :
346                return text.decode("UTF-8").encode(self.charset, "replace") 
347            except (UnicodeError, AttributeError) :   
348                try :
349                    # Maybe already in Unicode
350                    return text.encode(self.charset, "replace") 
351                except (UnicodeError, AttributeError) :
352                    pass # Don't know what to do
353        return text
354       
355    def userCharsetToUTF8(self, text) :
356        """Converts from user's charset to UTF-8."""
357        if text is not None :
358            try :
359                # We don't necessarily trust the default charset, because
360                # xprint sends us titles in UTF-8 but CUPS gives us an ISO-8859-1 charset !
361                # So we first try to see if the text is already in UTF-8 or not, and
362                # if it is, we delete characters which can't be converted to the user's charset,
363                # then convert back to UTF-8. PostgreSQL 7.3.x used to reject some unicode characters,
364                # this is fixed by the ugly line below :
365                return text.decode("UTF-8").encode(self.charset, "replace").decode(self.charset).encode("UTF-8", "replace")
366            except (UnicodeError, AttributeError) :
367                try :
368                    return text.decode(self.charset).encode("UTF-8", "replace") 
369                except (UnicodeError, AttributeError) :   
370                    try :
371                        # Maybe already in Unicode
372                        return text.encode("UTF-8", "replace") 
373                    except (UnicodeError, AttributeError) :
374                        pass # Don't know what to do
375        return text
376       
377
378class PyKotIconApp(wx.App):
379    def OnInit(self) :
380        self.frame = PyKotIcon(None, wx.ID_ANY)
381        self.frame.Show(False)
382        self.SetTopWindow(self.frame)
383        return True
384       
385    def postInit(self, charset, options, arguments) :   
386        """Continues processing."""
387        self.frame.postInit(charset, options, arguments)
388       
389       
390def main() :
391    """Program's entry point."""
392    try :
393        locale.setlocale(locale.LC_ALL, "")
394    except (locale.Error, IOError) :
395        sys.stderr.write("Problem while setting locale.\n")
396    try :
397        gettext.install("pykoticon")
398    except :
399        gettext.NullTranslations().install()
400       
401    localecharset = None
402    try :
403        try :
404            localecharset = locale.nl_langinfo(locale.CODESET)
405        except AttributeError :   
406            try :
407                localecharset = locale.getpreferredencoding()
408            except AttributeError :   
409                try :
410                    localecharset = locale.getlocale()[1]
411                    localecharset = localecharset or locale.getdefaultlocale()[1]
412                except ValueError :   
413                    pass        # Unknown locale, strange...
414    except locale.Error :           
415        pass
416    charset = os.environ.get("CHARSET") or localecharset or "ISO-8859-15"
417   
418    parser = optparse.OptionParser(usage="usage : pykoticon [options] server1 [server2 ...]")
419    parser.add_option("-v", "--version", 
420                            action="store_true", 
421                            dest="version",
422                            help=_("show PyKotIcon's version number and exit"))
423    parser.add_option("-d", "--debug", 
424                            action="store_true", 
425                            dest="debug",
426                            help=_("activate debug mode"))
427    parser.add_option("-p", "--port", 
428                            type="int", 
429                            default=7654, 
430                            dest="port",
431                            help=_("the TCP port PyKotIcon will listen to, default is %default"))
432    parser.add_option("-q", "--allowquit", 
433                            action="store_true", 
434                            dest="allowquit",
435                            help=_("allow the end user to close the application"))
436    (options, arguments) = parser.parse_args()
437    if options.version :
438        print "PyKotIcon v%(__version__)s" % globals()
439    else :
440        app = PyKotIconApp()
441        app.postInit(charset, options, arguments)
442        app.MainLoop()
443   
444   
445def crashed() :   
446    """Minimal crash method."""
447    import traceback
448    lines = []
449    for line in traceback.format_exception(*sys.exc_info()) :
450        lines.extend([l for l in line.split("\n") if l])
451    msg = "ERROR: ".join(["%s\n" % l for l in (["ERROR: PyKotIcon"] + lines)])
452    sys.stderr.write(msg)
453    sys.stderr.flush()
454   
455   
456if __name__ == '__main__':
457    main()
458   
Note: See TracBrowser for help on using the browser.