source: tags/0.1/uigtk.py @ 335

Revision 127, 40.2 KB checked in by marc, 5 years ago (diff)

Better fatal error handling, stop server, added BUGS file, fixed dynamic preference handling, added 32x32 icon, deleted legacy folder

  • Property svn:keywords set to Id Rev
Line 
1#! /usr/bin/env python
2# -*- coding: utf8 -*-
3#
4# Itaka is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# any later version.
8#
9# Itaka is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with Itaka; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17#
18# Copyright 2003-2007 Marc E. <santusmarc_at_gmail.com>.
19# http://itaka.jardinpresente.com.ar
20#
21# $Id$
22
23""" Itaka GTK+ GUI """
24
25import sys, os, datetime, traceback, copy
26
27try:
28    from twisted.internet import gtk2reactor
29    gtk2reactor.install()
30
31    from twisted.python import log
32    from twisted.web import server, static
33    from twisted.internet import reactor
34    import twisted.internet.error
35except ImportError:
36    print "[*] Warning: Twisted Network Framework is missing, quitting"
37    sys.exit(1)
38
39try:
40    import server as iserver
41except ImportError:
42    print "[*] ERROR: Failed to import Itaka server module"
43    traceback.print_exc()
44    sys.exit(1)
45
46try:
47    import pygtk
48    pygtk.require("2.0")
49except ImportError:
50    print "[*] WARNING: Pygtk module is missing"
51    pass
52try:
53    import gtk, gobject
54except ImportError:
55    print "[*] ERROR: GTK+ bindings are missing"
56    sys.exit(1)
57
58if gtk.gtk_version[1] < 10:
59    print "[*] ERROR: Itaka requires GTK+ 2.10, you have %s installed" % ".".join(str(x) for x in gtk.gtk_version)
60    sys.exit(1)
61
62class Gui:
63    """ GTK GUI """
64    def __init__(self, consoleinstance, configuration):
65        """ 'consoleinstance' is an instance of the Console class. 'configuration' is a tuple of configuration globals and an instance of the user's configuration values """
66       
67        # Set up some variables
68        self.server_listening = False
69
70        # Load our configuration and console instances and values
71        self.console = consoleinstance
72        self.itakaglobals = configuration[0]
73        # The configuration instance has the user's preferences already loaded.
74        self.configinstance = configuration[1]
75        self.configuration = self.itakaglobals.values
76
77        # Start defining widgets
78        self.icon_pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(self.itakaglobals.image_dir, "itaka.png"))
79        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
80        self.window.connect("destroy", self.destroy)
81        self.window.connect("size-allocate", self.__windowsizechanged)
82        self.window.set_title("Itaka")
83        self.window.set_icon(self.icon_pixbuf)
84        self.window.set_border_width(6)
85        self.window.set_default_size(400, 1)
86        self.window.set_position(gtk.WIN_POS_CENTER)
87
88        # Create our tray icon
89        self.statusIcon = gtk.StatusIcon()
90        self.statusmenu = gtk.Menu()
91        self.statusIcon.set_from_pixbuf(self.icon_pixbuf)
92        self.statusIcon.set_tooltip("Itaka")
93        self.statusIcon.set_visible(True)
94        self.statusIcon.connect('activate', self.__statusicon_activate)
95        self.statusIcon.connect('popup-menu', self.__statusicon_menu, self.statusmenu)
96
97        self.startimage = gtk.Image()
98        self.startimage.set_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU)
99
100        self.stopimage = gtk.Image()
101        self.stopimage.set_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_MENU)
102
103        self.menuitemstart = gtk.ImageMenuItem("Start")
104        self.menuitemstart.set_image(self.startimage)
105        self.menuitemstart.connect('activate', self.startstop, "start")
106        self.menuitemstop = gtk.ImageMenuItem("Stop")
107        self.menuitemstop.set_image(self.stopimage)
108        self.menuitemstop.connect('activate', self.startstop, "stop")
109        self.menuitemstop.set_sensitive(False)
110
111        if self.itakaglobals.notifyavailable:
112            self.menuitemnotifications = gtk.CheckMenuItem("Show Notifications")
113            if (self.configuration['server']['notify'] == "True"):
114                self.menuitemnotifications.set_active(True)
115            self.menuitemnotifications.connect('toggled', self.__statusicon_notify)
116
117        self.menuitemseparator = gtk.SeparatorMenuItem()
118        self.menuitemseparator1 = gtk.SeparatorMenuItem()
119        self.menuitemabout = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
120        self.menuitemabout.connect('activate', self.about)
121        self.menuitemquit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
122        self.menuitemquit.connect('activate', self.destroy)
123
124        self.statusmenu.append(self.menuitemstart)
125        self.statusmenu.append(self.menuitemstop)
126        if self.itakaglobals.notifyavailable:
127            self.statusmenu.append(self.menuitemseparator)
128            self.statusmenu.append(self.menuitemnotifications)
129        self.statusmenu.append(self.menuitemseparator1)
130        self.statusmenu.append(self.menuitemabout)
131        self.statusmenu.append(self.menuitemquit)
132
133        self.vbox = gtk.VBox(False, 6)
134        self.box = gtk.HBox(False, 0)
135
136        self.itakaLogo = gtk.Image()
137        self.itakaLogo.set_from_file(os.path.join(self.itakaglobals.image_dir, "itaka.png"))
138        self.itakaLogo.show()
139
140        self.box.pack_start(self.itakaLogo, True, True, 4)
141
142        self.ibox = gtk.HBox(False, 0)
143        self.buttonStartstop = gtk.ToggleButton("Start", gtk.STOCK_PREFERENCES)
144        self.startstopimage = gtk.Image()
145
146        self.startstopimage.set_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON)
147        self.buttonStartstop.set_image(self.startstopimage)
148        self.buttonStartstop.connect("toggled", self.startstop, "Start/Stop button")
149        self.ibox.pack_start(self.buttonStartstop, True, True, 8)
150
151        self.preferencesButton = gtk.Button("Preferences", gtk.STOCK_PREFERENCES)
152        self.preferencesButton.connect("clicked", self.__expandpreferences)
153        #self.preferencesButton.connect("clicked", ipreferences.Preferences().prefwindow, [config, self.configinstance], self, self.icon_pixbuf)
154
155        # Set up some variables for our timeouts/animations
156        self.preferenceshidden = False
157        self.preferencesexpanded = False
158        self.contracttimeout = None
159        self.expandtimeout = None
160
161        self.ibox.pack_start(self.preferencesButton, True, True, 4)
162
163        self.box.pack_start(self.ibox, True, True, 0)
164        self.vbox.pack_start(self.box, False, False, 0)
165
166        self.statusBox = gtk.HBox(False, 0)
167        self.labelServed = gtk.Label()
168        self.labelLastip = gtk.Label()
169        self.labelTime = gtk.Label()
170
171        self.statusBox.pack_start(self.labelLastip, True, False, 0)
172        self.statusBox.pack_start(self.labelTime, True, False, 0)
173        self.statusBox.pack_start(self.labelServed, True, False, 0)
174
175        # Logger widget (displayed when expanded)
176        self.logvbox = gtk.VBox(False, 0)
177        self.lognotebook = gtk.Notebook()
178        self.lognotebook.set_tab_pos(gtk.POS_BOTTOM)
179
180        self.logeventslabel = gtk.Label("Events")
181        self.logdetailslabel = gtk.Label("Details")
182
183        self.logeventsscroll = gtk.ScrolledWindow()
184        self.logeventsscroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
185        self.logeventsscroll.set_shadow_type(gtk.SHADOW_NONE)
186
187        self.logeventsstore = gtk.ListStore(gtk.gdk.Pixbuf, str)
188        self.logeventstreeview = gtk.TreeView(self.logeventsstore)
189        self.logeventstreeview.set_property("headers-visible", False)
190        self.logeventstreeview.set_property("rules-hint", True)
191
192        self.logeventscolumnicon = gtk.TreeViewColumn()
193        self.logeventscolumntext = gtk.TreeViewColumn()
194        self.logeventstreeview.append_column(self.logeventscolumnicon)
195        self.logeventstreeview.append_column(self.logeventscolumntext)
196
197        self.logeventscellpixbuf = gtk.CellRendererPixbuf()
198        self.logeventscolumnicon.pack_start(self.logeventscellpixbuf)
199        self.logeventscolumnicon.add_attribute(self.logeventscellpixbuf, 'pixbuf', 0)
200
201        self.logeventscelltext = gtk.CellRendererText()
202        self.logeventscolumntext.pack_start(self.logeventscelltext, True)
203        self.logeventscolumntext.add_attribute(self.logeventscelltext, 'text', 1)
204        self.logeventsscroll.add(self.logeventstreeview)
205
206        self.logdetailsscroll = gtk.ScrolledWindow()
207        self.logdetailsscroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
208        self.logdetailsscroll.set_shadow_type(gtk.SHADOW_NONE)
209        self.logdetailstextview = gtk.TextView()
210        self.logdetailstextview.set_wrap_mode(gtk.WRAP_WORD)
211        self.logdetailstextview.set_editable(False)
212        self.logdetailstextview.set_size_request(-1, 160)
213        self.logdetailsbuffer = self.logdetailstextview.get_buffer()
214        self.logdetailsscroll.add(self.logdetailstextview)
215
216        self.lognotebook.append_page(self.logeventsscroll, self.logeventslabel)
217        self.lognotebook.append_page(self.logdetailsscroll, self.logdetailslabel)
218
219        self.loghbox = gtk.HBox(False, 0)
220        self.logclearbutton = gtk.Button("Clear")
221        self.logclearbuttonimage = gtk.Image()
222        self.logclearbuttonimage.set_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_BUTTON)
223        self.logclearbutton.set_image(self.logclearbuttonimage)
224        self.logclearbutton.connect("clicked", self.clearlogger)
225
226        self.logpausebutton = gtk.ToggleButton("Pause")
227        self.logpausebuttonimage = gtk.Image()
228        self.logpausebuttonimage.set_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_BUTTON)
229        self.logpausebutton.set_image(self.logpausebuttonimage)
230        self.logpausebutton.connect("toggled", self.pauselogger)
231
232        self.loghbox.pack_end(self.logclearbutton, False, False, 4)
233        self.loghbox.pack_end(self.logpausebutton, False, False, 4)
234
235        self.logvbox.pack_start(self.lognotebook, False, False, 4)
236        self.logvbox.pack_start(self.loghbox, False, False, 4)
237
238        self.logboxLabel = gtk.Label("<b>Event log</b>")
239        self.logboxLabel.set_use_markup(True)
240
241        # Expander
242        self.expander_size_finalized = False
243        self.expander = gtk.Expander(None)
244        self.expander.set_label_widget(self.logboxLabel)
245        self.expander.connect('notify::expanded', self.__expandlogger)
246
247        # Log to the self.logger function, which sets the buffer for self.debubuffer
248        log.addObserver(self.logger)
249
250        self.vbox.pack_start(self.statusBox, False, False, 4)
251        self.vbox.pack_start(self.expander, False, False, 0)
252        self.expander.set_sensitive(False)
253
254        # This is are the preference widgets that are going to be added and shown later
255        self.preferencesVBox = gtk.VBox(False, 7)
256        self.preferencesVBoxitems = gtk.VBox(False, 5)
257        self.preferencesVBoxitems.set_border_width(2)
258        self.preferencesHBox1 = gtk.HBox(False, 0)
259        self.preferencesHBox2 = gtk.HBox(False, 0)
260        self.preferencesHBox3 = gtk.HBox(False, 0)
261        self.preferencesHBox4 = gtk.HBox(False, 0)
262        self.preferencesHBox5 = gtk.HBox(False, 0)
263
264        self.preferencesFramesettings = gtk.Frame()
265        self.preferencesSettingslabel = gtk.Label("<b>Preferences</b>")
266        self.preferencesSettingslabel.set_use_markup(True)
267        self.preferencesFramesettings.set_label_widget(self.preferencesSettingslabel)
268
269        self.preferencesLabelport = gtk.Label("Port:")
270        self.preferencesLabelport.set_justify(gtk.JUSTIFY_LEFT)
271        self.preferencesLabelport.set_alignment(0, 0.50)
272
273        self.preferencesLabelquality = gtk.Label("Quality:")
274        self.preferencesLabelquality.set_justify(gtk.JUSTIFY_LEFT)
275        self.preferencesLabelquality.set_alignment(0, 0.50)
276
277
278        self.preferencesLabelformat = gtk.Label("Format:")
279        self.preferencesLabelformat.set_justify(gtk.JUSTIFY_LEFT)
280        self.preferencesLabelformat.set_alignment(0, 0.50)
281
282        self.preferencesLabelnotifications = gtk.Label("Notifications:")
283        self.preferencesLabelnotifications.set_justify(gtk.JUSTIFY_LEFT)
284        self.preferencesLabelnotifications.set_alignment(0, 0.50)
285
286        # Spinbuttons
287        self.adjustmentport = gtk.Adjustment(float(self.configuration['server']['port']), 1024, 65535, 1, 0, 0)
288        self.preferencesSpinport = gtk.SpinButton(self.adjustmentport)
289        self.preferencesSpinport.set_numeric(True)
290
291        self.adjustmentquality = gtk.Adjustment(float(self.configuration['screenshot']['quality']), 0, 100, 1, 0, 0)
292        self.preferencesSpinquality = gtk.SpinButton(self.adjustmentquality)
293        self.preferencesSpinquality.set_numeric(True)
294
295        # Combos
296        self.preferencesComboformat = gtk.combo_box_new_text()
297        self.preferencesComboformat.connect('changed', self.__preferencesComboChanged)
298        self.preferencesComboformat.append_text("JPG")
299        self.preferencesComboformat.append_text("PNG")
300        if (self.configuration['screenshot']['format'] == "jpeg"):
301            self.preferencesComboformat.set_active(0)
302        else:
303            self.preferencesComboformat.set_active(1)
304            self.preferencesHBox3.set_sensitive(False)
305
306        self.preferencesChecknotifications = gtk.CheckButton()
307        if (self.configuration['server']['notify'] == "True"):
308            self.preferencesChecknotifications.set_active(1)
309        else:
310            self.preferencesChecknotifications.set_active(0)
311
312        self.preferencesButtonClose = gtk.Button("Close", gtk.STOCK_CLOSE)
313        self.preferencesButtonClose.connect("clicked", lambda wid: self.__contractpreferences())
314       
315        self.preferencesButtonAbout = gtk.Button("About", gtk.STOCK_ABOUT)
316        self.preferencesButtonAbout.connect("clicked", lambda wid: self.about())
317
318        self.preferencesHBox1.pack_start(self.preferencesLabelport, False, False, 12)
319        self.preferencesHBox2.pack_start(self.preferencesLabelformat, False, False, 12)
320        self.preferencesHBox3.pack_start(self.preferencesLabelquality, False, False, 12)
321        self.preferencesHBox4.pack_start(self.preferencesLabelnotifications, False, False, 12)
322
323        self.preferencesHBox1.pack_end(self.preferencesSpinport, False, False, 7)
324        self.preferencesHBox2.pack_end(self.preferencesComboformat, False, False, 7)
325        self.preferencesHBox3.pack_end(self.preferencesSpinquality, False, False, 7)
326        self.preferencesHBox4.pack_end(self.preferencesChecknotifications, False, False, 7)
327        self.preferencesHBox5.pack_start(self.preferencesButtonAbout, False, False, 7)
328        self.preferencesHBox5.pack_end(self.preferencesButtonClose, False, False, 7)
329
330        # Hbox4 contains notifications which is only available in some systems
331        if not self.itakaglobals.notifyavailable:
332            self.preferencesHBox4.set_sensitive(False)
333
334        # Add Hboxes to the Vbox
335        self.preferencesVBoxitems.pack_start(self.preferencesHBox1, False, False, 0)
336        self.preferencesVBoxitems.pack_start(self.preferencesHBox2, False, False, 0)
337        self.preferencesVBoxitems.pack_start(self.preferencesHBox3, False, False, 0)
338        self.preferencesVBoxitems.pack_start(self.preferencesHBox4, False, False, 0)
339
340        self.preferencesFramesettings.add(self.preferencesVBoxitems)
341        self.preferencesVBox.pack_start(self.preferencesFramesettings, False, False, 0)
342        self.preferencesVBox.pack_start(self.preferencesHBox5, False, False, 4)
343
344        self.window.add(self.vbox)
345        self.window.show_all()
346
347        # Once we have all our widgets shown, get the 'initial' real size, for expanding/contracting
348        self.window.initial_size = self.window.get_size()
349
350    def save_preferences(self):
351        """ Save and hide the preferences dialog """
352        # So we can mess with the values in the running one and not mess up our comparison
353        self.currentconfiguration = copy.deepcopy(self.configuration)
354
355        # Switch to the proper values
356        formatvalue = str(self.preferencesComboformat.get_active_text())
357        if formatvalue == "PNG":
358            formatvalue = "png"
359            self.configuration['screenshot']['format'] = 'png'
360        else:
361            formatvalue = "jpeg"
362            self.configuration['screenshot']['format'] = 'jpeg'
363
364        if self.itakaglobals.notifyavailable:
365            notifyvalue = self.preferencesChecknotifications.get_active()
366            if notifyvalue:
367                notifyvalue = 'True'
368                self.menuitemnotifications.set_active(True)
369                self.configuration['server']['notify'] = 'True'
370            else:
371                notifyvalue = 'False'
372                self.menuitemnotifications.set_active(False)
373                self.configuration['server']['notify'] = 'False'
374        else:
375            notifyvalue = 'False'
376            self.configuration['server']['notify'] = 'False'
377
378        # Build a configuration dictionary to send to the configuration engine's
379        # save method. Redundant values must be included for the comparison
380        self.configurationdict = {
381            'html':
382                {'html': '<html><body><img src="screenshot" alt="If you are seeing this message it means there was an error in Itaka or you are using a text-only browser." border="0"></a></body</html>'},
383
384            'screenshot':
385                {'path': '/tmp',
386                'format': formatvalue,
387                'quality': str(self.preferencesSpinquality.get_value_as_int())},
388
389            'server':
390                {'port': str(self.preferencesSpinport.get_value_as_int()),
391                'notify': notifyvalue}
392            }
393
394        # Set them for local use now
395        if self.configuration['screenshot']['quality'] != str(self.preferencesSpinquality.get_value_as_int()):
396            self.configuration['screenshot']['quality'] = str(self.preferencesSpinquality.get_value_as_int())
397
398        if self.configuration['server']['port'] !=  str(self.preferencesSpinport.get_value_as_int()):
399            self.configuration['server']['port'] =  str(self.preferencesSpinport.get_value_as_int())
400            self.restart_server()
401
402        # Check if the configuration changed
403        if (self.configurationdict != self.currentconfiguration):
404            try:
405                self.configinstance.save(self.configurationdict)
406            except:
407                self.console.error(['Gui', 'save'], "Could not save preferences")
408
409    def __expandpreferences(self, params=None):
410        """ Callback to expand the window for preferences. """
411        # We have a race condition here. If GTK cant resize fast enough, then it gets very sluggish
412        # See configure-event signal of gtk.Widget
413        # start timer, resize, catch configure-notify, set up idle handler, when idle resize to what the size should be at this point of time, repeat
414        if self.preferencesexpanded != True:
415            if self.expandtimeout is not None:
416                """NOTE: GTK+ GtkWidget.size_request() method can give you the amount of size a widget will take.
417                however, it has to be show()ned before. For our little hack, we show the preferencesVBox widgets
418                but not itself, which should yield a close enough calculation."""
419                self.preferencesFramesettings.show_all()
420                self.preferencesHBox5.show_all()
421
422                """If the logger is expanded, use that as the initial size.
423                _expander_size is set by our GtkWindow resize callback
424                but we also set a expander_size_finalized variable here
425                so that __windowsizechanged doesnt set the new expanded_size over
426                again as our window is expanding here."""
427               
428                self.expander_size_finalized = False
429                if self.expander.get_expanded():
430                    self.window.normal_size = self.expander_size
431                    self.expander_size_finalized = True
432                else:
433                    self.window.normal_size = self.window.initial_size
434
435                self.increment = 33
436                if self.window.current_size[1] < self.window.normal_size[1]+self.preferencesVBox.size_request()[1]:
437                    # Avoid overexpanding our calculation
438                    if self.window.current_size[1]+self.increment > self.window.normal_size[1]+self.preferencesVBox.size_request()[1]:
439                        self.increment = (self.window.normal_size[1]+self.preferencesVBox.size_request()[1] - self.window.current_size[1])
440
441                    self.window.resize(self.window.current_size[0], self.window.current_size[1]+self.increment)
442                    return True
443                else:
444                    # Its done expanding, add our widgets or display it if it has been done already
445                    self.preferencesButton.set_sensitive(False)
446                    self.preferencesexpanded = True
447
448                    # Reload our configuration and show the preferences
449                    self.configuration = self.configinstance.load(False)
450                    if self.preferenceshidden:
451                        self.preferencesVBox.show_all()
452                    else:
453                        self.vbox.pack_start(self.preferencesVBox, False, False, 0)
454                        self.preferencesVBox.show_all()
455                   
456                    self.expandtimeout = None
457                    return False
458            else:
459                self.expandtimeout = gobject.timeout_add(30, self.__expandpreferences)
460
461    def __contractpreferences(self, params=None):
462        """ Callback to contract the window of preferences. """
463        # TODO: Add save
464
465        if self.contracttimeout is not None:
466            # If you dont use the normal_size proxy to our window sizes,
467            # it generates a nice effect of doing the animation when closing the expander also.
468            # While sexy, it's inconsistent, and most definately a resource hungry bug.
469            if self.expander.get_expanded():
470                self.window.normal_size = self.expander_size
471                self.expander_size_finalized = True
472            else:
473                self.window.normal_size = self.window.initial_size
474           
475            if self.preferencesVBox.get_property("visible"):
476                self.preferencesVBox.hide_all()
477
478            if self.window.current_size[1] > self.window.normal_size[1]:
479                self.window.resize(self.window.current_size[0], self.window.current_size[1]-self.increment)
480                return True
481            else:
482                # Done, set some variables and stop our timer
483                self.preferencesexpanded = False
484                self.preferenceshidden = True
485                self.expander.size_finalized = False
486                self.preferencesButton.set_sensitive(True)
487               
488                # Save our settings
489                self.save_preferences()
490
491                self.contracttimeout = None
492                return False
493        else:
494            self.contracttimeout = gobject.timeout_add(30, self.__contractpreferences)
495
496    def __windowsizechanged(self, widget=None, data=None):
497        """ This is a callback to always have the latest window size  """
498        self.window.current_size = self.window.get_size()
499       
500        # If the logger is expanded, give them a new size unless our preferences expander is working
501        if self.expander.get_expanded() and not self.expander_size_finalized:
502            self.expander_size = self.window.current_size
503            # If the preferences were expanded before the logger
504            if self.preferencesexpanded:
505                # Cant assign tuple items
506                self.expander_size = [self.expander_size[0], self.expander_size[1] - self.preferencesVBox.size_request()[1]]
507
508    def __statusicon_menu(self, widget, button, time, menu):
509        """ Callback to display the menu on the status icon """
510        if button == 3:
511            if menu:
512                menu.show_all()
513                menu.popup(None, None, None, 3, time)
514            pass
515 
516    def __statusicon_activate(self, widget):
517        """ Callback to toggle the window visibility from the status icon """
518        if (self.window.get_property("visible")):
519            self.window.hide()
520        else:
521            self.window.show()
522
523    def __statusicon_notify(self, widget):
524        """ Callback to disable or enable notifications on the fly from the status icon. 'active' is a boolean for the checkbox """
525        if self.menuitemnotifications.get_active():
526            self.configuration['server']['notify'] = 'True'
527        else:
528            self.configuration['server']['notify'] = 'False'
529
530    def about(self, data=None):
531        """ Create the About dialog. """
532        self.aboutdialog = gtk.AboutDialog()
533        self.aboutdialog.set_transient_for(self.window)
534        self.aboutdialog.set_name('Itaka')
535        self.aboutdialog.set_version(self.itakaglobals.version)
536        self.aboutdialog.set_copyright(u'© 2003-2007 Marc E.')
537        self.aboutdialog.set_comments('Screenshooting de mercado.')
538        self.aboutdialog.set_authors(['Marc E. <santusmarc@gmail.com>'])
539        self.aboutdialog.set_artists(['Marc E. <santusmarc@gmail.com>', 'Tango Project (http://tango.freedesktop.org)'])
540        self.aboutdialog.set_license('''Itaka is free software; you can redistribute it and/or modify
541it under the terms of the GNU General Public License as published by
542the Free Software Foundation; either version 2 of the License, or
543any later version.
544
545Itaka is distributed in the hope that it will be useful,
546but WITHOUT ANY WARRANTY; without even the implied warranty of
547MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
548GNU General Public License for more details.
549
550You should have received a copy of the GNU General Public License
551along with Itaka; if not, write to the Free Software
552Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA''')
553        self.aboutdialog.set_website('http://itaka.jardinpresente.com.ar')
554        self.aboutdialog.set_logo(gtk.gdk.pixbuf_new_from_file(os.path.join(self.itakaglobals.image_dir, "itaka64x64.png")))
555        self.aboutdialog.set_icon(self.icon_pixbuf)
556        self.aboutdialog.run()
557        self.aboutdialog.destroy()
558
559    def __expandlogger(self, expander, params):
560        """ Callback for the expander widget. """
561        if self.expander.get_expanded():
562            # Show the debugvbox() and it's subwidgets
563            self.logvbox.show_all()
564
565            self.expander.add(self.logvbox)
566        else:
567            self.expander.remove(self.expander.child)
568            self.window.resize(self.window.initial_size[0], self.window.initial_size[1])
569        return
570
571    def logger(self, args, failure=False, failuretype=None, eventsonly=False, icon=None):
572        """ Handle logging in the GUI. Arguments: 'args' is a dict { 'key': [str(msg)]], however, when handling failures the message tuple contains an extra item containing a detailed message for separation in the GUI log viewers. 'failure' is a boolean specifying whether we are logging an error. 'failuretype' is a string specyfing what kind of failure it is, either 'error', 'warning' or 'debug'. 'eventsonly' is a boolean to spcecify if the log message will only go to the events log. 'icon' is tuple, the first argument is a string of either 'stock' or 'pixbuf', and the second is a string of gtk.STOCK_ICON or a gtk.gdk.pixbuf object. It is used for the event log in the GUI """
573
574        self.message = args['message'][0]
575        # The detailed log gets the detailed mesage on failures
576        if failure:
577            self.message = args['message'][1]
578   
579        # Write out the message to the GUI     
580        self.logdetailsbuffer.insert_at_cursor("\r" +self.message,len("\r" + self.message))
581        # Automatically scroll. Use wrap until fix.
582        self.logdetailstextview.scroll_mark_onscreen(self.logdetailsbuffer.get_insert())
583
584        if eventsonly or failure:
585            # The event log
586            if failure:
587                # Use the non-detailed mesage
588                self.message = args['message'][0]
589                # Failures can not set icons, we set them.
590                if failuretype == "error":
591                    icon = ['stock', 'STOCK_DIALOG_ERROR']
592                elif failuretype == "warning":
593                    icon = ['stock', 'STOCK_DIALOG_WARNING']
594                elif failuretype == "debug":
595                    icon = ['stock', 'STOCK_DIALOG_INFO']
596               
597            if icon is not None:
598                if icon[0] == "stock":
599                    self.insertediter = self.logeventsstore.append([self.logeventstreeview.render_icon(stock_id=getattr(gtk, icon[1]), size=gtk.ICON_SIZE_MENU, detail=None), self.message])
600                    # Select the item if its a failure
601                    if failure:
602                        self.selection = self.logeventstreeview.get_selection()
603                        self.selection.select_iter(self.insertediter)
604
605                else:
606                    self.logeventsstore.append([icon[1], self.message])
607            else:
608                self.logeventsstore.append([None, self.message])
609
610    def clearlogger(self, args):
611        """ Callback to clear the log """
612        self.logeventsstore.clear()
613        self.logdetailsbuffer.set_text("")
614
615    def pauselogger(self, widget, data=None):
616        """ Callback to pause log output. """
617        if widget.get_active():
618            # It would be nice if we could set a center background image to our textview.
619            # However, GTK makes that very hard.
620            """
621            self.logprepausetext = self.logdetailsbuffer.get_text(self.logdetailsbuffer.get_start_iter(), self.logdetailsbuffer.get_end_iter())
622            self.logdetailsbuffer.set_text("")
623
624            self.logdetailsbuffer.create_tag ('center-image', justification = gtk.JUSTIFY_CENTER)
625            self.logdetailsimageiter = self.logdetailsbuffer.get_iter_at_offset(0)
626            self.logdetailsbuffer.insert_pixbuf(self.logdetailsimageiter, self.logdetailstextview.render_icon(stock_id=getattr(gtk, 'STOCK_MEDIA_PAUSE'), size=gtk.ICON_SIZE_DIALOG, detail=None))
627            self.logdetailsbuffer.apply_tag_by_name('center-image', self.logdetailsbuffer.get_iter_at_offset(0), self.logdetailsimageiter)
628
629            """
630            self.logeventsstore.append([self.logeventstreeview.render_icon(stock_id=getattr(gtk, 'STOCK_MEDIA_PAUSE'), size=gtk.ICON_SIZE_MENU, detail=None), "Logging paused"])
631           
632            self.logeventstreeview.set_sensitive(False)
633            self.logdetailstextview.set_sensitive(False)
634
635            log.removeObserver(self.logger)
636        else:
637            log.addObserver(self.logger)
638            self.logdetailstextview.set_sensitive(True)
639            self.logeventstreeview.set_sensitive(True)
640
641            self.logeventsstore.append([self.logeventstreeview.render_icon(stock_id=getattr(gtk, 'STOCK_MEDIA_PLAY'), size=gtk.ICON_SIZE_MENU, detail=None), "Logging resumed"])
642
643    def main(self):
644        """ Main init function. Starts the GUI reactors."""
645
646        # Server reactor (interacts with the Twisted reactor)   
647        self.sreact = reactor.run()
648
649    def __preferencesComboChanged(self, widget):
650        """ Callback for when a preferenes gtk.combo_box is changed """
651        if self.preferencesComboformat.get_active_text() == "PNG":
652            self.preferencesHBox3.set_sensitive(False)
653        else:
654            self.preferencesHBox3.set_sensitive(True)
655
656    def __checkwidget(self, widget):
657        """ Check the status of the toggle button """
658        if hasattr(widget, 'get_active'):
659            return widget.get_active()
660        else:
661            return False
662
663    def startstop(self, widget, traydata=None, dontexpandlogger = False):
664        """ Start or stop the screenshooting server. 'traydata' is a string either 'start' or 'stop' to be used from the Status tray icon or error handling. 'dontexpandlogger' handles whether the logger is expanded or not by default when changing status. """
665        if (self.__checkwidget(widget) or traydata == "start"):
666            if self.server_listening: return
667            # NOTE: Twisted doesnt support hot-restarting as stopListening()/startListening(), just use the old one again
668
669            # Set up the twisted site
670            # Pass a reference of GUI and Console instance to Screenshot module for its notification handling.
671            self.sinstance = iserver.ImageResource(self, self.console)
672            self.root = static.Data(self.configuration['html']['html'], 'text/html; charset=UTF-8')
673            self.root.putChild('screenshot', self.sinstance)
674            self.root.putChild('', self.root)
675            self.site = server.Site(self.root)
676
677            # Start the server. Make an instance to distinguish from self.sreactor().
678            try:
679                self.ilistener = reactor.listenTCP(int(self.configuration['server']['port']), self.site)
680                self.server_listening = True
681            except twisted.internet.error.CannotListenError:
682                self.console.error(('Gui', 'startstop'), 'Failed to start server, port %s is already in use' % (self.configuration['server']['port']), self)
683                # NOTE: This actually calls startstop to stop the server again, acts as a click
684                self.buttonStartstop.set_active(False)
685                return
686
687            # Announce on log & console stdout
688            if self.configuration['screenshot']['format'] == "jpeg":
689                self.console.msg('Server started on port %s' % (self.configuration['server']['port']), self, True, ['stock', 'STOCK_CONNECT'])
690                self.console.msg('Server started on port %s TCP. Serving %s images with %s%% quality.' % (self.configuration['server']['port'], self.configuration['screenshot']['format'].upper(), self.configuration['screenshot']['quality']), self, False, ['stock', 'STOCK_CONNECT'])
691            else:
692                self.console.msg('Server started on port %s.' % (self.configuration['server']['port']), self, True, ['stock', 'STOCK_CONNECT'])
693                self.console.msg('Server started on port %s TCP. Serving %s images.' % (self.configuration['server']['port'], self.configuration['screenshot']['format'].upper()), self, False, ['stock', 'STOCK_CONNECT'])
694
695            # Change buttons
696            self.buttonStartstop.set_active(True)
697            self.buttonStartstop.set_label("Stop")
698            self.startstopimage.set_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON)
699            self.buttonStartstop.set_image(self.startstopimage)
700
701            self.statusIcon.set_tooltip("Itaka - Server running")
702            self.menuitemstart.set_sensitive(False)
703            self.menuitemstop.set_sensitive(True)
704
705            # Close the expander
706            self.expander.set_sensitive(True)
707
708            # I am not sure about this, notification
709            # if (self.configuration['server']['notify'] == "True"):
710            #        self.itakaLogo.set_from_file(os.path.join(self.itakaglobals.image_dir, "itaka-take.png"))
711
712        else:
713            if self.server_listening:
714                self.console.msg("Server stopped", self, True, ['stock', 'STOCK_DISCONNECT'])
715
716                self.ilistener.stopListening()
717                self.server_listening = False
718                # Stop the g_timeout
719                if hasattr(self, 'iagotimer'):
720                    gobject.source_remove(self.iagotimer)
721
722                # Change GUI elements
723                if (traydata):
724                    self.buttonStartstop.set_active(False)
725
726                self.statusIcon.set_tooltip("Itaka")
727                self.startstopimage.set_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON)
728                self.buttonStartstop.set_image(self.startstopimage)
729                self.buttonStartstop.set_label("Start")
730                self.labelLastip.set_text('')
731                self.labelTime.set_text('')
732                self.labelServed.set_text('')
733                if not dontexpandlogger:
734                    self.expander.set_expanded(False)                           
735                    self.expander.set_sensitive(False)
736                self.itakaLogo.set_from_file(os.path.join(self.itakaglobals.image_dir, "itaka.png"))
737                self.menuitemstart.set_sensitive(True)
738                self.menuitemstop.set_sensitive(False)
739
740    def restart_server(self):
741        if self.server_listening:
742            self.console.msg("Restarting the server to listen on port %s" % (self.configuration['server']['port']), self, True, ['stock', 'STOCK_REFRESH'])
743            self.startstop(None, "stop")
744            self.startstop(None, "start")
745
746    def destroy(self, *args):
747        """ Callback for the main window's destroy. """
748        # Stop server.
749        if self.server_listening:
750            self.console.msg("Shutting down server...")
751            self.ilistener.stopListening()
752            del self.console
753        else:
754            # Console goodbye!
755            if hasattr(self, 'console'):
756                del self.console
757
758        # Remove stale screenshot and quit
759        if os.path.exists(os.path.join(self.configuration['screenshot']['path'], 'itakashot.%s' % (self.configuration['screenshot']['format']))):
760            os.remove(os.path.join(self.configuration['screenshot']['path'], 'itakashot.%s' % (self.configuration['screenshot']['format'])))
761
762        # Windows needs this...
763        del self.statusIcon
764
765        gtk.main_quit()
766
767    def __calcsince(self, dtime):
768        """ Function to calculate the time difference from the last request to
769        the current time. Express a datetime.timedelta using a
770        phrase such as "1 hour, 20 minutes". """
771
772        # Create a timedelta from the datetime.datetime and the current time
773        # (you can create your own timedeltas with datetime.timedelta(5, (650 *
774        # 60) * 2, 12) for testing.
775        self.td = datetime.datetime.now() - dtime
776
777        self.pieces = []
778        if self.td.days:
779                self.pieces.append(self.__plural(self.td.days, 'day'))
780
781        self.minutes, self.seconds = divmod(self.td.seconds, 60)
782        self.hours, self.minutes = divmod(self.minutes, 60)
783        if self.hours:
784            self.pieces.append(self.__plural(self.hours, 'hour'))
785        if self.minutes or len(self.pieces) == 0:
786            self.pieces.append(self.__plural(self.minutes, 'minute'))
787
788        # "Time: " + ", ".join(self.pieces[:-1]) + "and" + self.pieces[-1] + " ago"
789
790        self.labelTime.set_text("<b>Time</b>: " + ", ".join(self.pieces) + " ago")
791        self.labelTime.set_use_markup(True)
792
793        # Need this so it runs more than once. Weird.
794        return True
795
796    def __plural(self, count, singular):
797        """ This is a helper function that handles english plural translations """
798
799        # This is the simplest version; a more general version
800        # should handle -y -> -ies, child -> children, etc.
801        return '%d %s%s' % (count, singular, ("", 's')[count != 1])
802
803    def notify(self):
804        """ Change the image on the main screen, for notification purpose. """
805        self.itakaLogo.set_from_file(os.path.join(self.itakaglobals.image_dir, "itaka.png"))
806        self.statusIcon.set_from_pixbuf(self.icon_pixbuf)
807        # Only run this event once
808        return False
809
810    def talk(self, action, number=False, ip=False, time=False):
811        """ Handler for communcations between the server backend, and the GUI. """
812        if (action == "updateGuiStatus" ):
813            self.console.msg("Screenshot number " + str(number) + " served to " + str(ip), self, True, ['pixbuf', gtk.gdk.pixbuf_new_from_file(os.path.join(self.itakaglobals.image_dir, "itaka16x16-take.png"))])
814
815            self.labelServed.set_text("<b>Served</b>: " + str(number))
816            self.labelServed.set_use_markup(True)
817            self.labelLastip.set_text("<b>IP</b>: " + str(ip))
818            self.labelLastip.set_use_markup(True)
819            self.statusIcon.set_tooltip("Itaka - %s served" % (self.__plural(int(number), 'screenshot')))
820            # if (self.configuration['server']['notify'] == "True"):
821            # Show the camera image on tray and interface for 1.5 seconds
822            self.itakaLogo.set_from_file(os.path.join(self.itakaglobals.image_dir, "itaka-take.png"))
823            self.statusIcon.set_from_file(os.path.join(self.itakaglobals.image_dir, "itaka-take.png"))
824            self.itakaLogo.set_from_file(os.path.join(self.itakaglobals.image_dir, "itaka-take.png"))
825            self.notifyimg = gobject.timeout_add(1500, self.notify)
826
827            # Call the update timer function, and add a timer to update the GUI of its
828            # "Last screenshot taken" time
829            self.__calcsince(time)
830            if hasattr(self, 'iagotimer'): gobject.source_remove(self.iagotimer)
831            self.iagotimer = gobject.timeout_add(60000, self.__calcsince, time)
Note: See TracBrowser for help on using the repository browser.