afb2175aa25694a17c5053e22526711209361ca0
[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 @bug Has the same Maemo tab color bug as DialCentral
16
17 Some useful things on Maemo
18 @li http://maemo.org/api_refs/4.1/libosso-2.16-1/group__Statesave.html
19 @li http://maemo.org/api_refs/4.1/libosso-2.16-1/group__Autosave.html
20 """
21
22
23 from __future__ import with_statement
24
25
26 import sys
27 import gc
28 import os
29 import string
30 import warnings
31
32 import gtk
33 import gtk.glade
34
35 try:
36         import hildon
37 except ImportError:
38         hildon = None
39
40 from libraries import gtkpie
41 from libraries import gtkpieboard
42 import plugin_utils
43 import history
44 import gtkhistory
45 import gtk_toolbox
46
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         __pretty_app_name__ = "e**(j pi) + 1 = 0"
101         __app_name__ = "ejpi"
102         __version__ = "0.9.4"
103         __app_magic__ = 0xdeadbeef
104
105         _glade_files = [
106                 '/usr/lib/ejpi/ejpi.glade',
107                 os.path.join(os.path.dirname(__file__), "ejpi.glade"),
108                 os.path.join(os.path.dirname(__file__), "../lib/ejpi.glade"),
109         ]
110
111         _plugin_search_paths = [
112                 "/usr/lib/ejpi/plugins/",
113                 os.path.join(os.path.dirname(__file__), "plugins/"),
114         ]
115
116         _user_data = os.path.expanduser("~/.%s/" % __app_name__)
117         _user_settings = "%s/settings.ini" % _user_data
118         _user_history = "%s/history.stack" % _user_data
119
120         def __init__(self):
121                 self.__constantPlugins = plugin_utils.ConstantPluginManager()
122                 self.__constantPlugins.add_path(*self._plugin_search_paths)
123                 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
124                         try:
125                                 pluginId = self.__constantPlugins.lookup_plugin(pluginName)
126                                 self.__constantPlugins.enable_plugin(pluginId)
127                         except:
128                                 warnings.warn("Failed to load plugin %s" % pluginName)
129
130                 self.__operatorPlugins = plugin_utils.OperatorPluginManager()
131                 self.__operatorPlugins.add_path(*self._plugin_search_paths)
132                 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
133                         try:
134                                 pluginId = self.__operatorPlugins.lookup_plugin(pluginName)
135                                 self.__operatorPlugins.enable_plugin(pluginId)
136                         except:
137                                 warnings.warn("Failed to load plugin %s" % pluginName)
138
139                 self.__keyboardPlugins = plugin_utils.KeyboardPluginManager()
140                 self.__keyboardPlugins.add_path(*self._plugin_search_paths)
141                 self.__activeKeyboards = {}
142
143                 for path in self._glade_files:
144                         if os.path.isfile(path):
145                                 self._widgetTree = gtk.glade.XML(path)
146                                 break
147                 else:
148                         self.display_error_message("Cannot find ejpi.glade")
149                         gtk.main_quit()
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                 global hildon
160                 self._app = None
161                 self._isFullScreen = False
162                 if hildon is not None:
163                         self._app = hildon.Program()
164                         oldWindow = self._window
165                         self.__window = hildon.Window()
166                         oldWindow.get_child().reparent(self.__window)
167                         self._app.add_window(self.__window)
168                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('scrollingHistory'), True)
169
170                         gtkMenu = self._widgetTree.get_widget("mainMenubar")
171                         menu = gtk.Menu()
172                         for child in gtkMenu.get_children():
173                                 child.reparent(menu)
174                         self.__window.set_menu(menu)
175                         gtkMenu.destroy()
176
177                         self.__window.connect("key-press-event", self._on_key_press)
178                         self.__window.connect("window-state-event", self._on_window_state_change)
179                 else:
180                         pass # warnings.warn("No Hildon", UserWarning, 2)
181
182                 self.__errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
183                 self.__userEntry = ValueEntry(self._widgetTree.get_widget("entryView"))
184                 self.__stackView = self._widgetTree.get_widget("historyView")
185
186                 self.__historyStore = gtkhistory.GtkCalcHistory(self.__stackView)
187                 self.__history = history.RpnCalcHistory(
188                         self.__historyStore,
189                         self.__userEntry, self.__errorDisplay,
190                         self.__constantPlugins.constants, self.__operatorPlugins.operators
191                 )
192                 self.__load_history()
193
194                 self.__sliceStyle = gtkpie.generate_pie_style(gtk.Button())
195                 self.__handler = gtkpieboard.KeyboardHandler(self._on_entry_direct)
196                 self.__handler.register_command_handler("push", self._on_push)
197                 self.__handler.register_command_handler("unpush", self._on_unpush)
198                 self.__handler.register_command_handler("backspace", self._on_entry_backspace)
199                 self.__handler.register_command_handler("clear", self._on_entry_clear)
200
201                 builtinKeyboardId = self.__keyboardPlugins.lookup_plugin("Builtin")
202                 self.__keyboardPlugins.enable_plugin(builtinKeyboardId)
203                 self.__builtinPlugin = self.__keyboardPlugins.keyboards["Builtin"].construct_keyboard()
204                 self.__builtinKeyboard = self.__builtinPlugin.setup(self.__history, self.__sliceStyle, self.__handler)
205                 self._widgetTree.get_widget("functionLayout").pack_start(self.__builtinKeyboard)
206                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Trigonometry"))
207                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Computer"))
208                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Alphabet"))
209
210                 callbackMapping = {
211                         "on_calculator_quit": self._on_close,
212                         "on_paste": self._on_paste,
213                         "on_clear_history": self._on_clear_all,
214                         "on_about": self._on_about_activate,
215                 }
216                 self._widgetTree.signal_autoconnect(callbackMapping)
217                 self._widgetTree.get_widget("copyMenuItem").connect("activate", self._on_copy)
218                 self._widgetTree.get_widget("copyEquationMenuItem").connect("activate", self._on_copy_equation)
219
220                 if self.__window:
221                         if hildon is None:
222                                 self.__window.set_title("%s" % self.__pretty_app_name__)
223                         self.__window.connect("destroy", self._on_close)
224                         self.__window.show_all()
225
226                 try:
227                         import osso
228                 except ImportError:
229                         osso = None
230
231                 self._osso = None
232                 if osso is not None:
233                         self._osso = osso.Context(Calculator.__app_name__, Calculator.__version__, False)
234                         device = osso.DeviceState(self._osso)
235                         device.set_device_state_callback(self._on_device_state_change, 0)
236                 else:
237                         pass # warnings.warn("No OSSO", UserWarning, 2)
238
239         def display_error_message(self, msg):
240                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
241
242                 def close(dialog, response, editor):
243                         editor.about_dialog = None
244                         dialog.destroy()
245                 error_dialog.connect("response", close, self)
246                 error_dialog.run()
247
248         def enable_plugin(self, pluginId):
249                 self.__keyboardPlugins.enable_plugin(pluginId)
250                 pluginData = self.__keyboardPlugins.plugin_info(pluginId)
251                 pluginName = pluginData[0]
252                 plugin = self.__keyboardPlugins.keyboards[pluginName].construct_keyboard()
253                 pluginKeyboard = plugin.setup(self.__history, self.__sliceStyle, self.__handler)
254
255                 keyboardTabs = self._widgetTree.get_widget("pluginKeyboards")
256                 keyboardTabs.append_page(pluginKeyboard, gtk.Label(pluginName))
257                 keyboardPageNum = keyboardTabs.page_num(pluginKeyboard)
258                 assert keyboardPageNum not in self.__activeKeyboards
259                 self.__activeKeyboards[keyboardPageNum] = {
260                         "pluginName": pluginName,
261                         "plugin": plugin,
262                         "pluginKeyboard": pluginKeyboard,
263                 }
264
265         def __load_history(self):
266                 serialized = []
267                 try:
268                         with open(self._user_history, "rU") as f:
269                                 serialized = (
270                                         (part.strip() for part in line.split(" "))
271                                         for line in f.readlines()
272                                 )
273                 except IOError, e:
274                         if e.errno != 2:
275                                 raise
276                 self.__history.deserialize_stack(serialized)
277
278         def __save_history(self):
279                 serialized = self.__history.serialize_stack()
280                 with open(self._user_history, "w") as f:
281                         for lineData in serialized:
282                                 line = " ".join(data for data in lineData)
283                                 f.write("%s\n" % line)
284
285         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
286                 """
287                 For system_inactivity, we have no background tasks to pause
288
289                 @note Hildon specific
290                 """
291                 if memory_low:
292                         gc.collect()
293
294                 if save_unsaved_data or shutdown:
295                         self.__save_history()
296
297         def _on_window_state_change(self, widget, event, *args):
298                 """
299                 @note Hildon specific
300                 """
301                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
302                         self._isFullScreen = True
303                 else:
304                         self._isFullScreen = False
305
306         def _on_close(self, *args, **kwds):
307                 if self._osso is not None:
308                         self._osso.close()
309
310                 try:
311                         self.__save_history()
312                 finally:
313                         gtk.main_quit()
314
315         def _on_copy(self, *args):
316                 try:
317                         equationNode = self.__history.history.peek()
318                         result = str(equationNode.evaluate())
319                         self._clipboard.set_text(result)
320                 except StandardError, e:
321                         self.__errorDisplay.push_exception()
322
323         def _on_copy_equation(self, *args):
324                 try:
325                         equationNode = self.__history.history.peek()
326                         equation = str(equationNode)
327                         self._clipboard.set_text(equation)
328                 except StandardError, e:
329                         self.__errorDisplay.push_exception()
330
331         def _on_paste(self, *args):
332                 contents = self._clipboard.wait_for_text()
333                 self.__userEntry.append(contents)
334
335         def _on_key_press(self, widget, event, *args):
336                 """
337                 @note Hildon specific
338                 """
339                 if event.keyval == gtk.keysyms.F6:
340                         if self._isFullScreen:
341                                 self.__window.unfullscreen()
342                         else:
343                                 self.__window.fullscreen()
344
345         def _on_push(self, *args):
346                 self.__history.push_entry()
347
348         def _on_unpush(self, *args):
349                 self.__historyStore.unpush()
350
351         def _on_entry_direct(self, keys, modifiers):
352                 if "shift" in modifiers:
353                         keys = keys.upper()
354                 self.__userEntry.append(keys)
355
356         def _on_entry_backspace(self, *args):
357                 self.__userEntry.pop()
358
359         def _on_entry_clear(self, *args):
360                 self.__userEntry.clear()
361
362         def _on_clear_all(self, *args):
363                 self.__history.clear()
364
365         def _on_about_activate(self, *args):
366                 dlg = gtk.AboutDialog()
367                 dlg.set_name(self.__pretty_app_name__)
368                 dlg.set_version(self.__version__)
369                 dlg.set_copyright("Copyright 2008 - LGPL")
370                 dlg.set_comments("""
371 ejpi A Touch Screen Optimized RPN Calculator for Maemo and Linux.
372
373 How do I use this?
374 The buttons are all pie-menus.  Clicking on them will give you the default (center) behavior.  If you click and hold, the menu gets displayed showing what other actions you can then perform.  While still holding, just drag in the direction of one of these actions.
375
376 This is RPN, where are the swap, roll, etc operations?
377 This also uses a touch screen, go ahead and feel adventerous by dragging the stack items around.
378 """)
379                 dlg.set_website("http://ejpi.garage.maemo.org")
380                 dlg.set_authors(["Ed Page"])
381                 dlg.run()
382                 dlg.destroy()
383
384
385 def run_doctest():
386         import doctest
387
388         failureCount, testCount = doctest.testmod()
389         if not failureCount:
390                 print "Tests Successful"
391                 sys.exit(0)
392         else:
393                 sys.exit(1)
394
395
396 def run_calculator():
397         gtk.gdk.threads_init()
398
399         gtkpie.IMAGES.add_path(os.path.join(os.path.dirname(__file__), "libraries/images"), )
400         if hildon is not None:
401                 gtk.set_application_name(Calculator.__pretty_app_name__)
402         handle = Calculator()
403         gtk.main()
404
405
406 class DummyOptions(object):
407
408         def __init__(self):
409                 self.test = False
410
411
412 if __name__ == "__main__":
413         if len(sys.argv) > 1:
414                 try:
415                         import optparse
416                 except ImportError:
417                         optparse = None
418
419                 if optparse is not None:
420                         parser = optparse.OptionParser()
421                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
422                         (commandOptions, commandArgs) = parser.parse_args()
423         else:
424                 commandOptions = DummyOptions()
425                 commandArgs = []
426
427         if commandOptions.test:
428                 run_doctest()
429         else:
430                 run_calculator()