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