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