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