fc56e9f04a57be977234f6a397b40b1c9f0ea575
[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._widget = QtGui.QWidget()
157                 self._widget.setLayout(self._topLevelLayout)
158                 self._hide_message()
159
160         @property
161         def toplevel(self):
162                 return self._widget
163
164         def push_message(self, message):
165                 self._messages.append(message)
166                 if 1 == len(self._messages):
167                         self._show_message(message)
168
169         def push_exception(self):
170                 userMessage = str(sys.exc_info()[1])
171                 _moduleLogger.exception(userMessage)
172                 self.push_message(userMessage)
173
174         def pop_message(self):
175                 del self._messages[0]
176                 if 0 == len(self._messages):
177                         self._hide_message()
178                 else:
179                         self._message.setText(self._messages[0])
180
181         def _on_close(self, *args):
182                 self.pop_message()
183
184         def _show_message(self, message):
185                 self._message.setText(message)
186                 self._widget.show()
187
188         def _hide_message(self):
189                 self._message.setText("")
190                 self._widget.hide()
191
192
193 class QValueEntry(object):
194
195         def __init__(self):
196                 self._widget = QtGui.QLineEdit("")
197                 maeqt.mark_numbers_preferred(self._widget)
198
199         @property
200         def toplevel(self):
201                 return self._widget
202
203         @property
204         def entry(self):
205                 return self._widget
206
207         def get_value(self):
208                 value = str(self._widget.text()).strip()
209                 if any(
210                         0 < value.find(whitespace)
211                         for whitespace in string.whitespace
212                 ):
213                         self.clear()
214                         raise ValueError('Invalid input "%s"' % value)
215                 return value
216
217         def set_value(self, value):
218                 value = value.strip()
219                 if any(
220                         0 < value.find(whitespace)
221                         for whitespace in string.whitespace
222                 ):
223                         raise ValueError('Invalid input "%s"' % 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                 self._userEntry.entry.returnPressed.connect(self._on_push)
262                 self._userEntryLayout = QtGui.QHBoxLayout()
263                 self._userEntryLayout.addWidget(self._userEntry.toplevel)
264
265                 self._controlLayout = QtGui.QVBoxLayout()
266                 self._controlLayout.addWidget(self._errorDisplay.toplevel)
267                 self._controlLayout.addWidget(self._historyView.toplevel)
268                 self._controlLayout.addLayout(self._userEntryLayout)
269
270                 self._keyboardTabs = QtGui.QTabWidget()
271
272                 if maeqt.screen_orientation() == QtCore.Qt.Vertical:
273                         defaultLayoutOrientation = QtGui.QBoxLayout.TopToBottom
274                         self._keyboardTabs.setTabPosition(QtGui.QTabWidget.East)
275                 else:
276                         defaultLayoutOrientation = QtGui.QBoxLayout.LeftToRight
277                         self._keyboardTabs.setTabPosition(QtGui.QTabWidget.North)
278                 self._layout = QtGui.QBoxLayout(defaultLayoutOrientation)
279                 self._layout.addLayout(self._controlLayout)
280                 self._layout.addWidget(self._keyboardTabs)
281
282                 centralWidget = QtGui.QWidget()
283                 centralWidget.setLayout(self._layout)
284
285                 self._window = QtGui.QMainWindow(parent)
286                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
287                 #maeqt.set_autorient(self._window, True)
288                 maeqt.set_stackable(self._window, True)
289                 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
290                 self._window.setCentralWidget(centralWidget)
291                 self._window.destroyed.connect(self._on_close_window)
292
293                 self._copyItemAction = QtGui.QAction(None)
294                 self._copyItemAction.setText("Copy")
295                 self._copyItemAction.setShortcut(QtGui.QKeySequence("CTRL+c"))
296                 self._copyItemAction.triggered.connect(self._on_copy)
297
298                 self._pasteItemAction = QtGui.QAction(None)
299                 self._pasteItemAction.setText("Paste")
300                 self._pasteItemAction.setShortcut(QtGui.QKeySequence("CTRL+v"))
301                 self._pasteItemAction.triggered.connect(self._on_paste)
302
303                 self._closeWindowAction = QtGui.QAction(None)
304                 self._closeWindowAction.setText("Close")
305                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
306                 self._closeWindowAction.triggered.connect(self._on_close_window)
307
308                 if IS_MAEMO:
309                         #fileMenu = self._window.menuBar().addMenu("&File")
310
311                         #viewMenu = self._window.menuBar().addMenu("&View")
312
313                         self._window.addAction(self._copyItemAction)
314                         self._window.addAction(self._pasteItemAction)
315                         self._window.addAction(self._closeWindowAction)
316                         self._window.addAction(self._app.quitAction)
317                         self._window.addAction(self._app.fullscreenAction)
318                 else:
319                         fileMenu = self._window.menuBar().addMenu("&Units")
320                         fileMenu.addAction(self._closeWindowAction)
321                         fileMenu.addAction(self._app.quitAction)
322
323                         viewMenu = self._window.menuBar().addMenu("&View")
324                         viewMenu.addAction(self._app.fullscreenAction)
325
326                 self._window.addAction(self._app.logAction)
327
328                 self._constantPlugins = plugin_utils.ConstantPluginManager()
329                 self._constantPlugins.add_path(*self._plugin_search_paths)
330                 for pluginName in ["Builtins", "Trigonometry", "Computer", "Alphabet"]:
331                         try:
332                                 pluginId = self._constantPlugins.lookup_plugin(pluginName)
333                                 self._constantPlugins.enable_plugin(pluginId)
334                         except:
335                                 _moduleLogger.info("Failed to load plugin %s" % pluginName)
336
337                 self._operatorPlugins = plugin_utils.OperatorPluginManager()
338                 self._operatorPlugins.add_path(*self._plugin_search_paths)
339                 for pluginName in ["Builtins", "Trigonometry", "Computer", "Alphabet"]:
340                         try:
341                                 pluginId = self._operatorPlugins.lookup_plugin(pluginName)
342                                 self._operatorPlugins.enable_plugin(pluginId)
343                         except:
344                                 _moduleLogger.info("Failed to load plugin %s" % pluginName)
345
346                 self._keyboardPlugins = plugin_utils.KeyboardPluginManager()
347                 self._keyboardPlugins.add_path(*self._plugin_search_paths)
348                 self._activeKeyboards = []
349
350                 self._history = history.RpnCalcHistory(
351                         self._historyView,
352                         self._userEntry, self._errorDisplay,
353                         self._constantPlugins.constants, self._operatorPlugins.operators
354                 )
355                 self._load_history()
356
357                 # Basic keyboard stuff
358                 self._handler = qtpieboard.KeyboardHandler(self._on_entry_direct)
359                 self._handler.register_command_handler("push", self._on_push)
360                 self._handler.register_command_handler("unpush", self._on_unpush)
361                 self._handler.register_command_handler("backspace", self._on_entry_backspace)
362                 self._handler.register_command_handler("clear", self._on_entry_clear)
363
364                 # Main keyboard
365                 entryKeyboardId = self._keyboardPlugins.lookup_plugin("Entry")
366                 self._keyboardPlugins.enable_plugin(entryKeyboardId)
367                 entryPlugin = self._keyboardPlugins.keyboards["Entry"].construct_keyboard()
368                 entryKeyboard = entryPlugin.setup(self._history, self._handler)
369                 self._userEntryLayout.addWidget(entryKeyboard.toplevel)
370
371                 # Plugins
372                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Builtins"))
373                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Trigonometry"))
374                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Computer"))
375                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Alphabet"))
376
377                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
378
379                 self._window.show()
380                 self._set_plugin_kb(0)
381
382         @property
383         def window(self):
384                 return self._window
385
386         def walk_children(self):
387                 return ()
388
389         def show(self):
390                 self._window.show()
391                 for child in self.walk_children():
392                         child.show()
393
394         def hide(self):
395                 for child in self.walk_children():
396                         child.hide()
397                 self._window.hide()
398
399         def close(self):
400                 for child in self.walk_children():
401                         child.window.destroyed.disconnect(self._on_child_close)
402                         child.close()
403                 self._window.close()
404
405         def set_fullscreen(self, isFullscreen):
406                 if isFullscreen:
407                         self._window.showFullScreen()
408                 else:
409                         self._window.showNormal()
410                 for child in self.walk_children():
411                         child.set_fullscreen(isFullscreen)
412
413         def enable_plugin(self, pluginId):
414                 self._keyboardPlugins.enable_plugin(pluginId)
415                 pluginData = self._keyboardPlugins.plugin_info(pluginId)
416                 pluginName = pluginData[0]
417                 plugin = self._keyboardPlugins.keyboards[pluginName].construct_keyboard()
418                 relIcon = self._keyboardPlugins.keyboards[pluginName].icon
419                 for iconPath in self._keyboardPlugins.keyboards[pluginName].iconPaths:
420                         absIconPath = os.path.join(iconPath, relIcon)
421                         if os.path.exists(absIconPath):
422                                 icon = QtGui.QIcon(absIconPath)
423                                 break
424                 else:
425                         icon = None
426                 pluginKeyboard = plugin.setup(self._history, self._handler)
427
428                 self._activeKeyboards.append({
429                         "pluginName": pluginName,
430                         "plugin": plugin,
431                         "pluginKeyboard": pluginKeyboard,
432                 })
433                 if icon is None:
434                         self._keyboardTabs.addTab(pluginKeyboard.toplevel, pluginName)
435                 else:
436                         self._keyboardTabs.addTab(pluginKeyboard.toplevel, icon, "")
437
438         def _set_plugin_kb(self, pluginIndex):
439                 plugin = self._activeKeyboards[pluginIndex]
440
441                 # @todo Switch the keyboard tab
442                 pluginKeyboard = plugin["pluginKeyboard"]
443
444         def _load_history(self):
445                 serialized = []
446                 try:
447                         with open(self._user_history, "rU") as f:
448                                 serialized = (
449                                         (part.strip() for part in line.split(" "))
450                                         for line in f.readlines()
451                                 )
452                 except IOError, e:
453                         if e.errno != 2:
454                                 raise
455                 self._history.deserialize_stack(serialized)
456
457         def _save_history(self):
458                 serialized = self._history.serialize_stack()
459                 with open(self._user_history, "w") as f:
460                         for lineData in serialized:
461                                 line = " ".join(data for data in lineData)
462                                 f.write("%s\n" % line)
463
464         @misc_utils.log_exception(_moduleLogger)
465         def _on_copy(self, *args):
466                 eqNode = self._historyView.peek()
467                 resultNode = eqNode.simplify()
468                 self._app._clipboard.setText(str(resultNode))
469
470         @misc_utils.log_exception(_moduleLogger)
471         def _on_paste(self, *args):
472                 result = str(self._app._clipboard.text())
473                 self._userEntry.append(result)
474
475         @misc_utils.log_exception(_moduleLogger)
476         def _on_entry_direct(self, keys, modifiers):
477                 if "shift" in modifiers:
478                         keys = keys.upper()
479                 self._userEntry.append(keys)
480
481         @misc_utils.log_exception(_moduleLogger)
482         def _on_push(self, *args):
483                 self._history.push_entry()
484
485         @misc_utils.log_exception(_moduleLogger)
486         def _on_unpush(self, *args):
487                 self._historyView.unpush()
488
489         @misc_utils.log_exception(_moduleLogger)
490         def _on_entry_backspace(self, *args):
491                 self._userEntry.pop()
492
493         @misc_utils.log_exception(_moduleLogger)
494         def _on_entry_clear(self, *args):
495                 self._userEntry.clear()
496
497         @misc_utils.log_exception(_moduleLogger)
498         def _on_clear_all(self, *args):
499                 self._history.clear()
500
501         @misc_utils.log_exception(_moduleLogger)
502         def _on_close_window(self, checked = True):
503                 self._save_history()
504
505
506 def run():
507         app = QtGui.QApplication([])
508         handle = Calculator(app)
509         qtpie.init_pies()
510         return app.exec_()
511
512
513 if __name__ == "__main__":
514         logging.basicConfig(level = logging.DEBUG)
515         try:
516                 os.makedirs(constants._data_path_)
517         except OSError, e:
518                 if e.errno != 17:
519                         raise
520
521         val = run()
522         sys.exit(val)