4 @todo Add preference file
5 @li enable/disable plugins
9 @todo Expand operations to support
10 @li mathml then to cairo?
12 @todo Expanded copy/paste (Unusure how far to go)
13 @li Copy formula, value, serialized, mathml, latex?
14 @li Paste serialized, value?
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
22 from __future__ import with_statement
37 from libraries import gtkpie
38 from libraries import gtkpieboard
46 _moduleLogger = logging.getLogger("ejpi_glade")
48 PLUGIN_SEARCH_PATHS = [
49 os.path.join(os.path.dirname(__file__), "plugins/"),
53 class ValueEntry(object):
55 def __init__(self, widget):
56 self.__widget = widget
57 self.__actualEntryDisplay = ""
60 value = self.__actualEntryDisplay.strip()
62 0 < value.find(whitespace)
63 for whitespace in string.whitespace
66 raise ValueError('Invalid input "%s"' % value)
69 def set_value(self, value):
72 0 < value.find(whitespace)
73 for whitespace in string.whitespace
75 raise ValueError('Invalid input "%s"' % value)
76 self.__actualEntryDisplay = value
77 self.__widget.set_text(value)
79 def append(self, value):
82 0 < value.find(whitespace)
83 for whitespace in string.whitespace
85 raise ValueError('Invalid input "%s"' % value)
86 self.set_value(self.get_value() + value)
89 value = self.get_value()[0:-1]
95 value = property(get_value, set_value, clear)
98 class Calculator(object):
101 '/usr/lib/ejpi/ejpi.glade',
102 os.path.join(os.path.dirname(__file__), "ejpi.glade"),
103 os.path.join(os.path.dirname(__file__), "../lib/ejpi.glade"),
106 _plugin_search_paths = [
107 "/usr/lib/ejpi/plugins/",
108 os.path.join(os.path.dirname(__file__), "plugins/"),
111 _user_data = os.path.expanduser("~/.%s/" % constants.__app_name__)
112 _user_settings = "%s/settings.ini" % _user_data
113 _user_history = "%s/history.stack" % _user_data
115 MIN_BUTTON_SIZE = min(800, 480) // 6 - 20
118 self.__constantPlugins = plugin_utils.ConstantPluginManager()
119 self.__constantPlugins.add_path(*self._plugin_search_paths)
120 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
122 pluginId = self.__constantPlugins.lookup_plugin(pluginName)
123 self.__constantPlugins.enable_plugin(pluginId)
125 warnings.warn("Failed to load plugin %s" % pluginName)
127 self.__operatorPlugins = plugin_utils.OperatorPluginManager()
128 self.__operatorPlugins.add_path(*self._plugin_search_paths)
129 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
131 pluginId = self.__operatorPlugins.lookup_plugin(pluginName)
132 self.__operatorPlugins.enable_plugin(pluginId)
134 warnings.warn("Failed to load plugin %s" % pluginName)
136 self.__keyboardPlugins = plugin_utils.KeyboardPluginManager()
137 self.__keyboardPlugins.add_path(*self._plugin_search_paths)
138 self.__activeKeyboards = []
140 for path in self._glade_files:
141 if os.path.isfile(path):
142 self._widgetTree = gtk.glade.XML(path)
145 self.display_error_message("Cannot find ejpi.glade")
149 os.makedirs(self._user_data)
154 self._clipboard = gtk.clipboard_get()
155 self._window = self._widgetTree.get_widget("mainWindow")
158 self._isFullScreen = False
159 self._app = hildonize.get_app_class()()
160 self._window = hildonize.hildonize_window(self._app, self._window)
162 menu = hildonize.hildonize_menu(
164 self._widgetTree.get_widget("mainMenubar"),
168 for scrollingWidgetName in (
171 scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
172 assert scrollingWidget is not None, scrollingWidgetName
173 hildonize.hildonize_scrollwindow_with_viewport(scrollingWidget)
175 self.__errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
176 self.__userEntry = ValueEntry(self._widgetTree.get_widget("entryView"))
177 self.__stackView = self._widgetTree.get_widget("historyView")
178 self.__pluginButton = self._widgetTree.get_widget("keyboardSelectionButton")
180 self.__historyStore = gtkhistory.GtkCalcHistory(self.__stackView)
181 self.__history = history.RpnCalcHistory(
183 self.__userEntry, self.__errorDisplay,
184 self.__constantPlugins.constants, self.__operatorPlugins.operators
186 self.__load_history()
188 self.__sliceStyle = gtkpie.generate_pie_style(gtk.Button())
189 self.__handler = gtkpieboard.KeyboardHandler(self._on_entry_direct)
190 self.__handler.register_command_handler("push", self._on_push)
191 self.__handler.register_command_handler("unpush", self._on_unpush)
192 self.__handler.register_command_handler("backspace", self._on_entry_backspace)
193 self.__handler.register_command_handler("clear", self._on_entry_clear)
195 builtinKeyboardId = self.__keyboardPlugins.lookup_plugin("Builtin")
196 self.__keyboardPlugins.enable_plugin(builtinKeyboardId)
197 self.__builtinPlugin = self.__keyboardPlugins.keyboards["Builtin"].construct_keyboard()
198 self.__builtinKeyboard = self.__builtinPlugin.setup(self.__history, self.__sliceStyle, self.__handler)
199 self._widgetTree.get_widget("mainKeyboard").pack_start(self.__builtinKeyboard)
200 for child in self.__builtinKeyboard.get_children():
201 child.set_size_request(self.MIN_BUTTON_SIZE, self.MIN_BUTTON_SIZE)
203 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Trigonometry"))
204 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Computer"))
205 self.enable_plugin(self.__keyboardPlugins.lookup_plugin("Alphabet"))
208 "on_calculator_quit": self._on_close,
209 "on_paste": self._on_paste,
210 "on_clear_history": self._on_clear_all,
211 "on_about": self._on_about_activate,
213 self._widgetTree.signal_autoconnect(callbackMapping)
214 self._widgetTree.get_widget("copyMenuItem").connect("activate", self._on_copy)
215 self._widgetTree.get_widget("copyEquationMenuItem").connect("activate", self._on_copy_equation)
216 self._window.connect("key-press-event", self._on_key_press)
217 self._window.connect("window-state-event", self._on_window_state_change)
218 self._widgetTree.get_widget("entryView").connect("activate", self._on_push)
219 self.__pluginButton.connect("clicked", self._on_kb_plugin_selection_button)
221 self._set_plugin_kb(0)
223 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
224 self._window.connect("destroy", self._on_close)
225 self._window.show_all()
227 if not hildonize.IS_HILDON_SUPPORTED:
228 _moduleLogger.warning("No hildonization support")
236 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
237 device = osso.DeviceState(self._osso)
238 device.set_device_state_callback(self._on_device_state_change, 0)
240 _moduleLogger.warning("No OSSO support")
242 def display_error_message(self, msg):
243 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
245 def close(dialog, response, editor):
246 editor.about_dialog = None
248 error_dialog.connect("response", close, self)
251 def enable_plugin(self, pluginId):
252 self.__keyboardPlugins.enable_plugin(pluginId)
253 pluginData = self.__keyboardPlugins.plugin_info(pluginId)
254 pluginName = pluginData[0]
255 plugin = self.__keyboardPlugins.keyboards[pluginName].construct_keyboard()
256 pluginKeyboard = plugin.setup(self.__history, self.__sliceStyle, self.__handler)
257 for child in pluginKeyboard.get_children():
258 child.set_size_request(self.MIN_BUTTON_SIZE, self.MIN_BUTTON_SIZE)
260 self.__activeKeyboards.append({
261 "pluginName": pluginName,
263 "pluginKeyboard": pluginKeyboard,
266 def _on_kb_plugin_selection_button(self, *args):
268 pluginNames = [plugin["pluginName"] for plugin in self.__activeKeyboards]
269 oldIndex = pluginNames.index(self.__pluginButton.get_label())
270 newIndex = hildonize.touch_selector(self._window, "Keyboards", pluginNames, oldIndex)
271 self._set_plugin_kb(newIndex)
273 self.__errorDisplay.push_exception()
275 def _set_plugin_kb(self, pluginIndex):
276 plugin = self.__activeKeyboards[pluginIndex]
277 self.__pluginButton.set_label(plugin["pluginName"])
279 pluginParent = self._widgetTree.get_widget("pluginKeyboard")
280 oldPluginChildren = pluginParent.get_children()
281 if oldPluginChildren:
282 assert len(oldPluginChildren) == 1, "%r" % (oldPluginChildren, )
283 pluginParent.remove(oldPluginChildren[0])
284 oldPluginChildren[0].hide()
285 pluginKeyboard = plugin["pluginKeyboard"]
286 pluginParent.pack_start(pluginKeyboard)
288 pluginKeyboard.show_all()
290 def __load_history(self):
293 with open(self._user_history, "rU") as f:
295 (part.strip() for part in line.split(" "))
296 for line in f.readlines()
301 self.__history.deserialize_stack(serialized)
303 def __save_history(self):
304 serialized = self.__history.serialize_stack()
305 with open(self._user_history, "w") as f:
306 for lineData in serialized:
307 line = " ".join(data for data in lineData)
308 f.write("%s\n" % line)
310 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
312 For system_inactivity, we have no background tasks to pause
314 @note Hildon specific
320 if save_unsaved_data or shutdown:
321 self.__save_history()
323 self.__errorDisplay.push_exception()
325 def _on_window_state_change(self, widget, event, *args):
327 @note Hildon specific
330 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
331 self._isFullScreen = True
333 self._isFullScreen = False
335 self.__errorDisplay.push_exception()
337 def _on_close(self, *args, **kwds):
339 if self._osso is not None:
343 self.__save_history()
347 self.__errorDisplay.push_exception()
349 def _on_copy(self, *args):
351 equationNode = self.__history.history.peek()
352 result = str(equationNode.evaluate())
353 self._clipboard.set_text(result)
355 self.__errorDisplay.push_exception()
357 def _on_copy_equation(self, *args):
359 equationNode = self.__history.history.peek()
360 equation = str(equationNode)
361 self._clipboard.set_text(equation)
363 self.__errorDisplay.push_exception()
365 def _on_paste(self, *args):
367 contents = self._clipboard.wait_for_text()
368 self.__userEntry.append(contents)
370 self.__errorDisplay.push_exception()
372 def _on_key_press(self, widget, event, *args):
374 @note Hildon specific
377 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
379 event.keyval == gtk.keysyms.F6 or
380 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
382 if self._isFullScreen:
383 self._window.unfullscreen()
385 self._window.fullscreen()
387 self.__errorDisplay.push_exception()
389 def _on_push(self, *args):
391 self.__history.push_entry()
393 self.__errorDisplay.push_exception()
395 def _on_unpush(self, *args):
397 self.__historyStore.unpush()
399 self.__errorDisplay.push_exception()
401 def _on_entry_direct(self, keys, modifiers):
403 if "shift" in modifiers:
405 self.__userEntry.append(keys)
407 self.__errorDisplay.push_exception()
409 def _on_entry_backspace(self, *args):
411 self.__userEntry.pop()
413 self.__errorDisplay.push_exception()
415 def _on_entry_clear(self, *args):
417 self.__userEntry.clear()
419 self.__errorDisplay.push_exception()
421 def _on_clear_all(self, *args):
423 self.__history.clear()
425 self.__errorDisplay.push_exception()
427 def _on_about_activate(self, *args):
428 dlg = gtk.AboutDialog()
429 dlg.set_name(constants.__pretty_app_name__)
430 dlg.set_version(constants.__version__)
431 dlg.set_copyright("Copyright 2008 - LGPL")
433 ejpi A Touch Screen Optimized RPN Calculator for Maemo and Linux.
435 RPN: Stack based math, its fun
436 Buttons: Try both pressing and hold/drag
437 History: Try dragging things around, deleting them, etc
439 dlg.set_website("http://ejpi.garage.maemo.org")
440 dlg.set_authors(["Ed Page"])
448 failureCount, testCount = doctest.testmod()
450 print "Tests Successful"
456 def run_calculator():
457 gtk.gdk.threads_init()
459 gtkpie.IMAGES.add_path(os.path.join(os.path.dirname(__file__), "libraries/images"), )
460 handle = Calculator()
464 class DummyOptions(object):
470 if __name__ == "__main__":
471 logging.basicConfig(level=logging.DEBUG)
472 if len(sys.argv) > 1:
478 if optparse is not None:
479 parser = optparse.OptionParser()
480 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
481 (commandOptions, commandArgs) = parser.parse_args()
483 commandOptions = DummyOptions()
486 if commandOptions.test: