Cleanup and instrumenting for profiling
[ejpi] / src / ejpi_glade.py
1 #!/usr/bin/python
2
3 """
4 @todo Add preference file
5         @li enable/disable plugins
6         @li plugin search path
7         @li Number format
8         @li Current tab
9 @todo Expand operations to support
10         @li mathml then to cairo?
11         @li cairo directly?
12 @todo Expanded copy/paste (Unusure how far to go)
13         @li Copy formula, value, serialized, mathml, latex?
14         @li Paste serialized, value?
15
16 Some useful things on Maemo
17 @li http://maemo.org/api_refs/4.1/libosso-2.16-1/group__Statesave.html
18 @li http://maemo.org/api_refs/4.1/libosso-2.16-1/group__Autosave.html
19 """
20
21
22 from __future__ import with_statement
23
24
25 import sys
26 import gc
27 import os
28 import string
29 import logging
30 import warnings
31
32 import gtk
33 import gtk.glade
34
35 import hildonize
36
37 from libraries import gtkpie
38 from libraries import gtkpieboard
39 import plugin_utils
40 import history
41 import gtkhistory
42 import gtk_toolbox
43 import constants
44
45
46 _moduleLogger = logging.getLogger("ejpi_glade")
47
48 PLUGIN_SEARCH_PATHS = [
49         os.path.join(os.path.dirname(__file__), "plugins/"),
50 ]
51
52 PROFILE_STARTUP = False
53
54
55 class ValueEntry(object):
56
57         def __init__(self, widget):
58                 self.__widget = widget
59                 self.__actualEntryDisplay = ""
60
61         def get_value(self):
62                 value = self.__actualEntryDisplay.strip()
63                 if any(
64                         0 < value.find(whitespace)
65                         for whitespace in string.whitespace
66                 ):
67                         self.clear()
68                         raise ValueError('Invalid input "%s"' % value)
69                 return value
70
71         def set_value(self, value):
72                 value = value.strip()
73                 if any(
74                         0 < value.find(whitespace)
75                         for whitespace in string.whitespace
76                 ):
77                         raise ValueError('Invalid input "%s"' % value)
78                 self.__actualEntryDisplay = value
79                 self.__widget.set_text(value)
80
81         def append(self, value):
82                 value = value.strip()
83                 if any(
84                         0 < value.find(whitespace)
85                         for whitespace in string.whitespace
86                 ):
87                         raise ValueError('Invalid input "%s"' % value)
88                 self.set_value(self.get_value() + value)
89
90         def pop(self):
91                 value = self.get_value()[0:-1]
92                 self.set_value(value)
93
94         def clear(self):
95                 self.set_value("")
96
97         value = property(get_value, set_value, clear)
98
99
100 class Calculator(object):
101
102         _glade_files = [
103                 '/usr/lib/ejpi/ejpi.glade',
104                 os.path.join(os.path.dirname(__file__), "ejpi.glade"),
105                 os.path.join(os.path.dirname(__file__), "../lib/ejpi.glade"),
106         ]
107
108         _plugin_search_paths = [
109                 "/usr/lib/ejpi/plugins/",
110                 os.path.join(os.path.dirname(__file__), "plugins/"),
111         ]
112
113         _user_data = os.path.expanduser("~/.%s/" % constants.__app_name__)
114         _user_settings = "%s/settings.ini" % _user_data
115         _user_history = "%s/history.stack" % _user_data
116
117         MIN_BUTTON_SIZE = min(800, 480) // 6 - 20
118
119         def __init__(self):
120                 self.__constantPlugins = plugin_utils.ConstantPluginManager()
121                 self.__constantPlugins.add_path(*self._plugin_search_paths)
122                 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
123                         try:
124                                 pluginId = self.__constantPlugins.lookup_plugin(pluginName)
125                                 self.__constantPlugins.enable_plugin(pluginId)
126                         except:
127                                 warnings.warn("Failed to load plugin %s" % pluginName)
128
129                 self.__operatorPlugins = plugin_utils.OperatorPluginManager()
130                 self.__operatorPlugins.add_path(*self._plugin_search_paths)
131                 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
132                         try:
133                                 pluginId = self.__operatorPlugins.lookup_plugin(pluginName)
134                                 self.__operatorPlugins.enable_plugin(pluginId)
135                         except:
136                                 warnings.warn("Failed to load plugin %s" % pluginName)
137
138                 self.__keyboardPlugins = plugin_utils.KeyboardPluginManager()
139                 self.__keyboardPlugins.add_path(*self._plugin_search_paths)
140                 self.__activeKeyboards = []
141
142                 for path in self._glade_files:
143                         if os.path.isfile(path):
144                                 self._widgetTree = gtk.glade.XML(path)
145                                 break
146                 else:
147                         self.display_error_message("Cannot find ejpi.glade")
148                         gtk.main_quit()
149                         return
150                 try:
151                         os.makedirs(self._user_data)
152                 except OSError, e:
153                         if e.errno != 17:
154                                 raise
155
156                 self._clipboard = gtk.clipboard_get()
157                 self._window = self._widgetTree.get_widget("mainWindow")
158
159                 self._app = None
160                 self._isFullScreen = False
161                 self._app = hildonize.get_app_class()()
162                 self._window = hildonize.hildonize_window(self._app, self._window)
163
164                 menu = hildonize.hildonize_menu(
165                         self._window,
166                         self._widgetTree.get_widget("mainMenubar"),
167                         []
168                 )
169
170                 for scrollingWidgetName in (
171                         "scrollingHistory",
172                 ):
173                         scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
174                         assert scrollingWidget is not None, scrollingWidgetName
175                         hildonize.hildonize_scrollwindow_with_viewport(scrollingWidget)
176
177                 self.__errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
178                 self.__userEntry = ValueEntry(self._widgetTree.get_widget("entryView"))
179                 self.__stackView = self._widgetTree.get_widget("historyView")
180                 self.__pluginButton = self._widgetTree.get_widget("keyboardSelectionButton")
181
182                 self.__historyStore = gtkhistory.GtkCalcHistory(self.__stackView)
183                 self.__history = history.RpnCalcHistory(
184                         self.__historyStore,
185                         self.__userEntry, self.__errorDisplay,
186                         self.__constantPlugins.constants, self.__operatorPlugins.operators
187                 )
188                 self.__load_history()
189
190                 # Basic keyboard stuff
191                 self.__sliceStyle = gtkpie.generate_pie_style(gtk.Button())
192                 self.__handler = gtkpieboard.KeyboardHandler(self._on_entry_direct)
193                 self.__handler.register_command_handler("push", self._on_push)
194                 self.__handler.register_command_handler("unpush", self._on_unpush)
195                 self.__handler.register_command_handler("backspace", self._on_entry_backspace)
196                 self.__handler.register_command_handler("clear", self._on_entry_clear)
197
198                 # Main keyboard
199                 builtinKeyboardId = self.__keyboardPlugins.lookup_plugin("Builtin")
200                 self.__keyboardPlugins.enable_plugin(builtinKeyboardId)
201                 self.__builtinPlugin = self.__keyboardPlugins.keyboards["Builtin"].construct_keyboard()
202                 self.__builtinKeyboard = self.__builtinPlugin.setup(self.__history, self.__sliceStyle, self.__handler)
203                 self._widgetTree.get_widget("mainKeyboard").pack_start(self.__builtinKeyboard)
204                 for child in self.__builtinKeyboard.get_children():
205                         child.set_size_request(self.MIN_BUTTON_SIZE, self.MIN_BUTTON_SIZE)
206
207                 # Plugins
208                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Trigonometry"))
209                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Computer"))
210                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Alphabet"))
211                 self._set_plugin_kb(0)
212
213                 # Callbacks
214                 callbackMapping = {
215                         "on_calculator_quit": self._on_close,
216                         "on_paste": self._on_paste,
217                         "on_clear_history": self._on_clear_all,
218                         "on_about": self._on_about_activate,
219                 }
220                 self._widgetTree.signal_autoconnect(callbackMapping)
221                 self._widgetTree.get_widget("copyMenuItem").connect("activate", self._on_copy)
222                 self._widgetTree.get_widget("copyEquationMenuItem").connect("activate", self._on_copy_equation)
223                 self._window.connect("key-press-event", self._on_key_press)
224                 self._window.connect("window-state-event", self._on_window_state_change)
225                 self._widgetTree.get_widget("entryView").connect("activate", self._on_push)
226                 self.__pluginButton.connect("clicked", self._on_kb_plugin_selection_button)
227
228                 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
229                 self._window.connect("destroy", self._on_close)
230                 self._window.show_all()
231
232                 if not hildonize.IS_HILDON_SUPPORTED:
233                         _moduleLogger.warning("No hildonization support")
234
235                 try:
236                         import osso
237                 except ImportError:
238                         osso = None
239                 self._osso = None
240                 if osso is not None:
241                         self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
242                         device = osso.DeviceState(self._osso)
243                         device.set_device_state_callback(self._on_device_state_change, 0)
244                 else:
245                         _moduleLogger.warning("No OSSO support")
246
247         def display_error_message(self, msg):
248                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
249
250                 def close(dialog, response, editor):
251                         editor.about_dialog = None
252                         dialog.destroy()
253                 error_dialog.connect("response", close, self)
254                 error_dialog.run()
255
256         def enable_plugin(self, pluginId):
257                 self.__keyboardPlugins.enable_plugin(pluginId)
258                 pluginData = self.__keyboardPlugins.plugin_info(pluginId)
259                 pluginName = pluginData[0]
260                 plugin = self.__keyboardPlugins.keyboards[pluginName].construct_keyboard()
261                 pluginKeyboard = plugin.setup(self.__history, self.__sliceStyle, self.__handler)
262                 for child in pluginKeyboard.get_children():
263                         child.set_size_request(self.MIN_BUTTON_SIZE, self.MIN_BUTTON_SIZE)
264
265                 self.__activeKeyboards.append({
266                         "pluginName": pluginName,
267                         "plugin": plugin,
268                         "pluginKeyboard": pluginKeyboard,
269                 })
270
271         def _on_kb_plugin_selection_button(self, *args):
272                 try:
273                         pluginNames = [plugin["pluginName"] for plugin in self.__activeKeyboards]
274                         oldIndex = pluginNames.index(self.__pluginButton.get_label())
275                         newIndex = hildonize.touch_selector(self._window, "Keyboards", pluginNames, oldIndex)
276                         self._set_plugin_kb(newIndex)
277                 except Exception:
278                         self.__errorDisplay.push_exception()
279
280         def _set_plugin_kb(self, pluginIndex):
281                 plugin = self.__activeKeyboards[pluginIndex]
282                 self.__pluginButton.set_label(plugin["pluginName"])
283
284                 pluginParent = self._widgetTree.get_widget("pluginKeyboard")
285                 oldPluginChildren = pluginParent.get_children()
286                 if oldPluginChildren:
287                         assert len(oldPluginChildren) == 1, "%r" % (oldPluginChildren, )
288                         pluginParent.remove(oldPluginChildren[0])
289                         oldPluginChildren[0].hide()
290                 pluginKeyboard = plugin["pluginKeyboard"]
291                 pluginParent.pack_start(pluginKeyboard)
292
293                 pluginKeyboard.show_all()
294
295         def __load_history(self):
296                 serialized = []
297                 try:
298                         with open(self._user_history, "rU") as f:
299                                 serialized = (
300                                         (part.strip() for part in line.split(" "))
301                                         for line in f.readlines()
302                                 )
303                 except IOError, e:
304                         if e.errno != 2:
305                                 raise
306                 self.__history.deserialize_stack(serialized)
307
308         def __save_history(self):
309                 serialized = self.__history.serialize_stack()
310                 with open(self._user_history, "w") as f:
311                         for lineData in serialized:
312                                 line = " ".join(data for data in lineData)
313                                 f.write("%s\n" % line)
314
315         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
316                 """
317                 For system_inactivity, we have no background tasks to pause
318
319                 @note Hildon specific
320                 """
321                 try:
322                         if memory_low:
323                                 gc.collect()
324
325                         if save_unsaved_data or shutdown:
326                                 self.__save_history()
327                 except Exception:
328                         self.__errorDisplay.push_exception()
329
330         def _on_window_state_change(self, widget, event, *args):
331                 """
332                 @note Hildon specific
333                 """
334                 try:
335                         if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
336                                 self._isFullScreen = True
337                         else:
338                                 self._isFullScreen = False
339                 except Exception:
340                         self.__errorDisplay.push_exception()
341
342         def _on_close(self, *args, **kwds):
343                 try:
344                         if self._osso is not None:
345                                 self._osso.close()
346
347                         try:
348                                 self.__save_history()
349                         finally:
350                                 gtk.main_quit()
351                 except Exception:
352                         self.__errorDisplay.push_exception()
353
354         def _on_copy(self, *args):
355                 try:
356                         equationNode = self.__history.history.peek()
357                         result = str(equationNode.evaluate())
358                         self._clipboard.set_text(result)
359                 except Exception:
360                         self.__errorDisplay.push_exception()
361
362         def _on_copy_equation(self, *args):
363                 try:
364                         equationNode = self.__history.history.peek()
365                         equation = str(equationNode)
366                         self._clipboard.set_text(equation)
367                 except Exception:
368                         self.__errorDisplay.push_exception()
369
370         def _on_paste(self, *args):
371                 try:
372                         contents = self._clipboard.wait_for_text()
373                         self.__userEntry.append(contents)
374                 except Exception:
375                         self.__errorDisplay.push_exception()
376
377         def _on_key_press(self, widget, event, *args):
378                 """
379                 @note Hildon specific
380                 """
381                 try:
382                         RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
383                         if (
384                                 event.keyval == gtk.keysyms.F6 or
385                                 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
386                         ):
387                                 if self._isFullScreen:
388                                         self._window.unfullscreen()
389                                 else:
390                                         self._window.fullscreen()
391                 except Exception:
392                         self.__errorDisplay.push_exception()
393
394         def _on_push(self, *args):
395                 try:
396                         self.__history.push_entry()
397                 except Exception:
398                         self.__errorDisplay.push_exception()
399
400         def _on_unpush(self, *args):
401                 try:
402                         self.__historyStore.unpush()
403                 except Exception:
404                         self.__errorDisplay.push_exception()
405
406         def _on_entry_direct(self, keys, modifiers):
407                 try:
408                         if "shift" in modifiers:
409                                 keys = keys.upper()
410                         self.__userEntry.append(keys)
411                 except Exception:
412                         self.__errorDisplay.push_exception()
413
414         def _on_entry_backspace(self, *args):
415                 try:
416                         self.__userEntry.pop()
417                 except Exception:
418                         self.__errorDisplay.push_exception()
419
420         def _on_entry_clear(self, *args):
421                 try:
422                         self.__userEntry.clear()
423                 except Exception:
424                         self.__errorDisplay.push_exception()
425
426         def _on_clear_all(self, *args):
427                 try:
428                         self.__history.clear()
429                 except Exception:
430                         self.__errorDisplay.push_exception()
431
432         def _on_about_activate(self, *args):
433                 dlg = gtk.AboutDialog()
434                 dlg.set_name(constants.__pretty_app_name__)
435                 dlg.set_version(constants.__version__)
436                 dlg.set_copyright("Copyright 2008 - LGPL")
437                 dlg.set_comments("""
438 ejpi A Touch Screen Optimized RPN Calculator for Maemo and Linux.
439
440 RPN: Stack based math, its fun
441 Buttons: Try both pressing and hold/drag
442 History: Try dragging things around, deleting them, etc
443 """)
444                 dlg.set_website("http://ejpi.garage.maemo.org")
445                 dlg.set_authors(["Ed Page"])
446                 dlg.run()
447                 dlg.destroy()
448
449
450 def run_doctest():
451         import doctest
452
453         failureCount, testCount = doctest.testmod()
454         if not failureCount:
455                 print "Tests Successful"
456                 sys.exit(0)
457         else:
458                 sys.exit(1)
459
460
461 def run_calculator():
462         gtk.gdk.threads_init()
463
464         gtkpie.IMAGES.add_path(os.path.join(os.path.dirname(__file__), "libraries/images"), )
465         handle = Calculator()
466         if not PROFILE_STARTUP:
467                 gtk.main()
468
469
470 class DummyOptions(object):
471
472         def __init__(self):
473                 self.test = False
474
475
476 if __name__ == "__main__":
477         logging.basicConfig(level=logging.DEBUG)
478         if len(sys.argv) > 1:
479                 try:
480                         import optparse
481                 except ImportError:
482                         optparse = None
483
484                 if optparse is not None:
485                         parser = optparse.OptionParser()
486                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
487                         (commandOptions, commandArgs) = parser.parse_args()
488         else:
489                 commandOptions = DummyOptions()
490                 commandArgs = []
491
492         if commandOptions.test:
493                 run_doctest()
494         else:
495                 run_calculator()