bae311629c84c611b1e38b8128f6abca532969ca
[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                 self._widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers)
200                 self._actualEntryDisplay = ""
201
202         @property
203         def toplevel(self):
204                 return self._widget
205
206         def get_value(self):
207                 value = self._actualEntryDisplay.strip()
208                 if any(
209                         0 < value.find(whitespace)
210                         for whitespace in string.whitespace
211                 ):
212                         self.clear()
213                         raise ValueError('Invalid input "%s"' % value)
214                 return value
215
216         def set_value(self, value):
217                 value = value.strip()
218                 if any(
219                         0 < value.find(whitespace)
220                         for whitespace in string.whitespace
221                 ):
222                         raise ValueError('Invalid input "%s"' % value)
223                 self._actualEntryDisplay = value
224                 self._widget.setText(value)
225
226         def append(self, value):
227                 value = value.strip()
228                 if any(
229                         0 < value.find(whitespace)
230                         for whitespace in string.whitespace
231                 ):
232                         raise ValueError('Invalid input "%s"' % value)
233                 self.set_value(self.get_value() + value)
234
235         def pop(self):
236                 value = self.get_value()[0:-1]
237                 self.set_value(value)
238
239         def clear(self):
240                 self.set_value("")
241
242         value = property(get_value, set_value, clear)
243
244
245 class MainWindow(object):
246
247         _plugin_search_paths = [
248                 "/opt/epi/lib/plugins/",
249                 "/usr/lib/ejpi/plugins/",
250                 os.path.join(os.path.dirname(__file__), "plugins/"),
251         ]
252
253         _user_history = "%s/history.stack" % constants._data_path_
254
255         def __init__(self, parent, app):
256                 self._app = app
257
258                 self._errorDisplay = QErrorDisplay()
259                 self._historyView = qhistory.QCalcHistory(self._errorDisplay)
260                 self._userEntry = QValueEntry()
261
262                 self._controlLayout = QtGui.QVBoxLayout()
263                 self._controlLayout.addLayout(self._errorDisplay.toplevel)
264                 self._controlLayout.addWidget(self._historyView.toplevel)
265                 self._controlLayout.addWidget(self._userEntry.toplevel)
266
267                 self._pluginKeyboardSpot = QtGui.QVBoxLayout()
268                 self._inputLayout = QtGui.QVBoxLayout()
269                 self._inputLayout.addLayout(self._pluginKeyboardSpot)
270
271                 self._layout = QtGui.QHBoxLayout()
272                 self._layout.addLayout(self._controlLayout)
273                 self._layout.addLayout(self._inputLayout)
274
275                 centralWidget = QtGui.QWidget()
276                 centralWidget.setLayout(self._layout)
277
278                 self._window = QtGui.QMainWindow(parent)
279                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
280                 maeqt.set_autorient(self._window, True)
281                 maeqt.set_stackable(self._window, True)
282                 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
283                 self._window.setCentralWidget(centralWidget)
284                 self._window.destroyed.connect(self._on_close_window)
285
286                 self._closeWindowAction = QtGui.QAction(None)
287                 self._closeWindowAction.setText("Close")
288                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
289                 self._closeWindowAction.triggered.connect(self._on_close_window)
290
291                 if IS_MAEMO:
292                         #fileMenu = self._window.menuBar().addMenu("&File")
293
294                         #viewMenu = self._window.menuBar().addMenu("&View")
295
296                         self._window.addAction(self._closeWindowAction)
297                         self._window.addAction(self._app.quitAction)
298                         self._window.addAction(self._app.fullscreenAction)
299                 else:
300                         fileMenu = self._window.menuBar().addMenu("&Units")
301                         fileMenu.addAction(self._closeWindowAction)
302                         fileMenu.addAction(self._app.quitAction)
303
304                         viewMenu = self._window.menuBar().addMenu("&View")
305                         viewMenu.addAction(self._app.fullscreenAction)
306
307                 self._window.addAction(self._app.logAction)
308
309                 self._constantPlugins = plugin_utils.ConstantPluginManager()
310                 self._constantPlugins.add_path(*self._plugin_search_paths)
311                 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
312                         try:
313                                 pluginId = self._constantPlugins.lookup_plugin(pluginName)
314                                 self._constantPlugins.enable_plugin(pluginId)
315                         except:
316                                 _moduleLogger.info("Failed to load plugin %s" % pluginName)
317
318                 self._operatorPlugins = plugin_utils.OperatorPluginManager()
319                 self._operatorPlugins.add_path(*self._plugin_search_paths)
320                 for pluginName in ["Builtin", "Trigonometry", "Computer", "Alphabet"]:
321                         try:
322                                 pluginId = self._operatorPlugins.lookup_plugin(pluginName)
323                                 self._operatorPlugins.enable_plugin(pluginId)
324                         except:
325                                 _moduleLogger.info("Failed to load plugin %s" % pluginName)
326
327                 self._keyboardPlugins = plugin_utils.KeyboardPluginManager()
328                 self._keyboardPlugins.add_path(*self._plugin_search_paths)
329                 self._activeKeyboards = []
330
331                 self._history = history.RpnCalcHistory(
332                         self._historyView,
333                         self._userEntry, self._errorDisplay,
334                         self._constantPlugins.constants, self._operatorPlugins.operators
335                 )
336                 self._load_history()
337
338                 # Basic keyboard stuff
339                 self._handler = qtpieboard.KeyboardHandler(self._on_entry_direct)
340                 self._handler.register_command_handler("push", self._on_push)
341                 self._handler.register_command_handler("unpush", self._on_unpush)
342                 self._handler.register_command_handler("backspace", self._on_entry_backspace)
343                 self._handler.register_command_handler("clear", self._on_entry_clear)
344
345                 # Main keyboard
346                 builtinKeyboardId = self._keyboardPlugins.lookup_plugin("Builtin")
347                 self._keyboardPlugins.enable_plugin(builtinKeyboardId)
348                 self._builtinPlugin = self._keyboardPlugins.keyboards["Builtin"].construct_keyboard()
349                 self._builtinKeyboard = self._builtinPlugin.setup(self._history, self._handler)
350                 self._inputLayout.addLayout(self._builtinKeyboard.toplevel)
351
352                 # Plugins
353                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Trigonometry"))
354                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Computer"))
355                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Alphabet"))
356                 self._set_plugin_kb(0)
357
358                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
359                 self._window.show()
360
361         @property
362         def window(self):
363                 return self._window
364
365         def walk_children(self):
366                 return ()
367
368         def show(self):
369                 self._window.show()
370                 for child in self.walk_children():
371                         child.show()
372
373         def hide(self):
374                 for child in self.walk_children():
375                         child.hide()
376                 self._window.hide()
377
378         def close(self):
379                 for child in self.walk_children():
380                         child.window.destroyed.disconnect(self._on_child_close)
381                         child.close()
382                 self._window.close()
383
384         def set_fullscreen(self, isFullscreen):
385                 if isFullscreen:
386                         self._window.showFullScreen()
387                 else:
388                         self._window.showNormal()
389                 for child in self.walk_children():
390                         child.set_fullscreen(isFullscreen)
391
392         def enable_plugin(self, pluginId):
393                 self._keyboardPlugins.enable_plugin(pluginId)
394                 pluginData = self._keyboardPlugins.plugin_info(pluginId)
395                 pluginName = pluginData[0]
396                 plugin = self._keyboardPlugins.keyboards[pluginName].construct_keyboard()
397                 pluginKeyboard = plugin.setup(self._history, self._handler)
398
399                 self._activeKeyboards.append({
400                         "pluginName": pluginName,
401                         "plugin": plugin,
402                         "pluginKeyboard": pluginKeyboard,
403                 })
404
405         def _set_plugin_kb(self, pluginIndex):
406                 plugin = self._activeKeyboards[pluginIndex]
407                 # @todo self._pluginButton.set_label(plugin["pluginName"])
408
409                 for i in xrange(self._pluginKeyboardSpot.count()):
410                         self._pluginKeyboardSpot.removeItem(self._pluginKeyboardSpot.itemAt(i))
411                 pluginKeyboard = plugin["pluginKeyboard"]
412                 self._pluginKeyboardSpot.addItem(pluginKeyboard.toplevel)
413
414         def _load_history(self):
415                 serialized = []
416                 try:
417                         with open(self._user_history, "rU") as f:
418                                 serialized = (
419                                         (part.strip() for part in line.split(" "))
420                                         for line in f.readlines()
421                                 )
422                 except IOError, e:
423                         if e.errno != 2:
424                                 raise
425                 self._history.deserialize_stack(serialized)
426
427         def _save_history(self):
428                 serialized = self._history.serialize_stack()
429                 with open(self._user_history, "w") as f:
430                         for lineData in serialized:
431                                 line = " ".join(data for data in lineData)
432                                 f.write("%s\n" % line)
433
434         @misc_utils.log_exception(_moduleLogger)
435         def _on_entry_direct(self, keys, modifiers):
436                 if "shift" in modifiers:
437                         keys = keys.upper()
438                 self._userEntry.append(keys)
439
440         @misc_utils.log_exception(_moduleLogger)
441         def _on_push(self, *args):
442                 self._history.push_entry()
443
444         @misc_utils.log_exception(_moduleLogger)
445         def _on_unpush(self, *args):
446                 self._historyStore.unpush()
447
448         @misc_utils.log_exception(_moduleLogger)
449         def _on_entry_backspace(self, *args):
450                 self._userEntry.pop()
451
452         @misc_utils.log_exception(_moduleLogger)
453         def _on_entry_clear(self, *args):
454                 self._userEntry.clear()
455
456         @misc_utils.log_exception(_moduleLogger)
457         def _on_clear_all(self, *args):
458                 self._history.clear()
459
460         @misc_utils.log_exception(_moduleLogger)
461         def _on_close_window(self, checked = True):
462                 self._save_history()
463
464
465 def run():
466         app = QtGui.QApplication([])
467         handle = Calculator(app)
468         qtpie.init_pies()
469         return app.exec_()
470
471
472 if __name__ == "__main__":
473         logging.basicConfig(level = logging.DEBUG)
474         try:
475                 os.makedirs(constants._data_path_)
476         except OSError, e:
477                 if e.errno != 17:
478                         raise
479
480         val = run()
481         sys.exit(val)