Copy and paste, as well as rotation prep
[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                 maeqt.mark_numbers_preferred(self._widget)
200
201         @property
202         def toplevel(self):
203                 return self._widget
204
205         @property
206         def entry(self):
207                 return self._widget
208
209         def get_value(self):
210                 value = str(self._widget.text()).strip()
211                 if any(
212                         0 < value.find(whitespace)
213                         for whitespace in string.whitespace
214                 ):
215                         self.clear()
216                         raise ValueError('Invalid input "%s"' % value)
217                 return value
218
219         def set_value(self, value):
220                 value = value.strip()
221                 if any(
222                         0 < value.find(whitespace)
223                         for whitespace in string.whitespace
224                 ):
225                         raise ValueError('Invalid input "%s"' % value)
226                 self._widget.setText(value)
227
228         def append(self, value):
229                 value = value.strip()
230                 if any(
231                         0 < value.find(whitespace)
232                         for whitespace in string.whitespace
233                 ):
234                         raise ValueError('Invalid input "%s"' % value)
235                 self.set_value(self.get_value() + value)
236
237         def pop(self):
238                 value = self.get_value()[0:-1]
239                 self.set_value(value)
240
241         def clear(self):
242                 self.set_value("")
243
244         value = property(get_value, set_value, clear)
245
246
247 class MainWindow(object):
248
249         _plugin_search_paths = [
250                 "/opt/epi/lib/plugins/",
251                 "/usr/lib/ejpi/plugins/",
252                 os.path.join(os.path.dirname(__file__), "plugins/"),
253         ]
254
255         _user_history = "%s/history.stack" % constants._data_path_
256
257         def __init__(self, parent, app):
258                 self._app = app
259
260                 self._errorDisplay = QErrorDisplay()
261                 self._historyView = qhistory.QCalcHistory(self._errorDisplay)
262                 self._userEntry = QValueEntry()
263                 self._userEntry.entry.returnPressed.connect(self._on_push)
264                 self._userEntryLayout = QtGui.QHBoxLayout()
265                 self._userEntryLayout.addWidget(self._userEntry.toplevel)
266
267                 self._controlLayout = QtGui.QVBoxLayout()
268                 self._controlLayout.addLayout(self._errorDisplay.toplevel)
269                 self._controlLayout.addWidget(self._historyView.toplevel)
270                 self._controlLayout.addLayout(self._userEntryLayout)
271
272                 self._inputLayout = QtGui.QVBoxLayout()
273
274                 self._layout = QtGui.QHBoxLayout()
275                 self._layout.addLayout(self._controlLayout)
276                 self._layout.addLayout(self._inputLayout)
277
278                 centralWidget = QtGui.QWidget()
279                 centralWidget.setLayout(self._layout)
280
281                 QtGui.QApplication.desktop().resized.connect(self._on_desktop_size_change)
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_close_window)
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                 if IS_MAEMO:
306                         #fileMenu = self._window.menuBar().addMenu("&File")
307
308                         #viewMenu = self._window.menuBar().addMenu("&View")
309
310                         self._window.addAction(self._copyItemAction)
311                         self._window.addAction(self._pasteItemAction)
312                         self._window.addAction(self._closeWindowAction)
313                         self._window.addAction(self._app.quitAction)
314                         self._window.addAction(self._app.fullscreenAction)
315                 else:
316                         fileMenu = self._window.menuBar().addMenu("&Units")
317                         fileMenu.addAction(self._closeWindowAction)
318                         fileMenu.addAction(self._app.quitAction)
319
320                         viewMenu = self._window.menuBar().addMenu("&View")
321                         viewMenu.addAction(self._app.fullscreenAction)
322
323                 self._window.addAction(self._app.logAction)
324
325                 self._constantPlugins = plugin_utils.ConstantPluginManager()
326                 self._constantPlugins.add_path(*self._plugin_search_paths)
327                 for pluginName in ["Builtins", "Trigonometry", "Computer", "Alphabet"]:
328                         try:
329                                 pluginId = self._constantPlugins.lookup_plugin(pluginName)
330                                 self._constantPlugins.enable_plugin(pluginId)
331                         except:
332                                 _moduleLogger.info("Failed to load plugin %s" % pluginName)
333
334                 self._operatorPlugins = plugin_utils.OperatorPluginManager()
335                 self._operatorPlugins.add_path(*self._plugin_search_paths)
336                 for pluginName in ["Builtins", "Trigonometry", "Computer", "Alphabet"]:
337                         try:
338                                 pluginId = self._operatorPlugins.lookup_plugin(pluginName)
339                                 self._operatorPlugins.enable_plugin(pluginId)
340                         except:
341                                 _moduleLogger.info("Failed to load plugin %s" % pluginName)
342
343                 self._keyboardPlugins = plugin_utils.KeyboardPluginManager()
344                 self._keyboardPlugins.add_path(*self._plugin_search_paths)
345                 self._activeKeyboards = []
346
347                 self._history = history.RpnCalcHistory(
348                         self._historyView,
349                         self._userEntry, self._errorDisplay,
350                         self._constantPlugins.constants, self._operatorPlugins.operators
351                 )
352                 self._load_history()
353
354                 # Basic keyboard stuff
355                 self._handler = qtpieboard.KeyboardHandler(self._on_entry_direct)
356                 self._handler.register_command_handler("push", self._on_push)
357                 self._handler.register_command_handler("unpush", self._on_unpush)
358                 self._handler.register_command_handler("backspace", self._on_entry_backspace)
359                 self._handler.register_command_handler("clear", self._on_entry_clear)
360
361                 # Main keyboard
362                 builtinKeyboardId = self._keyboardPlugins.lookup_plugin("Builtins")
363                 self._keyboardPlugins.enable_plugin(builtinKeyboardId)
364                 self._builtinPlugin = self._keyboardPlugins.keyboards["Builtins"].construct_keyboard()
365                 self._builtinKeyboard = self._builtinPlugin.setup(self._history, self._handler)
366
367                 entryKeyboardId = self._keyboardPlugins.lookup_plugin("Entry")
368                 self._keyboardPlugins.enable_plugin(entryKeyboardId)
369                 entryPlugin = self._keyboardPlugins.keyboards["Entry"].construct_keyboard()
370                 entryKeyboard = entryPlugin.setup(self._history, self._handler)
371                 self._userEntryLayout.addLayout(entryKeyboard.toplevel)
372
373                 # Plugins
374                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Trigonometry"))
375                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Computer"))
376                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Alphabet"))
377                 self._inputLayout.addLayout(self._builtinKeyboard.toplevel)
378                 for keyboardData in self._activeKeyboards:
379                         keyboardData["pluginKeyboard"].hide()
380                 self._set_plugin_kb(0)
381
382                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
383                 self._window.show()
384
385         @property
386         def window(self):
387                 return self._window
388
389         def walk_children(self):
390                 return ()
391
392         def show(self):
393                 self._window.show()
394                 for child in self.walk_children():
395                         child.show()
396
397         def hide(self):
398                 for child in self.walk_children():
399                         child.hide()
400                 self._window.hide()
401
402         def close(self):
403                 for child in self.walk_children():
404                         child.window.destroyed.disconnect(self._on_child_close)
405                         child.close()
406                 self._window.close()
407
408         def set_fullscreen(self, isFullscreen):
409                 if isFullscreen:
410                         self._window.showFullScreen()
411                 else:
412                         self._window.showNormal()
413                 for child in self.walk_children():
414                         child.set_fullscreen(isFullscreen)
415
416         def enable_plugin(self, pluginId):
417                 self._keyboardPlugins.enable_plugin(pluginId)
418                 pluginData = self._keyboardPlugins.plugin_info(pluginId)
419                 pluginName = pluginData[0]
420                 plugin = self._keyboardPlugins.keyboards[pluginName].construct_keyboard()
421                 pluginKeyboard = plugin.setup(self._history, self._handler)
422
423                 self._activeKeyboards.append({
424                         "pluginName": pluginName,
425                         "plugin": plugin,
426                         "pluginKeyboard": pluginKeyboard,
427                 })
428                 self._inputLayout.addLayout(pluginKeyboard.toplevel)
429
430         def _set_plugin_kb(self, pluginIndex):
431                 plugin = self._activeKeyboards[pluginIndex]
432
433                 for keyboardData in self._activeKeyboards:
434                         if plugin["pluginName"] != keyboardData["pluginName"]:
435                                 keyboardData["pluginKeyboard"].hide()
436
437                 # @todo self._pluginButton.set_label(plugin["pluginName"])
438                 pluginKeyboard = plugin["pluginKeyboard"]
439                 pluginKeyboard.show()
440
441         def _load_history(self):
442                 serialized = []
443                 try:
444                         with open(self._user_history, "rU") as f:
445                                 serialized = (
446                                         (part.strip() for part in line.split(" "))
447                                         for line in f.readlines()
448                                 )
449                 except IOError, e:
450                         if e.errno != 2:
451                                 raise
452                 self._history.deserialize_stack(serialized)
453
454         def _save_history(self):
455                 serialized = self._history.serialize_stack()
456                 with open(self._user_history, "w") as f:
457                         for lineData in serialized:
458                                 line = " ".join(data for data in lineData)
459                                 f.write("%s\n" % line)
460
461         @misc_utils.log_exception(_moduleLogger)
462         def _on_desktop_size_change(self):
463                 print QtGui.QApplication.desktop().screenGeometry()
464
465         @misc_utils.log_exception(_moduleLogger)
466         def _on_copy(self, *args):
467                 eqNode = self._historyView.peek()
468                 resultNode = eqNode.simplify()
469                 self._app._clipboard.setText(str(resultNode))
470
471         @misc_utils.log_exception(_moduleLogger)
472         def _on_paste(self, *args):
473                 result = str(self._app._clipboard.text())
474                 self._userEntry.append(result)
475
476         @misc_utils.log_exception(_moduleLogger)
477         def _on_entry_direct(self, keys, modifiers):
478                 if "shift" in modifiers:
479                         keys = keys.upper()
480                 self._userEntry.append(keys)
481
482         @misc_utils.log_exception(_moduleLogger)
483         def _on_push(self, *args):
484                 self._history.push_entry()
485
486         @misc_utils.log_exception(_moduleLogger)
487         def _on_unpush(self, *args):
488                 self._historyStore.unpush()
489
490         @misc_utils.log_exception(_moduleLogger)
491         def _on_entry_backspace(self, *args):
492                 self._userEntry.pop()
493
494         @misc_utils.log_exception(_moduleLogger)
495         def _on_entry_clear(self, *args):
496                 self._userEntry.clear()
497
498         @misc_utils.log_exception(_moduleLogger)
499         def _on_clear_all(self, *args):
500                 self._history.clear()
501
502         @misc_utils.log_exception(_moduleLogger)
503         def _on_close_window(self, checked = True):
504                 self._save_history()
505
506
507 def run():
508         app = QtGui.QApplication([])
509         handle = Calculator(app)
510         qtpie.init_pies()
511         return app.exec_()
512
513
514 if __name__ == "__main__":
515         logging.basicConfig(level = logging.DEBUG)
516         try:
517                 os.makedirs(constants._data_path_)
518         except OSError, e:
519                 if e.errno != 17:
520                         raise
521
522         val = run()
523         sys.exit(val)