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