root / pykoticon / trunk / bin / pykoticon @ 132

Revision 132, 18.7 kB (checked in by jerome, 18 years ago)

Changed %default to the real default value because of Python2.3.

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