Minor tweak
[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
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_title(self._window, "%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                 if osso is not None:
231                         self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
232                         device = osso.DeviceState(self._osso)
233                         device.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                 if self._osso is not None:
326                         self._osso.close()
327
328                 try:
329                         self.__save_history()
330                 finally:
331                         gtk.main_quit()
332
333         @gtk_toolbox.log_exception(_moduleLogger)
334         def _on_copy(self, *args):
335                 equationNode = self.__history.history.peek()
336                 result = str(equationNode.evaluate())
337                 self._clipboard.set_text(result)
338
339         @gtk_toolbox.log_exception(_moduleLogger)
340         def _on_copy_equation(self, *args):
341                 equationNode = self.__history.history.peek()
342                 equation = str(equationNode)
343                 self._clipboard.set_text(equation)
344
345         @gtk_toolbox.log_exception(_moduleLogger)
346         def _on_paste(self, *args):
347                 contents = self._clipboard.wait_for_text()
348                 self.__userEntry.append(contents)
349
350         @gtk_toolbox.log_exception(_moduleLogger)
351         def _on_key_press(self, widget, event, *args):
352                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
353                 if (
354                         event.keyval == gtk.keysyms.F6 or
355                         event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
356                 ):
357                         if self._isFullScreen:
358                                 self._window.unfullscreen()
359                         else:
360                                 self._window.fullscreen()
361                 elif event.keyval == ord("l") and event.get_state() & gtk.gdk.CONTROL_MASK:
362                         with open(constants._user_logpath_, "r") as f:
363                                 logLines = f.xreadlines()
364                                 log = "".join(logLines)
365                                 self._clipboard.set_text(str(log))
366
367         @gtk_toolbox.log_exception(_moduleLogger)
368         def _on_push(self, *args):
369                 self.__history.push_entry()
370
371         @gtk_toolbox.log_exception(_moduleLogger)
372         def _on_unpush(self, *args):
373                 self.__historyStore.unpush()
374
375         @gtk_toolbox.log_exception(_moduleLogger)
376         def _on_entry_direct(self, keys, modifiers):
377                 if "shift" in modifiers:
378                         keys = keys.upper()
379                 self.__userEntry.append(keys)
380
381         @gtk_toolbox.log_exception(_moduleLogger)
382         def _on_entry_backspace(self, *args):
383                 self.__userEntry.pop()
384
385         @gtk_toolbox.log_exception(_moduleLogger)
386         def _on_entry_clear(self, *args):
387                 self.__userEntry.clear()
388
389         @gtk_toolbox.log_exception(_moduleLogger)
390         def _on_clear_all(self, *args):
391                 self.__history.clear()
392
393         @gtk_toolbox.log_exception(_moduleLogger)
394         def _on_about_activate(self, *args):
395                 dlg = gtk.AboutDialog()
396                 dlg.set_name(constants.__pretty_app_name__)
397                 dlg.set_version(constants.__version__)
398                 dlg.set_copyright("Copyright 2008 - LGPL")
399                 dlg.set_comments("""
400 ejpi A Touch Screen Optimized RPN Calculator for Maemo and Linux.
401
402 RPN: Stack based math, its fun
403 Buttons: Try both pressing and hold/drag
404 History: Try dragging things around, deleting them, etc
405 """)
406                 dlg.set_website("http://ejpi.garage.maemo.org")
407                 dlg.set_authors(["Ed Page"])
408                 dlg.run()
409                 dlg.destroy()
410
411
412 def run_doctest():
413         import doctest
414
415         failureCount, testCount = doctest.testmod()
416         if not failureCount:
417                 print "Tests Successful"
418                 sys.exit(0)
419         else:
420                 sys.exit(1)
421
422
423 def run_calculator():
424         gtk.gdk.threads_init()
425
426         gtkpie.IMAGES.add_path(os.path.join(os.path.dirname(__file__), "libraries/images"), )
427         handle = Calculator()
428         if not PROFILE_STARTUP:
429                 gtk.main()
430
431
432 class DummyOptions(object):
433
434         def __init__(self):
435                 self.test = False
436
437
438 if __name__ == "__main__":
439         logging.basicConfig(level=logging.DEBUG)
440         if len(sys.argv) > 1:
441                 try:
442                         import optparse
443                 except ImportError:
444                         optparse = None
445
446                 if optparse is not None:
447                         parser = optparse.OptionParser()
448                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
449                         (commandOptions, commandArgs) = parser.parse_args()
450         else:
451                 commandOptions = DummyOptions()
452                 commandArgs = []
453
454         if commandOptions.test:
455                 run_doctest()
456         else:
457                 run_calculator()