root / pykoticon / trunk / bin / pykoticon @ 119

Revision 119, 16.6 kB (checked in by jerome, 18 years ago)

Code cleaning.

  • 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
43
44
45if sys.platform == "win32" :
46    isWindows = True
47    try :
48        import win32api
49    except ImportError :   
50        raise ImportError, "Mark Hammond's Win32 Extensions are missing. Please install them."
51    else :   
52        iconsdir = os.path.split(sys.argv[0])[0]
53else :       
54    isWindows = False
55    iconsdir = "/usr/share/pykoticon"   # TODO : change this
56    import pwd
57   
58try :   
59    import wx
60    hasWxPython = True
61except ImportError :   
62    hasWxPython = False
63    raise ImportError, "wxPython is missing. Please install it."
64   
65aboutbox = """PyKotIcon v%(__version__)s (c) 2003-2006 %(__author__)s - %(__author_email__)s
66
67PyKotIcon is generic, networked, cross-platform dialog box manager.
68
69It is often used as a client side companion for PyKota, but it
70can be used from other applications if you want.
71
72This program is free software; you can redistribute it and/or modify
73it under the terms of the GNU General Public License as published by
74the Free Software Foundation; either version 2 of the License, or
75(at your option) any later version.
76
77This program is distributed in the hope that it will be useful,
78but WITHOUT ANY WARRANTY; without even the implied warranty of
79MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
80GNU General Public License for more details.
81
82You should have received a copy of the GNU General Public License
83along with this program; if not, write to the Free Software
84Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA."""
85
86       
87class MyXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer) :
88    """My own server class."""
89    allow_reuse_address = True
90    def __init__(self, frame, printserver, localport, debug=False) :
91        SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, ('0.0.0.0', localport))
92        self.printServer = printserver
93        self.frame = frame
94        self.debug = debug
95        loop = threading.Thread(target=self.mainloop)
96        loop.start()
97       
98    def logDebug(self, message) :   
99        """Logs a debug message if debug mode is active."""
100        if self.debug :
101            sys.stderr.write("%s\n" % message)
102           
103    def export_quitApplication(self) :   
104        """Makes the application quit."""
105        self.frame.quitEvent.set()
106        wx.CallAfter(self.frame.OnClose, None)
107        return True
108       
109    def export_askDatas(self, labels, varnames, varvalues) :
110        """Asks some textual datas defined by a list of labels, a list of variables' names and a list of variables values in a mapping."""
111        values = {}
112        for (key, value) in varvalues.items() :
113            values[key] = self.frame.UTF8ToUserCharset(value.data)
114        wx.CallAfter(self.frame.askDatas, [ self.frame.UTF8ToUserCharset(label.data) for label in labels ], \
115                                          varnames, \
116                                          values)
117        # ugly, isn't it ?
118        while self.frame.dialogAnswer is None :
119            time.sleep(0.1)
120        retcode = self.frame.dialogAnswer   
121        for (key, value) in retcode.items() :
122            if key != "isValid" :
123                retcode[key] = xmlrpclib.Binary(self.frame.userCharsetToUTF8(value))
124        self.frame.dialogAnswer = None # prepare for next call, just in case
125        return retcode
126       
127    def export_showDialog(self, message, yesno) :
128        """Opens a notification or confirmation dialog."""
129        wx.CallAfter(self.frame.showDialog, self.frame.UTF8ToUserCharset(message.data), yesno)
130        # ugly, isn't it ?
131        while self.frame.dialogAnswer is None :
132            time.sleep(0.1)
133        retcode = self.frame.dialogAnswer   
134        self.frame.dialogAnswer = None # prepare for next call, just in case
135        return retcode
136       
137    def export_nop(self) :   
138        """Does nothing, but allows a clean shutdown from the frame itself."""
139        return True
140       
141    def verify_request(self, request, client_address) :
142        """Ensures that requests which don't come from the print server are rejected."""
143        (client, port) = client_address
144        if socket.gethostbyname(self.printServer) == client :
145            self.logDebug("%s accepted." % client)
146            return True
147        else :
148            # Unauthorized access !
149            self.logDebug("%s rejected." % client)
150            return False
151       
152    def _dispatch(self, method, params) :   
153        """Ensure that only export_* methods are available."""
154        return getattr(self, "export_%s" % method)(*params)
155       
156    def mainloop(self) :
157        """XML-RPC Server's main loop."""
158        self.register_function(self.export_askDatas)
159        self.register_function(self.export_showDialog)
160        self.register_function(self.export_quitApplication)
161        self.register_function(self.export_nop)
162        while not self.frame.quitEvent.isSet() :
163            self.handle_request()
164        self.server_close()   
165        sys.exit(0)
166   
167   
168class GenericInputDialog(wx.Dialog) :
169    """Generic input dialog box."""
170    def __init__(self, parent, id, labels, varnames, varvalues):
171        wx.Dialog.__init__(self, parent, id, \
172               _("PyKotIcon data input"), \
173               style = wx.CAPTION \
174                     | wx.THICK_FRAME \
175                     | wx.STAY_ON_TOP \
176                     | wx.DIALOG_MODAL)
177
178        self.variables = []
179        vsizer = wx.BoxSizer(wx.VERTICAL)
180        for i in range(len(varnames)) :
181            varname = varnames[i]
182            try :
183                label = labels[i]
184            except IndexError :   
185                label = ""
186            labelid = wx.NewId()   
187            varid = wx.NewId()
188            label = wx.StaticText(self, labelid, label)
189            if varname.lower().find("password") != -1 :
190                variable = wx.TextCtrl(self, varid, varvalues.get(varname, ""), style=wx.TE_PASSWORD)
191            else :
192                variable = wx.TextCtrl(self, varid, varvalues.get(varname, ""))
193            self.variables.append(variable)   
194            hsizer = wx.BoxSizer(wx.HORIZONTAL)
195            hsizer.Add(label, 0, wx.ALIGN_CENTER | wx.ALIGN_RIGHT | wx.ALL, 5)
196            hsizer.Add(variable, 0, wx.ALIGN_CENTER | wx.ALIGN_LEFT | wx.ALL, 5)
197            vsizer.Add(hsizer, 0, wx.ALIGN_CENTER | wx.ALL, 5)
198           
199        okbutton = wx.Button(self, wx.ID_OK, "OK")   
200        vsizer.Add(okbutton, 0, wx.ALIGN_CENTER | wx.ALL, 5)
201       
202        self.SetAutoLayout(True)
203        self.SetSizerAndFit(vsizer)
204        self.Layout()
205       
206       
207class PyKotIcon(wx.Frame):
208    """Main class."""
209    def __init__(self, parent, id):
210        self.dialogAnswer = None
211        wx.Frame.__init__(self, parent, id, \
212               _("PyKotIcon info for %s") % self.getCurrentUserName(), \
213               size = (1, 1), \
214               style = wx.FRAME_NO_TASKBAR | wx.NO_FULL_REPAINT_ON_RESIZE)
215                     
216        self.tbicon = wx.TaskBarIcon()
217        self.greenicon = wx.Icon(os.path.join(iconsdir, "pykoticon-green.ico"), \
218                                  wx.BITMAP_TYPE_ICO)
219        self.redicon = wx.Icon(os.path.join(iconsdir, "pykoticon-red.ico"), \
220                                  wx.BITMAP_TYPE_ICO)
221        self.tbicon.SetIcon(self.greenicon, "PyKotIcon")
222       
223        wx.EVT_TASKBAR_LEFT_DCLICK(self.tbicon, self.OnTaskBarActivate)
224        wx.EVT_TASKBAR_RIGHT_UP(self.tbicon, self.OnTaskBarMenu)
225       
226        self.TBMENU_ABOUT = wx.NewId()
227        self.TBMENU_RESTORE = wx.NewId()
228        self.TBMENU_CLOSE = wx.NewId()
229        wx.EVT_MENU(self.tbicon, self.TBMENU_ABOUT, \
230                                          self.OnAbout)
231        wx.EVT_MENU(self.tbicon, self.TBMENU_RESTORE, \
232                                          self.OnTaskBarActivate)
233        wx.EVT_MENU(self.tbicon, self.TBMENU_CLOSE, \
234                                          self.OnTaskBarClose)
235        self.menu = wx.Menu()
236        self.menu.Append(self.TBMENU_ABOUT, _("About"))
237        self.menu.Append(self.TBMENU_CLOSE, _("Quit"))
238       
239        wx.EVT_ICONIZE(self, self.OnIconify)
240        wx.EVT_CLOSE(self, self.OnClose)
241        self.Show(True)
242       
243    def getCurrentUserName(self) :
244        """Retrieves the current user's name."""
245        if isWindows :
246            return win32api.GetUserName()
247        else :   
248            try :
249                return pwd.getpwuid(os.geteuid())[0]
250            except :
251                return "** Unknown **"
252           
253    def OnIconify(self, event) :
254        if not self.IsIconized() :
255            self.Iconize(True)
256        self.Hide()
257
258    def OnTaskBarActivate(self, event) :
259        if self.IsIconized() :
260            self.Iconize(False)
261        if not self.IsShown() :
262            self.Show(True)
263        self.Raise()
264
265    def OnClose(self, event) :
266        self.closeServer()
267        self.menu.Destroy()
268        self.tbicon.Destroy()
269        self.Destroy()
270
271    def OnTaskBarMenu(self, event) :
272        self.tbicon.PopupMenu(self.menu)
273
274    def OnTaskBarClose(self, event) :
275        self.Close()
276       
277    def OnAbout(self, event) :   
278        """Displays the about box."""
279        dialog = wx.MessageDialog(self, aboutbox % globals(), _("About"), wx.OK | wx.ICON_INFORMATION)
280        dialog.ShowModal()
281        dialog.Destroy()
282       
283    def showDialog(self, message, yesno) :
284        """Opens a notification dialog."""
285        self.dialogAnswer = None
286        if yesno :
287            caption = _("Confirmation")
288            style = wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION
289        else :
290            caption = _("Information")
291            style = wx.OK | wx.ICON_INFORMATION
292        style |= wx.STAY_ON_TOP   
293        dialog = wx.MessageDialog(self, message, caption, style)
294        self.dialogAnswer = ((dialog.ShowModal() == wx.ID_NO) and "CANCEL") or "OK"
295        dialog.Destroy()
296       
297    def askDatas(self, labels, varnames, varvalues) :
298        """Opens a dialog box asking for data entry."""
299        # use it this way : self.askDatas(["Username", "Password", "Billing code"], ["username", "password", "billingcode"])
300        self.dialogAnswer = None
301        dialog = GenericInputDialog(self, wx.ID_ANY, labels, varnames, varvalues)
302        retvalues = {}
303        if dialog.ShowModal() == wx.ID_OK :
304            retvalues["isValid"] = True
305            for i in range(len(varnames)) :
306                retvalues[varnames[i]] = dialog.variables[i].GetValue()
307        else :       
308            retvalues["isValid"] = False
309            for k in varvalues.keys() :
310                retvalues[k] = ""
311        self.dialogAnswer = retvalues
312        dialog.Destroy()
313       
314    def closeServer(self) :   
315        """Tells the xml-rpc server to exit."""
316        if not self.quitEvent.isSet() :
317            self.quitEvent.set()
318        server = xmlrpclib.ServerProxy("http://localhost:%s" % self.port)   
319        try :
320            # wake the server with an empty request
321            # for it to see the event object
322            # which has just been set
323            server.nop()
324        except :   
325            # Probably already stopped
326            pass
327       
328    def postInit(self, charset, printserver, localport) :   
329        """Starts the XML-RPC server."""
330        self.quitEvent = threading.Event()
331        self.charset = charset
332        self.port = localport
333        self.server = MyXMLRPCServer(self, printserver, localport)
334       
335    def UTF8ToUserCharset(self, text) :
336        """Converts from UTF-8 to user's charset."""
337        if text is not None :
338            try :
339                return text.decode("UTF-8").encode(self.charset, "replace") 
340            except (UnicodeError, AttributeError) :   
341                try :
342                    # Maybe already in Unicode
343                    return text.encode(self.charset, "replace") 
344                except (UnicodeError, AttributeError) :
345                    pass # Don't know what to do
346        return text
347       
348    def userCharsetToUTF8(self, text) :
349        """Converts from user's charset to UTF-8."""
350        if text is not None :
351            try :
352                # We don't necessarily trust the default charset, because
353                # xprint sends us titles in UTF-8 but CUPS gives us an ISO-8859-1 charset !
354                # So we first try to see if the text is already in UTF-8 or not, and
355                # if it is, we delete characters which can't be converted to the user's charset,
356                # then convert back to UTF-8. PostgreSQL 7.3.x used to reject some unicode characters,
357                # this is fixed by the ugly line below :
358                return text.decode("UTF-8").encode(self.charset, "replace").decode(self.charset).encode("UTF-8", "replace")
359            except (UnicodeError, AttributeError) :
360                try :
361                    return text.decode(self.charset).encode("UTF-8", "replace") 
362                except (UnicodeError, AttributeError) :   
363                    try :
364                        # Maybe already in Unicode
365                        return text.encode("UTF-8", "replace") 
366                    except (UnicodeError, AttributeError) :
367                        pass # Don't know what to do
368        return text
369       
370
371class PyKotIconApp(wx.App):
372    def OnInit(self) :
373        self.frame = PyKotIcon(None, wx.ID_ANY)
374        self.frame.Show(False)
375        self.SetTopWindow(self.frame)
376        return True
377       
378    def postInit(self, charset, printserver, localport) :   
379        """Continues processing."""
380        self.frame.postInit(charset, printserver, localport)
381       
382       
383def main(printserver, localport):
384    """Program's entry point."""
385    try :
386        locale.setlocale(locale.LC_ALL, "")
387    except (locale.Error, IOError) :
388        sys.stderr.write("Problem while setting locale.\n")
389    try :
390        gettext.install("pykoticon")
391    except :
392        gettext.NullTranslations().install()
393       
394    localecharset = None
395    try :
396        try :
397            localecharset = locale.nl_langinfo(locale.CODESET)
398        except AttributeError :   
399            try :
400                localecharset = locale.getpreferredencoding()
401            except AttributeError :   
402                try :
403                    localecharset = locale.getlocale()[1]
404                    localecharset = localecharset or locale.getdefaultlocale()[1]
405                except ValueError :   
406                    pass        # Unknown locale, strange...
407    except locale.Error :           
408        pass
409    charset = os.environ.get("CHARSET") or localecharset or "ISO-8859-15"
410   
411    app = PyKotIconApp()
412    try :
413        localport = int(localport)   
414    except (TypeError, ValueError) :   
415        raise ValueError, "Invalid TCP port parameter %s\n" % localport
416    app.postInit(charset, printserver, localport)
417    app.MainLoop()
418   
419   
420def crashed() :   
421    """Minimal crash method."""
422    import traceback
423    lines = []
424    for line in traceback.format_exception(*sys.exc_info()) :
425        lines.extend([l for l in line.split("\n") if l])
426    msg = "ERROR: ".join(["%s\n" % l for l in (["ERROR: PyKotIcon"] + lines)])
427    sys.stderr.write(msg)
428    sys.stderr.flush()
429   
430   
431if __name__ == '__main__':
432    if len(sys.argv) >= 2 :
433        arg = sys.argv[1]
434        if arg in ("-v", "--version") :   
435            print __version__
436        elif arg in ("-h", "--help") :   
437            sys.stderr.write("usage : pykoticon  pykota_server_hostname_or_ip_address  localTCPPort\n")
438        else :
439            main(*sys.argv[1:3])
440    else :   
441        sys.stderr.write("usage : pykoticon  pykota_server_hostname_or_ip_address  localTCPPort\n")
Note: See TracBrowser for help on using the browser.