Caching even more stuff to try and speed up popups
[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 warnings
30
31 import gtk
32 import gtk.glade
33
34 try:
35         import hildon
36 except ImportError:
37         hildon = None
38
39 from libraries import gtkpie
40 from libraries import gtkpieboard
41 import plugin_utils
42 import history
43 import gtkhistory
44
45
46 PLUGIN_SEARCH_PATHS = [
47         os.path.join(os.path.dirname(__file__), "plugins/"),
48 ]
49
50
51 class ValueEntry(object):
52
53         def __init__(self, widget):
54                 self.__widget = widget
55                 self.__actualEntryDisplay = ""
56
57         def get_value(self):
58                 value = self.__actualEntryDisplay.strip()
59                 if any(
60                         0 < value.find(whitespace)
61                         for whitespace in string.whitespace
62                 ):
63                         self.clear()
64                         raise ValueError('Invalid input "%s"' % value)
65                 return value
66
67         def set_value(self, value):
68                 value = value.strip()
69                 if any(
70                         0 < value.find(whitespace)
71                         for whitespace in string.whitespace
72                 ):
73                         raise ValueError('Invalid input "%s"' % value)
74                 self.__actualEntryDisplay = value
75                 self.__widget.set_text(value)
76
77         def append(self, value):
78                 value = value.strip()
79                 if any(
80                         0 < value.find(whitespace)
81                         for whitespace in string.whitespace
82                 ):
83                         raise ValueError('Invalid input "%s"' % value)
84                 self.set_value(self.get_value() + value)
85
86         def pop(self):
87                 value = self.get_value()[0:-1]
88                 self.set_value(value)
89
90         def clear(self):
91                 self.set_value("")
92
93         value = property(get_value, set_value, clear)
94
95
96 class ErrorDisplay(history.ErrorReporting):
97
98         def __init__(self, widgetTree):
99                 super(ErrorDisplay, self).__init__()
100                 self.__errorBox = widgetTree.get_widget("errorEventBox")
101                 self.__errorDescription = widgetTree.get_widget("errorDescription")
102                 self.__errorClose = widgetTree.get_widget("errorClose")
103                 self.__parentBox = self.__errorBox.get_parent()
104
105                 self.__errorBox.connect("button_release_event", self._on_close)
106
107                 self.__messages = []
108                 self.__parentBox.remove(self.__errorBox)
109
110         def push_message(self, message):
111                 if 0 < len(self.__messages):
112                         self.__messages.append(message)
113                 else:
114                         self.__show_message(message)
115
116         def pop_message(self):
117                 if 0 < len(self.__messages):
118                         self.__show_message(self.__messages[0])
119                         del self.__messages[0]
120                 else:
121                         self.__hide_message()
122
123         def _on_close(self, *args):
124                 self.pop_message()
125
126         def __show_message(self, message):
127                 self.__errorDescription.set_text(message)
128                 self.__parentBox.pack_start(self.__errorBox, False, False)
129                 self.__parentBox.reorder_child(self.__errorBox, 1)
130
131         def __hide_message(self):
132                 self.__errorDescription.set_text("")
133                 self.__parentBox.remove(self.__errorBox)
134
135
136 class Calculator(object):
137
138         __pretty_app_name__ = "e^(j pi) + 1 = 0"
139         __app_name__ = "ejpi"
140         __version__ = "0.9.0"
141         __app_magic__ = 0xdeadbeef
142
143         _glade_files = [
144                 '/usr/lib/ejpi/ejpi.glade',
145                 os.path.join(os.path.dirname(__file__), "ejpi.glade"),
146                 os.path.join(os.path.dirname(__file__), "../lib/ejpi.glade"),
147         ]
148
149         _plugin_search_paths = [
150                 "/usr/lib/ejpi/plugins/",
151                 os.path.join(os.path.dirname(__file__), "plugins/"),
152         ]
153
154         _user_data = os.path.expanduser("~/.%s/" % __app_name__)
155         _user_settings = "%s/settings.ini" % _user_data
156         _user_history = "%s/history.stack" % _user_data
157
158         def __init__(self):
159                 self.__constantPlugins = plugin_utils.ConstantPluginManager()
160                 self.__constantPlugins.add_path(*self._plugin_search_paths)
161                 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
162                         try:
163                                 pluginId = self.__constantPlugins.lookup_plugin(pluginName)
164                                 self.__constantPlugins.enable_plugin(pluginId)
165                         except:
166                                 warnings.warn("Failed to load plugin %s" % pluginName)
167
168                 self.__operatorPlugins = plugin_utils.OperatorPluginManager()
169                 self.__operatorPlugins.add_path(*self._plugin_search_paths)
170                 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
171                         try:
172                                 pluginId = self.__operatorPlugins.lookup_plugin(pluginName)
173                                 self.__operatorPlugins.enable_plugin(pluginId)
174                         except:
175                                 warnings.warn("Failed to load plugin %s" % pluginName)
176
177                 self.__keyboardPlugins = plugin_utils.KeyboardPluginManager()
178                 self.__keyboardPlugins.add_path(*self._plugin_search_paths)
179                 self.__activeKeyboards = {}
180
181                 for path in self._glade_files:
182                         if os.path.isfile(path):
183                                 self._widgetTree = gtk.glade.XML(path)
184                                 break
185                 else:
186                         self.display_error_message("Cannot find ejpi.glade")
187                         gtk.main_quit()
188                 try:
189                         os.makedirs(self._user_data)
190                 except OSError, e:
191                         if e.errno != 17:
192                                 raise
193
194                 self._clipboard = gtk.clipboard_get()
195                 self.__window = self._widgetTree.get_widget("mainWindow")
196
197                 global hildon
198                 self._app = None
199                 self._isFullScreen = False
200                 if hildon is not None and self.__window is gtk.Window:
201                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
202                         hildon = None
203                 elif hildon is not None:
204                         self._app = hildon.Program()
205                         self.__window = hildon.Window()
206                         self._widgetTree.get_widget("mainLayout").reparent(self.__window)
207                         self._app.add_window(self.__window)
208                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('scrollingHistory'), True)
209
210                         gtkMenu = self._widgetTree.get_widget("mainMenubar")
211                         menu = gtk.Menu()
212                         for child in gtkMenu.get_children():
213                                 child.reparent(menu)
214                         self.__window.set_menu(menu)
215                         gtkMenu.destroy()
216
217                         self.__window.connect("key-press-event", self._on_key_press)
218                         self.__window.connect("window-state-event", self._on_window_state_change)
219                 else:
220                         warnings.warn("No Hildon", UserWarning, 2)
221
222                 self.__errorDisplay = ErrorDisplay(self._widgetTree)
223                 self.__userEntry = ValueEntry(self._widgetTree.get_widget("entryView"))
224                 self.__stackView = self._widgetTree.get_widget("historyView")
225
226                 self.__historyStore = gtkhistory.GtkCalcHistory(self.__stackView)
227                 self.__history = history.RpnCalcHistory(
228                         self.__historyStore,
229                         self.__userEntry, self.__errorDisplay,
230                         self.__constantPlugins.constants, self.__operatorPlugins.operators
231                 )
232                 self.__load_history()
233
234                 self.__sliceStyle = gtkpie.generate_pie_style(self.__window)
235                 self.__handler = gtkpieboard.KeyboardHandler(self._on_entry_direct)
236                 self.__handler.register_command_handler("push", self._on_push)
237                 self.__handler.register_command_handler("unpush", self._on_unpush)
238                 self.__handler.register_command_handler("backspace", self._on_entry_backspace)
239                 self.__handler.register_command_handler("clear", self._on_entry_clear)
240
241                 builtinKeyboardId = self.__keyboardPlugins.lookup_plugin("Builtin")
242                 self.__keyboardPlugins.enable_plugin(builtinKeyboardId)
243                 self.__builtinPlugin = self.__keyboardPlugins.keyboards["Builtin"].construct_keyboard()
244                 self.__builtinKeyboard = self.__builtinPlugin.setup(self.__history, self.__sliceStyle, self.__handler)
245                 self._widgetTree.get_widget("functionLayout").pack_start(self.__builtinKeyboard)
246                 self._widgetTree.get_widget("functionLayout").reorder_child(self.__builtinKeyboard, 0)
247                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Trigonometry"))
248                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Computer"))
249                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Alphabet"))
250
251                 callbackMapping = {
252                         "on_calculator_quit": self._on_close,
253                         "on_paste": self._on_paste,
254                         "on_clear_history": self._on_clear_all,
255                         "on_about": self._on_about_activate,
256                 }
257                 self._widgetTree.signal_autoconnect(callbackMapping)
258
259                 if self.__window:
260                         if hildon is None:
261                                 self.__window.set_title("%s" % self.__pretty_app_name__)
262                         self.__window.connect("destroy", self._on_close)
263                         self.__window.show_all()
264
265                 try:
266                         import osso
267                 except ImportError:
268                         osso = None
269
270                 self._osso = None
271                 if osso is not None:
272                         self._osso = osso.Context(Calculator.__app_name__, Calculator.__version__, False)
273                         device = osso.DeviceState(self._osso)
274                         device.set_device_state_callback(self._on_device_state_change, 0)
275                 else:
276                         warnings.warn("No OSSO", UserWarning, 2)
277
278         def display_error_message(self, msg):
279                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
280
281                 def close(dialog, response, editor):
282                         editor.about_dialog = None
283                         dialog.destroy()
284                 error_dialog.connect("response", close, self)
285                 error_dialog.run()
286
287         def enable_plugin(self, pluginId):
288                 self.__keyboardPlugins.enable_plugin(pluginId)
289                 pluginData = self.__keyboardPlugins.plugin_info(pluginId)
290                 pluginName = pluginData[0]
291                 plugin = self.__keyboardPlugins.keyboards[pluginName].construct_keyboard()
292                 pluginKeyboard = plugin.setup(self.__history, self.__sliceStyle, self.__handler)
293
294                 keyboardTabs = self._widgetTree.get_widget("pluginKeyboards")
295                 keyboardTabs.append_page(pluginKeyboard, gtk.Label(pluginName))
296                 keyboardPageNum = keyboardTabs.page_num(pluginKeyboard)
297                 assert keyboardPageNum not in self.__activeKeyboards
298                 self.__activeKeyboards[keyboardPageNum] = {
299                         "pluginName": pluginName,
300                         "plugin": plugin,
301                         "pluginKeyboard": pluginKeyboard,
302                 }
303
304         def __load_history(self):
305                 serialized = []
306                 try:
307                         with open(self._user_history, "rU") as f:
308                                 serialized = (
309                                         (part.strip() for part in line.split(" "))
310                                         for line in f.readlines()
311                                 )
312                 except IOError, e:
313                         if e.errno != 2:
314                                 raise
315                 self.__history.deserialize_stack(serialized)
316
317         def __save_history(self):
318                 serialized = self.__history.serialize_stack()
319                 with open(self._user_history, "w") as f:
320                         for lineData in serialized:
321                                 line = " ".join(data for data in lineData)
322                                 f.write("%s\n" % line)
323
324         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
325                 """
326                 For system_inactivity, we have no background tasks to pause
327
328                 @note Hildon specific
329                 """
330                 if memory_low:
331                         gc.collect()
332
333                 if save_unsaved_data or shutdown:
334                         self.__save_history()
335
336         def _on_window_state_change(self, widget, event, *args):
337                 """
338                 @note Hildon specific
339                 """
340                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
341                         self._isFullScreen = True
342                 else:
343                         self._isFullScreen = False
344
345         def _on_close(self, *args, **kwds):
346                 try:
347                         self.__save_history()
348                 finally:
349                         gtk.main_quit()
350
351         def _on_paste(self, *args):
352                 contents = self._clipboard.wait_for_text()
353                 self.__userEntry.append(contents)
354
355         def _on_key_press(self, widget, event, *args):
356                 """
357                 @note Hildon specific
358                 """
359                 if event.keyval == gtk.keysyms.F6:
360                         if self._isFullScreen:
361                                 self.__window.unfullscreen()
362                         else:
363                                 self.__window.fullscreen()
364
365         def _on_push(self, *args):
366                 self.__history.push_entry()
367
368         def _on_unpush(self, *args):
369                 self.__historyStore.unpush()
370
371         def _on_entry_direct(self, keys, modifiers):
372                 if "shift" in modifiers:
373                         keys = keys.upper()
374                 self.__userEntry.append(keys)
375
376         def _on_entry_backspace(self, *args):
377                 self.__userEntry.pop()
378
379         def _on_entry_clear(self, *args):
380                 self.__userEntry.clear()
381
382         def _on_clear_all(self, *args):
383                 self.__history.clear()
384
385         def _on_about_activate(self, *args):
386                 dlg = gtk.AboutDialog()
387                 dlg.set_name(self.__pretty_app_name__)
388                 dlg.set_version(self.__version__)
389                 dlg.set_copyright("Copyright 2008 - LGPL")
390                 dlg.set_comments("")
391                 dlg.set_website("")
392                 dlg.set_authors([""])
393                 dlg.run()
394                 dlg.destroy()
395
396
397 def run_doctest():
398         import doctest
399
400         failureCount, testCount = doctest.testmod()
401         if not failureCount:
402                 print "Tests Successful"
403                 sys.exit(0)
404         else:
405                 sys.exit(1)
406
407
408 def run_calculator():
409         gtk.gdk.threads_init()
410
411         gtkpie.IMAGES.add_path(os.path.join(os.path.dirname(__file__), "libraries/images"), )
412         if hildon is not None:
413                 gtk.set_application_name(Calculator.__pretty_app_name__)
414         handle = Calculator()
415         gtk.main()
416
417
418 class DummyOptions(object):
419
420         def __init__(self):
421                 self.test = False
422
423
424 if __name__ == "__main__":
425         if len(sys.argv) > 1:
426                 try:
427                         import optparse
428                 except ImportError:
429                         optparse = None
430
431                 if optparse is not None:
432                         parser = optparse.OptionParser()
433                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
434                         (commandOptions, commandArgs) = parser.parse_args()
435         else:
436                 commandOptions = DummyOptions()
437                 commandArgs = []
438
439         if commandOptions.test:
440                 run_doctest()
441         else:
442                 run_calculator()