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