Minor improvements and icons added
[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._widgetTree.get_widget("functionLayout").reorder_child(self.__builtinKeyboard, 0)
207                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Trigonometry"))
208                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Computer"))
209                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Alphabet"))
210
211                 callbackMapping = {
212                         "on_calculator_quit": self._on_close,
213                         "on_paste": self._on_paste,
214                         "on_clear_history": self._on_clear_all,
215                         "on_about": self._on_about_activate,
216                 }
217                 self._widgetTree.signal_autoconnect(callbackMapping)
218                 self._widgetTree.get_widget("copyMenuItem").connect("activate", self._on_copy)
219                 self._widgetTree.get_widget("copyEquationMenuItem").connect("activate", self._on_copy_equation)
220
221                 if self.__window:
222                         if hildon is None:
223                                 self.__window.set_title("%s" % self.__pretty_app_name__)
224                         self.__window.connect("destroy", self._on_close)
225                         self.__window.show_all()
226
227                 try:
228                         import osso
229                 except ImportError:
230                         osso = None
231
232                 self._osso = None
233                 if osso is not None:
234                         self._osso = osso.Context(Calculator.__app_name__, Calculator.__version__, False)
235                         device = osso.DeviceState(self._osso)
236                         device.set_device_state_callback(self._on_device_state_change, 0)
237                 else:
238                         pass # warnings.warn("No OSSO", UserWarning, 2)
239
240         def display_error_message(self, msg):
241                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
242
243                 def close(dialog, response, editor):
244                         editor.about_dialog = None
245                         dialog.destroy()
246                 error_dialog.connect("response", close, self)
247                 error_dialog.run()
248
249         def enable_plugin(self, pluginId):
250                 self.__keyboardPlugins.enable_plugin(pluginId)
251                 pluginData = self.__keyboardPlugins.plugin_info(pluginId)
252                 pluginName = pluginData[0]
253                 plugin = self.__keyboardPlugins.keyboards[pluginName].construct_keyboard()
254                 pluginKeyboard = plugin.setup(self.__history, self.__sliceStyle, self.__handler)
255
256                 keyboardTabs = self._widgetTree.get_widget("pluginKeyboards")
257                 keyboardTabs.append_page(pluginKeyboard, gtk.Label(pluginName))
258                 keyboardPageNum = keyboardTabs.page_num(pluginKeyboard)
259                 assert keyboardPageNum not in self.__activeKeyboards
260                 self.__activeKeyboards[keyboardPageNum] = {
261                         "pluginName": pluginName,
262                         "plugin": plugin,
263                         "pluginKeyboard": pluginKeyboard,
264                 }
265
266         def __load_history(self):
267                 serialized = []
268                 try:
269                         with open(self._user_history, "rU") as f:
270                                 serialized = (
271                                         (part.strip() for part in line.split(" "))
272                                         for line in f.readlines()
273                                 )
274                 except IOError, e:
275                         if e.errno != 2:
276                                 raise
277                 self.__history.deserialize_stack(serialized)
278
279         def __save_history(self):
280                 serialized = self.__history.serialize_stack()
281                 with open(self._user_history, "w") as f:
282                         for lineData in serialized:
283                                 line = " ".join(data for data in lineData)
284                                 f.write("%s\n" % line)
285
286         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
287                 """
288                 For system_inactivity, we have no background tasks to pause
289
290                 @note Hildon specific
291                 """
292                 if memory_low:
293                         gc.collect()
294
295                 if save_unsaved_data or shutdown:
296                         self.__save_history()
297
298         def _on_window_state_change(self, widget, event, *args):
299                 """
300                 @note Hildon specific
301                 """
302                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
303                         self._isFullScreen = True
304                 else:
305                         self._isFullScreen = False
306
307         def _on_close(self, *args, **kwds):
308                 if self._osso is not None:
309                         self._osso.close()
310
311                 try:
312                         self.__save_history()
313                 finally:
314                         gtk.main_quit()
315
316         def _on_copy(self, *args):
317                 try:
318                         equationNode = self.__history.history.peek()
319                         result = str(equationNode.evaluate())
320                         self._clipboard.set_text(result)
321                 except StandardError, e:
322                         self.__errorDisplay.push_exception()
323
324         def _on_copy_equation(self, *args):
325                 try:
326                         equationNode = self.__history.history.peek()
327                         equation = str(equationNode)
328                         self._clipboard.set_text(equation)
329                 except StandardError, e:
330                         self.__errorDisplay.push_exception()
331
332         def _on_paste(self, *args):
333                 contents = self._clipboard.wait_for_text()
334                 self.__userEntry.append(contents)
335
336         def _on_key_press(self, widget, event, *args):
337                 """
338                 @note Hildon specific
339                 """
340                 if event.keyval == gtk.keysyms.F6:
341                         if self._isFullScreen:
342                                 self.__window.unfullscreen()
343                         else:
344                                 self.__window.fullscreen()
345
346         def _on_push(self, *args):
347                 self.__history.push_entry()
348
349         def _on_unpush(self, *args):
350                 self.__historyStore.unpush()
351
352         def _on_entry_direct(self, keys, modifiers):
353                 if "shift" in modifiers:
354                         keys = keys.upper()
355                 self.__userEntry.append(keys)
356
357         def _on_entry_backspace(self, *args):
358                 self.__userEntry.pop()
359
360         def _on_entry_clear(self, *args):
361                 self.__userEntry.clear()
362
363         def _on_clear_all(self, *args):
364                 self.__history.clear()
365
366         def _on_about_activate(self, *args):
367                 dlg = gtk.AboutDialog()
368                 dlg.set_name(self.__pretty_app_name__)
369                 dlg.set_version(self.__version__)
370                 dlg.set_copyright("Copyright 2008 - LGPL")
371                 dlg.set_comments("""
372 ejpi A Touch Screen Optimized RPN Calculator for Maemo and Linux.
373
374 How do I use this?
375 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.
376
377 This is RPN, where are the swap, roll, etc operations?
378 This also uses a touch screen, go ahead and feel adventerous by dragging the stack items around.
379 """)
380                 dlg.set_website("http://ejpi.garage.maemo.org")
381                 dlg.set_authors(["Ed Page"])
382                 dlg.run()
383                 dlg.destroy()
384
385
386 def run_doctest():
387         import doctest
388
389         failureCount, testCount = doctest.testmod()
390         if not failureCount:
391                 print "Tests Successful"
392                 sys.exit(0)
393         else:
394                 sys.exit(1)
395
396
397 def run_calculator():
398         gtk.gdk.threads_init()
399
400         gtkpie.IMAGES.add_path(os.path.join(os.path.dirname(__file__), "libraries/images"), )
401         if hildon is not None:
402                 gtk.set_application_name(Calculator.__pretty_app_name__)
403         handle = Calculator()
404         gtk.main()
405
406
407 class DummyOptions(object):
408
409         def __init__(self):
410                 self.test = False
411
412
413 if __name__ == "__main__":
414         if len(sys.argv) > 1:
415                 try:
416                         import optparse
417                 except ImportError:
418                         optparse = None
419
420                 if optparse is not None:
421                         parser = optparse.OptionParser()
422                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
423                         (commandOptions, commandArgs) = parser.parse_args()
424         else:
425                 commandOptions = DummyOptions()
426                 commandArgs = []
427
428         if commandOptions.test:
429                 run_doctest()
430         else:
431                 run_calculator()