bef1527b6cbb5145338a176684087c58bbff494a
[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 logging
31 import warnings
32
33 import gtk
34 import gtk.glade
35
36 try:
37         import hildon
38 except ImportError:
39         hildon = None
40
41 from libraries import gtkpie
42 from libraries import gtkpieboard
43 import plugin_utils
44 import history
45 import gtkhistory
46 import gtk_toolbox
47 import constants
48
49
50 _moduleLogger = logging.getLogger("ejpi_glade")
51
52 PLUGIN_SEARCH_PATHS = [
53         os.path.join(os.path.dirname(__file__), "plugins/"),
54 ]
55
56
57 class ValueEntry(object):
58
59         def __init__(self, widget):
60                 self.__widget = widget
61                 self.__actualEntryDisplay = ""
62
63         def get_value(self):
64                 value = self.__actualEntryDisplay.strip()
65                 if any(
66                         0 < value.find(whitespace)
67                         for whitespace in string.whitespace
68                 ):
69                         self.clear()
70                         raise ValueError('Invalid input "%s"' % value)
71                 return value
72
73         def set_value(self, value):
74                 value = value.strip()
75                 if any(
76                         0 < value.find(whitespace)
77                         for whitespace in string.whitespace
78                 ):
79                         raise ValueError('Invalid input "%s"' % value)
80                 self.__actualEntryDisplay = value
81                 self.__widget.set_text(value)
82
83         def append(self, value):
84                 value = value.strip()
85                 if any(
86                         0 < value.find(whitespace)
87                         for whitespace in string.whitespace
88                 ):
89                         raise ValueError('Invalid input "%s"' % value)
90                 self.set_value(self.get_value() + value)
91
92         def pop(self):
93                 value = self.get_value()[0:-1]
94                 self.set_value(value)
95
96         def clear(self):
97                 self.set_value("")
98
99         value = property(get_value, set_value, clear)
100
101
102 class Calculator(object):
103
104         _glade_files = [
105                 '/usr/lib/ejpi/ejpi.glade',
106                 os.path.join(os.path.dirname(__file__), "ejpi.glade"),
107                 os.path.join(os.path.dirname(__file__), "../lib/ejpi.glade"),
108         ]
109
110         _plugin_search_paths = [
111                 "/usr/lib/ejpi/plugins/",
112                 os.path.join(os.path.dirname(__file__), "plugins/"),
113         ]
114
115         _user_data = os.path.expanduser("~/.%s/" % constants.__app_name__)
116         _user_settings = "%s/settings.ini" % _user_data
117         _user_history = "%s/history.stack" % _user_data
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                 if hildon is not None:
162                         self._app = hildon.Program()
163                         oldWindow = self._window
164                         self._window = hildon.Window()
165                         oldWindow.get_child().reparent(self._window)
166                         self._app.add_window(self._window)
167                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('scrollingHistory'), True)
168
169                         gtkMenu = self._widgetTree.get_widget("mainMenubar")
170                         menu = gtk.Menu()
171                         for child in gtkMenu.get_children():
172                                 child.reparent(menu)
173                         self._window.set_menu(menu)
174                         gtkMenu.destroy()
175
176                         self._window.connect("key-press-event", self._on_key_press)
177                         self._window.connect("window-state-event", self._on_window_state_change)
178                 else:
179                         pass # warnings.warn("No Hildon", UserWarning, 2)
180
181                 self.__errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
182                 self.__userEntry = ValueEntry(self._widgetTree.get_widget("entryView"))
183                 self.__stackView = self._widgetTree.get_widget("historyView")
184
185                 self.__historyStore = gtkhistory.GtkCalcHistory(self.__stackView)
186                 self.__history = history.RpnCalcHistory(
187                         self.__historyStore,
188                         self.__userEntry, self.__errorDisplay,
189                         self.__constantPlugins.constants, self.__operatorPlugins.operators
190                 )
191                 self.__load_history()
192
193                 self.__sliceStyle = gtkpie.generate_pie_style(gtk.Button())
194                 self.__handler = gtkpieboard.KeyboardHandler(self._on_entry_direct)
195                 self.__handler.register_command_handler("push", self._on_push)
196                 self.__handler.register_command_handler("unpush", self._on_unpush)
197                 self.__handler.register_command_handler("backspace", self._on_entry_backspace)
198                 self.__handler.register_command_handler("clear", self._on_entry_clear)
199
200                 builtinKeyboardId = self.__keyboardPlugins.lookup_plugin("Builtin")
201                 self.__keyboardPlugins.enable_plugin(builtinKeyboardId)
202                 self.__builtinPlugin = self.__keyboardPlugins.keyboards["Builtin"].construct_keyboard()
203                 self.__builtinKeyboard = self.__builtinPlugin.setup(self.__history, self.__sliceStyle, self.__handler)
204                 self._widgetTree.get_widget("functionLayout").pack_start(self.__builtinKeyboard)
205                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Trigonometry"))
206                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Computer"))
207                 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Alphabet"))
208
209                 callbackMapping = {
210                         "on_calculator_quit": self._on_close,
211                         "on_paste": self._on_paste,
212                         "on_clear_history": self._on_clear_all,
213                         "on_about": self._on_about_activate,
214                 }
215                 self._widgetTree.signal_autoconnect(callbackMapping)
216                 self._widgetTree.get_widget("copyMenuItem").connect("activate", self._on_copy)
217                 self._widgetTree.get_widget("copyEquationMenuItem").connect("activate", self._on_copy_equation)
218
219                 if hildon is None:
220                         self._window.set_title("%s" % constants.__pretty_app_name__)
221                 self._window.connect("destroy", self._on_close)
222                 self._window.show_all()
223
224                 try:
225                         import osso
226                 except ImportError:
227                         osso = None
228                 self._osso = None
229                 if osso is not None:
230                         self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
231                         device = osso.DeviceState(self._osso)
232                         device.set_device_state_callback(self._on_device_state_change, 0)
233                 else:
234                         pass # warnings.warn("No OSSO", UserWarning, 2)
235
236         def display_error_message(self, msg):
237                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
238
239                 def close(dialog, response, editor):
240                         editor.about_dialog = None
241                         dialog.destroy()
242                 error_dialog.connect("response", close, self)
243                 error_dialog.run()
244
245         def enable_plugin(self, pluginId):
246                 self.__keyboardPlugins.enable_plugin(pluginId)
247                 pluginData = self.__keyboardPlugins.plugin_info(pluginId)
248                 pluginName = pluginData[0]
249                 plugin = self.__keyboardPlugins.keyboards[pluginName].construct_keyboard()
250                 pluginKeyboard = plugin.setup(self.__history, self.__sliceStyle, self.__handler)
251
252                 keyboardTabs = self._widgetTree.get_widget("pluginKeyboards")
253                 keyboardTabs.append_page(pluginKeyboard, gtk.Label(pluginName))
254                 keyboardPageNum = keyboardTabs.page_num(pluginKeyboard)
255                 assert keyboardPageNum not in self.__activeKeyboards
256                 self.__activeKeyboards[keyboardPageNum] = {
257                         "pluginName": pluginName,
258                         "plugin": plugin,
259                         "pluginKeyboard": pluginKeyboard,
260                 }
261
262         def __load_history(self):
263                 serialized = []
264                 try:
265                         with open(self._user_history, "rU") as f:
266                                 serialized = (
267                                         (part.strip() for part in line.split(" "))
268                                         for line in f.readlines()
269                                 )
270                 except IOError, e:
271                         if e.errno != 2:
272                                 raise
273                 self.__history.deserialize_stack(serialized)
274
275         def __save_history(self):
276                 serialized = self.__history.serialize_stack()
277                 with open(self._user_history, "w") as f:
278                         for lineData in serialized:
279                                 line = " ".join(data for data in lineData)
280                                 f.write("%s\n" % line)
281
282         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
283                 """
284                 For system_inactivity, we have no background tasks to pause
285
286                 @note Hildon specific
287                 """
288                 if memory_low:
289                         gc.collect()
290
291                 if save_unsaved_data or shutdown:
292                         self.__save_history()
293
294         def _on_window_state_change(self, widget, event, *args):
295                 """
296                 @note Hildon specific
297                 """
298                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
299                         self._isFullScreen = True
300                 else:
301                         self._isFullScreen = False
302
303         def _on_close(self, *args, **kwds):
304                 if self._osso is not None:
305                         self._osso.close()
306
307                 try:
308                         self.__save_history()
309                 finally:
310                         gtk.main_quit()
311
312         def _on_copy(self, *args):
313                 try:
314                         equationNode = self.__history.history.peek()
315                         result = str(equationNode.evaluate())
316                         self._clipboard.set_text(result)
317                 except StandardError, e:
318                         self.__errorDisplay.push_exception()
319
320         def _on_copy_equation(self, *args):
321                 try:
322                         equationNode = self.__history.history.peek()
323                         equation = str(equationNode)
324                         self._clipboard.set_text(equation)
325                 except StandardError, e:
326                         self.__errorDisplay.push_exception()
327
328         def _on_paste(self, *args):
329                 contents = self._clipboard.wait_for_text()
330                 self.__userEntry.append(contents)
331
332         def _on_key_press(self, widget, event, *args):
333                 """
334                 @note Hildon specific
335                 """
336                 if event.keyval == gtk.keysyms.F6:
337                         if self._isFullScreen:
338                                 self._window.unfullscreen()
339                         else:
340                                 self._window.fullscreen()
341
342         def _on_push(self, *args):
343                 self.__history.push_entry()
344
345         def _on_unpush(self, *args):
346                 self.__historyStore.unpush()
347
348         def _on_entry_direct(self, keys, modifiers):
349                 if "shift" in modifiers:
350                         keys = keys.upper()
351                 self.__userEntry.append(keys)
352
353         def _on_entry_backspace(self, *args):
354                 self.__userEntry.pop()
355
356         def _on_entry_clear(self, *args):
357                 self.__userEntry.clear()
358
359         def _on_clear_all(self, *args):
360                 self.__history.clear()
361
362         def _on_about_activate(self, *args):
363                 dlg = gtk.AboutDialog()
364                 dlg.set_name(constants.__pretty_app_name__)
365                 dlg.set_version(constants.__version__)
366                 dlg.set_copyright("Copyright 2008 - LGPL")
367                 dlg.set_comments("""
368 ejpi A Touch Screen Optimized RPN Calculator for Maemo and Linux.
369
370 RPN: Stack based math, its fun
371 Buttons: Try both pressing and hold/drag
372 History: Try dragging things around, deleting them, etc
373 """)
374                 dlg.set_website("http://ejpi.garage.maemo.org")
375                 dlg.set_authors(["Ed Page"])
376                 dlg.run()
377                 dlg.destroy()
378
379
380 def run_doctest():
381         import doctest
382
383         failureCount, testCount = doctest.testmod()
384         if not failureCount:
385                 print "Tests Successful"
386                 sys.exit(0)
387         else:
388                 sys.exit(1)
389
390
391 def run_calculator():
392         gtk.gdk.threads_init()
393
394         gtkpie.IMAGES.add_path(os.path.join(os.path.dirname(__file__), "libraries/images"), )
395         if hildon is not None:
396                 gtk.set_application_name(constants.__pretty_app_name__)
397         handle = Calculator()
398         gtk.main()
399
400
401 class DummyOptions(object):
402
403         def __init__(self):
404                 self.test = False
405
406
407 if __name__ == "__main__":
408         logging.basicConfig(level=logging.DEBUG)
409         if len(sys.argv) > 1:
410                 try:
411                         import optparse
412                 except ImportError:
413                         optparse = None
414
415                 if optparse is not None:
416                         parser = optparse.OptionParser()
417                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
418                         (commandOptions, commandArgs) = parser.parse_args()
419         else:
420                 commandOptions = DummyOptions()
421                 commandArgs = []
422
423         if commandOptions.test:
424                 run_doctest()
425         else:
426                 run_calculator()