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