Maemo 4.1 has an older Qt without fromTheme, so let's abstract that away and switch...
[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 = maeqt.get_theme_icon(("dialog-error", "app_install_error", "gtk-dialog-error"))
133                 self._severityIcon = errorIcon.pixmap(32, 32)
134                 self._severityLabel = QtGui.QLabel()
135                 self._severityLabel.setPixmap(self._severityIcon)
136
137                 self._message = QtGui.QLabel()
138                 self._message.setText("Boo")
139
140                 closeIcon = maeqt.get_theme_icon(("window-close", "general_close", "gtk-close"))
141                 self._closeLabel = QtGui.QPushButton(closeIcon, "")
142                 self._closeLabel.clicked.connect(self._on_close)
143
144                 self._controlLayout = QtGui.QHBoxLayout()
145                 self._controlLayout.addWidget(self._severityLabel)
146                 self._controlLayout.addWidget(self._message)
147                 self._controlLayout.addWidget(self._closeLabel)
148
149                 self._topLevelLayout = QtGui.QHBoxLayout()
150                 self._topLevelLayout.addLayout(self._controlLayout)
151                 self._widget = QtGui.QWidget()
152                 self._widget.setLayout(self._topLevelLayout)
153                 self._hide_message()
154
155         @property
156         def toplevel(self):
157                 return self._widget
158
159         def push_message(self, message):
160                 self._messages.append(message)
161                 if 1 == len(self._messages):
162                         self._show_message(message)
163
164         def push_exception(self):
165                 userMessage = str(sys.exc_info()[1])
166                 _moduleLogger.exception(userMessage)
167                 self.push_message(userMessage)
168
169         def pop_message(self):
170                 del self._messages[0]
171                 if 0 == len(self._messages):
172                         self._hide_message()
173                 else:
174                         self._message.setText(self._messages[0])
175
176         def _on_close(self, *args):
177                 self.pop_message()
178
179         def _show_message(self, message):
180                 self._message.setText(message)
181                 self._widget.show()
182
183         def _hide_message(self):
184                 self._message.setText("")
185                 self._widget.hide()
186
187
188 class QValueEntry(object):
189
190         def __init__(self):
191                 self._widget = QtGui.QLineEdit("")
192                 maeqt.mark_numbers_preferred(self._widget)
193
194         @property
195         def toplevel(self):
196                 return self._widget
197
198         @property
199         def entry(self):
200                 return self._widget
201
202         def get_value(self):
203                 value = str(self._widget.text()).strip()
204                 if any(
205                         0 < value.find(whitespace)
206                         for whitespace in string.whitespace
207                 ):
208                         self.clear()
209                         raise ValueError('Invalid input "%s"' % value)
210                 return value
211
212         def set_value(self, value):
213                 value = value.strip()
214                 if any(
215                         0 < value.find(whitespace)
216                         for whitespace in string.whitespace
217                 ):
218                         raise ValueError('Invalid input "%s"' % 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                 os.path.join(os.path.dirname(__file__), "plugins/"),
244         ]
245
246         _user_history = "%s/history.stack" % constants._data_path_
247
248         def __init__(self, parent, app):
249                 self._app = app
250
251                 self._errorDisplay = QErrorDisplay()
252                 self._historyView = qhistory.QCalcHistory(self._errorDisplay)
253                 self._userEntry = QValueEntry()
254                 self._userEntry.entry.returnPressed.connect(self._on_push)
255                 self._userEntryLayout = QtGui.QHBoxLayout()
256                 self._userEntryLayout.addWidget(self._userEntry.toplevel)
257
258                 self._controlLayout = QtGui.QVBoxLayout()
259                 self._controlLayout.addWidget(self._errorDisplay.toplevel)
260                 self._controlLayout.addWidget(self._historyView.toplevel)
261                 self._controlLayout.addLayout(self._userEntryLayout)
262
263                 self._keyboardTabs = QtGui.QTabWidget()
264
265                 if maeqt.screen_orientation() == QtCore.Qt.Vertical:
266                         defaultLayoutOrientation = QtGui.QBoxLayout.TopToBottom
267                         self._keyboardTabs.setTabPosition(QtGui.QTabWidget.East)
268                 else:
269                         defaultLayoutOrientation = QtGui.QBoxLayout.LeftToRight
270                         self._keyboardTabs.setTabPosition(QtGui.QTabWidget.North)
271                 self._layout = QtGui.QBoxLayout(defaultLayoutOrientation)
272                 self._layout.addLayout(self._controlLayout)
273                 self._layout.addWidget(self._keyboardTabs)
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_window_closed)
285
286                 self._copyItemAction = QtGui.QAction(None)
287                 self._copyItemAction.setText("Copy")
288                 self._copyItemAction.setShortcut(QtGui.QKeySequence("CTRL+c"))
289                 self._copyItemAction.triggered.connect(self._on_copy)
290
291                 self._pasteItemAction = QtGui.QAction(None)
292                 self._pasteItemAction.setText("Paste")
293                 self._pasteItemAction.setShortcut(QtGui.QKeySequence("CTRL+v"))
294                 self._pasteItemAction.triggered.connect(self._on_paste)
295
296                 self._closeWindowAction = QtGui.QAction(None)
297                 self._closeWindowAction.setText("Close")
298                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
299                 self._closeWindowAction.triggered.connect(self._on_close_window)
300
301                 self._window.addAction(self._copyItemAction)
302                 self._window.addAction(self._pasteItemAction)
303                 self._window.addAction(self._closeWindowAction)
304                 self._window.addAction(self._app.quitAction)
305                 self._window.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 ["Builtins", "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 ["Builtins", "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                 entryKeyboardId = self._keyboardPlugins.lookup_plugin("Entry")
347                 self._keyboardPlugins.enable_plugin(entryKeyboardId)
348                 entryPlugin = self._keyboardPlugins.keyboards["Entry"].construct_keyboard()
349                 entryKeyboard = entryPlugin.setup(self._history, self._handler)
350                 self._userEntryLayout.addWidget(entryKeyboard.toplevel)
351
352                 # Plugins
353                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Builtins"))
354                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Trigonometry"))
355                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Computer"))
356                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Alphabet"))
357
358                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
359
360                 self._window.show()
361
362         @property
363         def window(self):
364                 return self._window
365
366         def walk_children(self):
367                 return ()
368
369         def show(self):
370                 self._window.show()
371                 for child in self.walk_children():
372                         child.show()
373
374         def hide(self):
375                 for child in self.walk_children():
376                         child.hide()
377                 self._window.hide()
378
379         def close(self):
380                 for child in self.walk_children():
381                         child.window.destroyed.disconnect(self._on_child_close)
382                         child.close()
383                 self._window.close()
384
385         def set_fullscreen(self, isFullscreen):
386                 if isFullscreen:
387                         self._window.showFullScreen()
388                 else:
389                         self._window.showNormal()
390                 for child in self.walk_children():
391                         child.set_fullscreen(isFullscreen)
392
393         def enable_plugin(self, pluginId):
394                 self._keyboardPlugins.enable_plugin(pluginId)
395                 pluginData = self._keyboardPlugins.plugin_info(pluginId)
396                 pluginName = pluginData[0]
397                 plugin = self._keyboardPlugins.keyboards[pluginName].construct_keyboard()
398                 relIcon = self._keyboardPlugins.keyboards[pluginName].icon
399                 for iconPath in self._keyboardPlugins.keyboards[pluginName].iconPaths:
400                         absIconPath = os.path.join(iconPath, relIcon)
401                         if os.path.exists(absIconPath):
402                                 icon = QtGui.QIcon(absIconPath)
403                                 break
404                 else:
405                         icon = None
406                 pluginKeyboard = plugin.setup(self._history, self._handler)
407
408                 self._activeKeyboards.append({
409                         "pluginName": pluginName,
410                         "plugin": plugin,
411                         "pluginKeyboard": pluginKeyboard,
412                 })
413                 if icon is None:
414                         self._keyboardTabs.addTab(pluginKeyboard.toplevel, pluginName)
415                 else:
416                         self._keyboardTabs.addTab(pluginKeyboard.toplevel, icon, "")
417
418         def _load_history(self):
419                 serialized = []
420                 try:
421                         with open(self._user_history, "rU") as f:
422                                 serialized = (
423                                         (part.strip() for part in line.split(" "))
424                                         for line in f.readlines()
425                                 )
426                 except IOError, e:
427                         if e.errno != 2:
428                                 raise
429                 self._history.deserialize_stack(serialized)
430
431         def _save_history(self):
432                 serialized = self._history.serialize_stack()
433                 with open(self._user_history, "w") as f:
434                         for lineData in serialized:
435                                 line = " ".join(data for data in lineData)
436                                 f.write("%s\n" % line)
437
438         @misc_utils.log_exception(_moduleLogger)
439         def _on_copy(self, *args):
440                 eqNode = self._historyView.peek()
441                 resultNode = eqNode.simplify()
442                 self._app._clipboard.setText(str(resultNode))
443
444         @misc_utils.log_exception(_moduleLogger)
445         def _on_paste(self, *args):
446                 result = str(self._app._clipboard.text())
447                 self._userEntry.append(result)
448
449         @misc_utils.log_exception(_moduleLogger)
450         def _on_entry_direct(self, keys, modifiers):
451                 if "shift" in modifiers:
452                         keys = keys.upper()
453                 self._userEntry.append(keys)
454
455         @misc_utils.log_exception(_moduleLogger)
456         def _on_push(self, *args):
457                 self._history.push_entry()
458
459         @misc_utils.log_exception(_moduleLogger)
460         def _on_unpush(self, *args):
461                 self._historyView.unpush()
462
463         @misc_utils.log_exception(_moduleLogger)
464         def _on_entry_backspace(self, *args):
465                 self._userEntry.pop()
466
467         @misc_utils.log_exception(_moduleLogger)
468         def _on_entry_clear(self, *args):
469                 self._userEntry.clear()
470
471         @misc_utils.log_exception(_moduleLogger)
472         def _on_clear_all(self, *args):
473                 self._history.clear()
474
475         @misc_utils.log_exception(_moduleLogger)
476         def _on_window_closed(self, checked = True):
477                 self._save_history()
478
479         @misc_utils.log_exception(_moduleLogger)
480         def _on_close_window(self, checked = True):
481                 self.close()
482
483
484 def run():
485         app = QtGui.QApplication([])
486         handle = Calculator(app)
487         qtpie.init_pies()
488         return app.exec_()
489
490
491 if __name__ == "__main__":
492         logging.basicConfig(level = logging.DEBUG)
493         try:
494                 os.makedirs(constants._data_path_)
495         except OSError, e:
496                 if e.errno != 17:
497                         raise
498
499         val = run()
500         sys.exit(val)