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