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