Improving input behavor
[ejpi] / src / ejpi_qt.py
1 #!/usr/bin/env python
2 # -*- coding: UTF8 -*-
3
4 from __future__ import with_statement
5
6 import sys
7 import os
8 import simplejson
9 import string
10 import logging
11
12 from PyQt4 import QtGui
13 from PyQt4 import QtCore
14
15 import constants
16 import maeqt
17 from util import misc as misc_utils
18
19 from libraries import qtpie
20 from libraries import qtpieboard
21 import plugin_utils
22 import history
23 import qhistory
24
25
26 _moduleLogger = logging.getLogger(__name__)
27
28
29 IS_MAEMO = True
30
31
32 PLUGIN_SEARCH_PATHS = [
33         os.path.join(os.path.dirname(__file__), "plugins/"),
34 ]
35
36
37 class Calculator(object):
38
39         def __init__(self, app):
40                 self._app = app
41                 self._recent = []
42                 self._hiddenCategories = set()
43                 self._hiddenUnits = {}
44                 self._clipboard = QtGui.QApplication.clipboard()
45                 self._mainWindow = None
46
47                 self._fullscreenAction = QtGui.QAction(None)
48                 self._fullscreenAction.setText("Fullscreen")
49                 self._fullscreenAction.setCheckable(True)
50                 self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
51                 self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
52
53                 self._logAction = QtGui.QAction(None)
54                 self._logAction.setText("Log")
55                 self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l"))
56                 self._logAction.triggered.connect(self._on_log)
57
58                 self._quitAction = QtGui.QAction(None)
59                 self._quitAction.setText("Quit")
60                 self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q"))
61                 self._quitAction.triggered.connect(self._on_quit)
62
63                 self._app.lastWindowClosed.connect(self._on_app_quit)
64                 self.load_settings()
65
66                 self._mainWindow = MainWindow(None, self)
67                 self._mainWindow.window.destroyed.connect(self._on_child_close)
68
69         def load_settings(self):
70                 try:
71                         with open(constants._user_settings_, "r") as settingsFile:
72                                 settings = simplejson.load(settingsFile)
73                 except IOError, e:
74                         _moduleLogger.info("No settings")
75                         settings = {}
76                 except ValueError:
77                         _moduleLogger.info("Settings were corrupt")
78                         settings = {}
79
80                 self._fullscreenAction.setChecked(settings.get("isFullScreen", False))
81
82         def save_settings(self):
83                 settings = {
84                         "isFullScreen": self._fullscreenAction.isChecked(),
85                 }
86                 with open(constants._user_settings_, "w") as settingsFile:
87                         simplejson.dump(settings, settingsFile)
88
89         @property
90         def fullscreenAction(self):
91                 return self._fullscreenAction
92
93         @property
94         def logAction(self):
95                 return self._logAction
96
97         @property
98         def quitAction(self):
99                 return self._quitAction
100
101         def _close_windows(self):
102                 if self._mainWindow is not None:
103                         self._mainWindow.window.destroyed.disconnect(self._on_child_close)
104                         self._mainWindow.close()
105                         self._mainWindow = None
106
107         @misc_utils.log_exception(_moduleLogger)
108         def _on_app_quit(self, checked = False):
109                 self.save_settings()
110
111         @misc_utils.log_exception(_moduleLogger)
112         def _on_child_close(self, obj = None):
113                 self._mainWindow = None
114
115         @misc_utils.log_exception(_moduleLogger)
116         def _on_toggle_fullscreen(self, checked = False):
117                 for window in self._walk_children():
118                         window.set_fullscreen(checked)
119
120         @misc_utils.log_exception(_moduleLogger)
121         def _on_log(self, checked = False):
122                 with open(constants._user_logpath_, "r") as f:
123                         logLines = f.xreadlines()
124                         log = "".join(logLines)
125                         self._clipboard.setText(log)
126
127         @misc_utils.log_exception(_moduleLogger)
128         def _on_quit(self, checked = False):
129                 self._close_windows()
130
131
132 class QErrorDisplay(object):
133
134         def __init__(self):
135                 self._messages = []
136
137                 icon = QtGui.QIcon.fromTheme("gtk-dialog-error")
138                 self._severityIcon = icon.pixmap(32, 32)
139                 self._severityLabel = QtGui.QLabel()
140                 self._severityLabel.setPixmap(self._severityIcon)
141
142                 self._message = QtGui.QLabel()
143                 self._message.setText("Boo")
144
145                 icon = QtGui.QIcon.fromTheme("gtk-close")
146                 self._closeLabel = QtGui.QPushButton(icon, "")
147                 self._closeLabel.clicked.connect(self._on_close)
148
149                 self._controlLayout = QtGui.QHBoxLayout()
150                 self._controlLayout.addWidget(self._severityLabel)
151                 self._controlLayout.addWidget(self._message)
152                 self._controlLayout.addWidget(self._closeLabel)
153
154                 self._topLevelLayout = QtGui.QHBoxLayout()
155                 self._topLevelLayout.addLayout(self._controlLayout)
156                 self._hide_message()
157
158         @property
159         def toplevel(self):
160                 return self._topLevelLayout
161
162         def push_message(self, message):
163                 self._messages.append(message)
164                 if 1 == len(self._messages):
165                         self._show_message(message)
166
167         def push_exception(self):
168                 userMessage = str(sys.exc_info()[1])
169                 _moduleLogger.exception(userMessage)
170                 self.push_message(userMessage)
171
172         def pop_message(self):
173                 del self._messages[0]
174                 if 0 == len(self._messages):
175                         self._hide_message()
176                 else:
177                         self._message.setText(self._messages[0])
178
179         def _on_close(self, *args):
180                 self.pop_message()
181
182         def _show_message(self, message):
183                 self._message.setText(message)
184                 self._severityLabel.show()
185                 self._message.show()
186                 self._closeLabel.show()
187
188         def _hide_message(self):
189                 self._message.setText("")
190                 self._severityLabel.hide()
191                 self._message.hide()
192                 self._closeLabel.hide()
193
194
195 class QValueEntry(object):
196
197         def __init__(self):
198                 self._widget = QtGui.QLineEdit("")
199                 maeqt.mark_numbers_preferred(self._widget)
200
201         @property
202         def toplevel(self):
203                 return self._widget
204
205         @property
206         def entry(self):
207                 return self._widget
208
209         def get_value(self):
210                 value = str(self._widget.text()).strip()
211                 if any(
212                         0 < value.find(whitespace)
213                         for whitespace in string.whitespace
214                 ):
215                         self.clear()
216                         raise ValueError('Invalid input "%s"' % value)
217                 return value
218
219         def set_value(self, value):
220                 value = value.strip()
221                 if any(
222                         0 < value.find(whitespace)
223                         for whitespace in string.whitespace
224                 ):
225                         raise ValueError('Invalid input "%s"' % value)
226                 self._widget.setText(value)
227
228         def append(self, value):
229                 value = value.strip()
230                 if any(
231                         0 < value.find(whitespace)
232                         for whitespace in string.whitespace
233                 ):
234                         raise ValueError('Invalid input "%s"' % value)
235                 self.set_value(self.get_value() + value)
236
237         def pop(self):
238                 value = self.get_value()[0:-1]
239                 self.set_value(value)
240
241         def clear(self):
242                 self.set_value("")
243
244         value = property(get_value, set_value, clear)
245
246
247 class MainWindow(object):
248
249         _plugin_search_paths = [
250                 "/opt/epi/lib/plugins/",
251                 "/usr/lib/ejpi/plugins/",
252                 os.path.join(os.path.dirname(__file__), "plugins/"),
253         ]
254
255         _user_history = "%s/history.stack" % constants._data_path_
256
257         def __init__(self, parent, app):
258                 self._app = app
259
260                 self._errorDisplay = QErrorDisplay()
261                 self._historyView = qhistory.QCalcHistory(self._errorDisplay)
262                 self._userEntry = QValueEntry()
263                 self._userEntry.entry.returnPressed.connect(self._on_push)
264
265                 self._controlLayout = QtGui.QVBoxLayout()
266                 self._controlLayout.addLayout(self._errorDisplay.toplevel)
267                 self._controlLayout.addWidget(self._historyView.toplevel)
268                 self._controlLayout.addWidget(self._userEntry.toplevel)
269
270                 self._pluginKeyboardSpot = QtGui.QVBoxLayout()
271                 self._inputLayout = QtGui.QVBoxLayout()
272                 self._inputLayout.addLayout(self._pluginKeyboardSpot)
273
274                 self._layout = QtGui.QHBoxLayout()
275                 self._layout.addLayout(self._controlLayout)
276                 self._layout.addLayout(self._inputLayout)
277
278                 centralWidget = QtGui.QWidget()
279                 centralWidget.setLayout(self._layout)
280
281                 self._window = QtGui.QMainWindow(parent)
282                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
283                 maeqt.set_autorient(self._window, True)
284                 maeqt.set_stackable(self._window, True)
285                 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
286                 self._window.setCentralWidget(centralWidget)
287                 self._window.destroyed.connect(self._on_close_window)
288
289                 self._closeWindowAction = QtGui.QAction(None)
290                 self._closeWindowAction.setText("Close")
291                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
292                 self._closeWindowAction.triggered.connect(self._on_close_window)
293
294                 if IS_MAEMO:
295                         #fileMenu = self._window.menuBar().addMenu("&File")
296
297                         #viewMenu = self._window.menuBar().addMenu("&View")
298
299                         self._window.addAction(self._closeWindowAction)
300                         self._window.addAction(self._app.quitAction)
301                         self._window.addAction(self._app.fullscreenAction)
302                 else:
303                         fileMenu = self._window.menuBar().addMenu("&Units")
304                         fileMenu.addAction(self._closeWindowAction)
305                         fileMenu.addAction(self._app.quitAction)
306
307                         viewMenu = self._window.menuBar().addMenu("&View")
308                         viewMenu.addAction(self._app.fullscreenAction)
309
310                 self._window.addAction(self._app.logAction)
311
312                 self._constantPlugins = plugin_utils.ConstantPluginManager()
313                 self._constantPlugins.add_path(*self._plugin_search_paths)
314                 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
315                         try:
316                                 pluginId = self._constantPlugins.lookup_plugin(pluginName)
317                                 self._constantPlugins.enable_plugin(pluginId)
318                         except:
319                                 _moduleLogger.info("Failed to load plugin %s" % pluginName)
320
321                 self._operatorPlugins = plugin_utils.OperatorPluginManager()
322                 self._operatorPlugins.add_path(*self._plugin_search_paths)
323                 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
324                         try:
325                                 pluginId = self._operatorPlugins.lookup_plugin(pluginName)
326                                 self._operatorPlugins.enable_plugin(pluginId)
327                         except:
328                                 _moduleLogger.info("Failed to load plugin %s" % pluginName)
329
330                 self._keyboardPlugins = plugin_utils.KeyboardPluginManager()
331                 self._keyboardPlugins.add_path(*self._plugin_search_paths)
332                 self._activeKeyboards = []
333
334                 self._history = history.RpnCalcHistory(
335                         self._historyView,
336                         self._userEntry, self._errorDisplay,
337                         self._constantPlugins.constants, self._operatorPlugins.operators
338                 )
339                 self._load_history()
340
341                 # Basic keyboard stuff
342                 self._handler = qtpieboard.KeyboardHandler(self._on_entry_direct)
343                 self._handler.register_command_handler("push", self._on_push)
344                 self._handler.register_command_handler("unpush", self._on_unpush)
345                 self._handler.register_command_handler("backspace", self._on_entry_backspace)
346                 self._handler.register_command_handler("clear", self._on_entry_clear)
347
348                 # Main keyboard
349                 builtinKeyboardId = self._keyboardPlugins.lookup_plugin("Builtin")
350                 self._keyboardPlugins.enable_plugin(builtinKeyboardId)
351                 self._builtinPlugin = self._keyboardPlugins.keyboards["Builtin"].construct_keyboard()
352                 self._builtinKeyboard = self._builtinPlugin.setup(self._history, self._handler)
353                 self._inputLayout.addLayout(self._builtinKeyboard.toplevel)
354
355                 # Plugins
356                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Trigonometry"))
357                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Computer"))
358                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Alphabet"))
359                 self._set_plugin_kb(0)
360
361                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
362                 self._window.show()
363
364         @property
365         def window(self):
366                 return self._window
367
368         def walk_children(self):
369                 return ()
370
371         def show(self):
372                 self._window.show()
373                 for child in self.walk_children():
374                         child.show()
375
376         def hide(self):
377                 for child in self.walk_children():
378                         child.hide()
379                 self._window.hide()
380
381         def close(self):
382                 for child in self.walk_children():
383                         child.window.destroyed.disconnect(self._on_child_close)
384                         child.close()
385                 self._window.close()
386
387         def set_fullscreen(self, isFullscreen):
388                 if isFullscreen:
389                         self._window.showFullScreen()
390                 else:
391                         self._window.showNormal()
392                 for child in self.walk_children():
393                         child.set_fullscreen(isFullscreen)
394
395         def enable_plugin(self, pluginId):
396                 self._keyboardPlugins.enable_plugin(pluginId)
397                 pluginData = self._keyboardPlugins.plugin_info(pluginId)
398                 pluginName = pluginData[0]
399                 plugin = self._keyboardPlugins.keyboards[pluginName].construct_keyboard()
400                 pluginKeyboard = plugin.setup(self._history, self._handler)
401
402                 self._activeKeyboards.append({
403                         "pluginName": pluginName,
404                         "plugin": plugin,
405                         "pluginKeyboard": pluginKeyboard,
406                 })
407
408         def _set_plugin_kb(self, pluginIndex):
409                 plugin = self._activeKeyboards[pluginIndex]
410                 # @todo self._pluginButton.set_label(plugin["pluginName"])
411
412                 for i in xrange(self._pluginKeyboardSpot.count()):
413                         self._pluginKeyboardSpot.removeItem(self._pluginKeyboardSpot.itemAt(i))
414                 pluginKeyboard = plugin["pluginKeyboard"]
415                 self._pluginKeyboardSpot.addItem(pluginKeyboard.toplevel)
416
417         def _load_history(self):
418                 serialized = []
419                 try:
420                         with open(self._user_history, "rU") as f:
421                                 serialized = (
422                                         (part.strip() for part in line.split(" "))
423                                         for line in f.readlines()
424                                 )
425                 except IOError, e:
426                         if e.errno != 2:
427                                 raise
428                 self._history.deserialize_stack(serialized)
429
430         def _save_history(self):
431                 serialized = self._history.serialize_stack()
432                 with open(self._user_history, "w") as f:
433                         for lineData in serialized:
434                                 line = " ".join(data for data in lineData)
435                                 f.write("%s\n" % line)
436
437         @misc_utils.log_exception(_moduleLogger)
438         def _on_entry_direct(self, keys, modifiers):
439                 if "shift" in modifiers:
440                         keys = keys.upper()
441                 self._userEntry.append(keys)
442
443         @misc_utils.log_exception(_moduleLogger)
444         def _on_push(self, *args):
445                 self._history.push_entry()
446
447         @misc_utils.log_exception(_moduleLogger)
448         def _on_unpush(self, *args):
449                 self._historyStore.unpush()
450
451         @misc_utils.log_exception(_moduleLogger)
452         def _on_entry_backspace(self, *args):
453                 self._userEntry.pop()
454
455         @misc_utils.log_exception(_moduleLogger)
456         def _on_entry_clear(self, *args):
457                 self._userEntry.clear()
458
459         @misc_utils.log_exception(_moduleLogger)
460         def _on_clear_all(self, *args):
461                 self._history.clear()
462
463         @misc_utils.log_exception(_moduleLogger)
464         def _on_close_window(self, checked = True):
465                 self._save_history()
466
467
468 def run():
469         app = QtGui.QApplication([])
470         handle = Calculator(app)
471         qtpie.init_pies()
472         return app.exec_()
473
474
475 if __name__ == "__main__":
476         logging.basicConfig(level = logging.DEBUG)
477         try:
478                 os.makedirs(constants._data_path_)
479         except OSError, e:
480                 if e.errno != 17:
481                         raise
482
483         val = run()
484         sys.exit(val)