Consolidating handling of code between projects
[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 import gtk_toolbox
45
46
47 PLUGIN_SEARCH_PATHS = [
48         os.path.join(os.path.dirname(__file__), "plugins/"),
49 ]
50
51
52 class ValueEntry(object):
53
54         def __init__(self, widget):
55                 self.__widget = widget
56                 self.__actualEntryDisplay = ""
57
58         def get_value(self):
59                 value = self.__actualEntryDisplay.strip()
60                 if any(
61                         0 < value.find(whitespace)
62                         for whitespace in string.whitespace
63                 ):
64                         self.clear()
65                         raise ValueError('Invalid input "%s"' % value)
66                 return value
67
68         def set_value(self, value):
69                 value = value.strip()
70                 if any(
71                         0 < value.find(whitespace)
72                         for whitespace in string.whitespace
73                 ):
74                         raise ValueError('Invalid input "%s"' % value)
75                 self.__actualEntryDisplay = value
76                 self.__widget.set_text(value)
77
78         def append(self, value):
79                 value = value.strip()
80                 if any(
81                         0 < value.find(whitespace)
82                         for whitespace in string.whitespace
83                 ):
84                         raise ValueError('Invalid input "%s"' % value)
85                 self.set_value(self.get_value() + value)
86
87         def pop(self):
88                 value = self.get_value()[0:-1]
89                 self.set_value(value)
90
91         def clear(self):
92                 self.set_value("")
93
94         value = property(get_value, set_value, clear)
95
96
97 class Calculator(object):
98
99         __pretty_app_name__ = "e**(j pi) + 1 = 0"
100         __app_name__ = "ejpi"
101         __version__ = "0.9.3"
102         __app_magic__ = 0xdeadbeef
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/" % __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                 try:
150                         os.makedirs(self._user_data)
151                 except OSError, e:
152                         if e.errno != 17:
153                                 raise
154
155                 self._clipboard = gtk.clipboard_get()
156                 self.__window = self._widgetTree.get_widget("mainWindow")
157
158                 global hildon
159                 self._app = None
160                 self._isFullScreen = False
161                 if hildon is not None:
162                         self._app = hildon.Program()
163                         self.__window = hildon.Window()
164                         self._widgetTree.get_widget("mainLayout").reparent(self.__window)
165                         self._app.add_window(self.__window)
166                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('scrollingHistory'), True)
167
168                         gtkMenu = self._widgetTree.get_widget("mainMenubar")
169                         menu = gtk.Menu()
170                         for child in gtkMenu.get_children():
171                                 child.reparent(menu)
172                         self.__window.set_menu(menu)
173                         gtkMenu.destroy()
174
175                         self.__window.connect("key-press-event", self._on_key_press)
176                         self.__window.connect("window-state-event", self._on_window_state_change)
177                 else:
178                         pass # warnings.warn("No Hildon", UserWarning, 2)
179
180                 self.__errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
181                 self.__userEntry = ValueEntry(self._widgetTree.get_widget("entryView"))
182                 self.__stackView = self._widgetTree.get_widget("historyView")
183
184                 self.__historyStore = gtkhistory.GtkCalcHistory(self.__stackView)
185                 self.__history = history.RpnCalcHistory(
186                         self.__historyStore,
187                         self.__userEntry, self.__errorDisplay,
188                         self.__constantPlugins.constants, self.__operatorPlugins.operators
189                 )
190                 self.__load_history()
191
192                 self.__sliceStyle = gtkpie.generate_pie_style(gtk.Button())
193                 self.__handler = gtkpieboard.KeyboardHandler(self._on_entry_direct)
194                 self.__handler.register_command_handler("push", self._on_push)
195                 self.__handler.register_command_handler("unpush", self._on_unpush)
196                 self.__handler.register_command_handler("backspace", self._on_entry_backspace)
197                 self.__handler.register_command_handler("clear", self._on_entry_clear)
198
199                 builtinKeyboardId = self.__keyboardPlugins.lookup_plugin("Builtin")
200                 self.__keyboardPlugins.enable_plugin(builtinKeyboardId)
201                 self.__builtinPlugin = self.__keyboardPlugins.keyboards["Builtin"].construct_keyboard()
202                 self.__builtinKeyboard = self.__builtinPlugin.setup(self.__history, self.__sliceStyle, self.__handler)
203                 self._widgetTree.get_widget("functionLayout").pack_start(self.__builtinKeyboard)
204                 self._widgetTree.get_widget("functionLayout").reorder_child(self.__builtinKeyboard, 0)
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
217                 if self.__window:
218                         if hildon is None:
219                                 self.__window.set_title("%s" % self.__pretty_app_name__)
220                         self.__window.connect("destroy", self._on_close)
221                         self.__window.show_all()
222
223                 try:
224                         import osso
225                 except ImportError:
226                         osso = None
227
228                 self._osso = None
229                 if osso is not None:
230                         self._osso = osso.Context(Calculator.__app_name__, Calculator.__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_paste(self, *args):
313                 contents = self._clipboard.wait_for_text()
314                 self.__userEntry.append(contents)
315
316         def _on_key_press(self, widget, event, *args):
317                 """
318                 @note Hildon specific
319                 """
320                 if event.keyval == gtk.keysyms.F6:
321                         if self._isFullScreen:
322                                 self.__window.unfullscreen()
323                         else:
324                                 self.__window.fullscreen()
325
326         def _on_push(self, *args):
327                 self.__history.push_entry()
328
329         def _on_unpush(self, *args):
330                 self.__historyStore.unpush()
331
332         def _on_entry_direct(self, keys, modifiers):
333                 if "shift" in modifiers:
334                         keys = keys.upper()
335                 self.__userEntry.append(keys)
336
337         def _on_entry_backspace(self, *args):
338                 self.__userEntry.pop()
339
340         def _on_entry_clear(self, *args):
341                 self.__userEntry.clear()
342
343         def _on_clear_all(self, *args):
344                 self.__history.clear()
345
346         def _on_about_activate(self, *args):
347                 dlg = gtk.AboutDialog()
348                 dlg.set_name(self.__pretty_app_name__)
349                 dlg.set_version(self.__version__)
350                 dlg.set_copyright("Copyright 2008 - LGPL")
351                 dlg.set_comments("")
352                 dlg.set_website("")
353                 dlg.set_authors([""])
354                 dlg.run()
355                 dlg.destroy()
356
357
358 def run_doctest():
359         import doctest
360
361         failureCount, testCount = doctest.testmod()
362         if not failureCount:
363                 print "Tests Successful"
364                 sys.exit(0)
365         else:
366                 sys.exit(1)
367
368
369 def run_calculator():
370         gtk.gdk.threads_init()
371
372         gtkpie.IMAGES.add_path(os.path.join(os.path.dirname(__file__), "libraries/images"), )
373         if hildon is not None:
374                 gtk.set_application_name(Calculator.__pretty_app_name__)
375         handle = Calculator()
376         gtk.main()
377
378
379 class DummyOptions(object):
380
381         def __init__(self):
382                 self.test = False
383
384
385 if __name__ == "__main__":
386         if len(sys.argv) > 1:
387                 try:
388                         import optparse
389                 except ImportError:
390                         optparse = None
391
392                 if optparse is not None:
393                         parser = optparse.OptionParser()
394                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
395                         (commandOptions, commandArgs) = parser.parse_args()
396         else:
397                 commandOptions = DummyOptions()
398                 commandArgs = []
399
400         if commandOptions.test:
401                 run_doctest()
402         else:
403                 run_calculator()