Switching to the built-in json module, with a fallback to the external one
[ejpi] / ejpi / ejpi_qt.py
1 #!/usr/bin/env python
2 # -*- coding: UTF8 -*-
3
4 from __future__ import with_statement
5
6 import os
7 try:
8         import json as simplejson
9 except ImportError:
10         print "json not available, falling back to simplejson"
11         import simplejson
12 import string
13 import logging
14 import logging.handlers
15
16 import util.qt_compat as qt_compat
17 QtCore = qt_compat.QtCore
18 QtGui = qt_compat.import_module("QtGui")
19
20 import constants
21 from util import misc as misc_utils
22 from util import linux as linux_utils
23
24 from util import qui_utils
25 from util import qwrappers
26 from util import qtpie
27 from util import qtpieboard
28 import plugin_utils
29 import history
30 import qhistory
31
32
33 _moduleLogger = logging.getLogger(__name__)
34
35
36 class Calculator(qwrappers.ApplicationWrapper):
37
38         def __init__(self, app):
39                 self._recent = []
40                 self._hiddenCategories = set()
41                 self._hiddenUnits = {}
42                 qwrappers.ApplicationWrapper.__init__(self, app, constants)
43
44         def load_settings(self):
45                 settingsPath = linux_utils.get_resource_path(
46                         "config", constants.__app_name__, "settings.json"
47                 )
48                 try:
49                         with open(settingsPath) as settingsFile:
50                                 settings = simplejson.load(settingsFile)
51                 except IOError, e:
52                         _moduleLogger.info("No settings")
53                         settings = {}
54                 except ValueError:
55                         _moduleLogger.info("Settings were corrupt")
56                         settings = {}
57
58                 isPortraitDefault = qui_utils.screen_orientation() == QtCore.Qt.Vertical
59                 self._fullscreenAction.setChecked(settings.get("isFullScreen", False))
60                 self._orientationAction.setChecked(settings.get("isPortrait", isPortraitDefault))
61
62         def save_settings(self):
63                 settings = {
64                         "isFullScreen": self._fullscreenAction.isChecked(),
65                         "isPortrait": self._orientationAction.isChecked(),
66                 }
67
68                 settingsPath = linux_utils.get_resource_path(
69                         "config", constants.__app_name__, "settings.json"
70                 )
71                 with open(settingsPath, "w") as settingsFile:
72                         simplejson.dump(settings, settingsFile)
73
74         @property
75         def dataPath(self):
76                 return self._dataPath
77
78         def _new_main_window(self):
79                 return MainWindow(None, self)
80
81         @misc_utils.log_exception(_moduleLogger)
82         def _on_about(self, checked = True):
83                 raise NotImplementedError("Booh")
84
85
86 class QValueEntry(object):
87
88         def __init__(self):
89                 self._widget = QtGui.QLineEdit("")
90                 qui_utils.mark_numbers_preferred(self._widget)
91
92         @property
93         def toplevel(self):
94                 return self._widget
95
96         @property
97         def entry(self):
98                 return self._widget
99
100         def get_value(self):
101                 value = str(self._widget.text()).strip()
102                 if any(
103                         0 < value.find(whitespace)
104                         for whitespace in string.whitespace
105                 ):
106                         self.clear()
107                         raise ValueError('Invalid input "%s"' % value)
108                 return value
109
110         def set_value(self, value):
111                 value = value.strip()
112                 if any(
113                         0 < value.find(whitespace)
114                         for whitespace in string.whitespace
115                 ):
116                         raise ValueError('Invalid input "%s"' % value)
117                 self._widget.setText(value)
118
119         def append(self, value):
120                 value = value.strip()
121                 if any(
122                         0 < value.find(whitespace)
123                         for whitespace in string.whitespace
124                 ):
125                         raise ValueError('Invalid input "%s"' % value)
126                 self.set_value(self.get_value() + value)
127
128         def pop(self):
129                 value = self.get_value()[0:-1]
130                 self.set_value(value)
131
132         def clear(self):
133                 self.set_value("")
134
135         value = property(get_value, set_value, clear)
136
137
138 class MainWindow(qwrappers.WindowWrapper):
139
140         _plugin_search_paths = [
141                 os.path.join(os.path.dirname(__file__), "plugins/"),
142         ]
143
144         _user_history = linux_utils.get_resource_path("config", constants.__app_name__, "history.stack")
145
146         def __init__(self, parent, app):
147                 qwrappers.WindowWrapper.__init__(self, parent, app)
148                 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
149                 #self._freezer = qwrappers.AutoFreezeWindowFeature(self._app, self._window)
150
151                 self._historyView = qhistory.QCalcHistory(self._app.errorLog)
152                 self._userEntry = QValueEntry()
153                 self._userEntry.entry.returnPressed.connect(self._on_push)
154                 self._userEntryLayout = QtGui.QHBoxLayout()
155                 self._userEntryLayout.setContentsMargins(0, 0, 0, 0)
156                 self._userEntryLayout.addWidget(self._userEntry.toplevel, 10)
157
158                 self._controlLayout = QtGui.QVBoxLayout()
159                 self._controlLayout.setContentsMargins(0, 0, 0, 0)
160                 self._controlLayout.addWidget(self._historyView.toplevel, 1000)
161                 self._controlLayout.addLayout(self._userEntryLayout, 0)
162
163                 self._keyboardTabs = QtGui.QTabWidget()
164
165                 self._layout.addLayout(self._controlLayout)
166                 self._layout.addWidget(self._keyboardTabs)
167
168                 self._copyItemAction = QtGui.QAction(None)
169                 self._copyItemAction.setText("Copy")
170                 self._copyItemAction.setShortcut(QtGui.QKeySequence("CTRL+c"))
171                 self._copyItemAction.triggered.connect(self._on_copy)
172
173                 self._pasteItemAction = QtGui.QAction(None)
174                 self._pasteItemAction.setText("Paste")
175                 self._pasteItemAction.setShortcut(QtGui.QKeySequence("CTRL+v"))
176                 self._pasteItemAction.triggered.connect(self._on_paste)
177
178                 self._closeWindowAction = QtGui.QAction(None)
179                 self._closeWindowAction.setText("Close")
180                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
181                 self._closeWindowAction.triggered.connect(self._on_close_window)
182
183                 self._window.addAction(self._copyItemAction)
184                 self._window.addAction(self._pasteItemAction)
185
186                 self._constantPlugins = plugin_utils.ConstantPluginManager()
187                 self._constantPlugins.add_path(*self._plugin_search_paths)
188                 for pluginName in ["Builtins", "Trigonometry", "Computer", "Alphabet"]:
189                         try:
190                                 pluginId = self._constantPlugins.lookup_plugin(pluginName)
191                                 self._constantPlugins.enable_plugin(pluginId)
192                         except:
193                                 _moduleLogger.exception("Failed to load plugin %s" % pluginName)
194
195                 self._operatorPlugins = plugin_utils.OperatorPluginManager()
196                 self._operatorPlugins.add_path(*self._plugin_search_paths)
197                 for pluginName in ["Builtins", "Trigonometry", "Computer", "Alphabet"]:
198                         try:
199                                 pluginId = self._operatorPlugins.lookup_plugin(pluginName)
200                                 self._operatorPlugins.enable_plugin(pluginId)
201                         except:
202                                 _moduleLogger.exception("Failed to load plugin %s" % pluginName)
203
204                 self._keyboardPlugins = plugin_utils.KeyboardPluginManager()
205                 self._keyboardPlugins.add_path(*self._plugin_search_paths)
206                 self._activeKeyboards = []
207
208                 self._history = history.RpnCalcHistory(
209                         self._historyView,
210                         self._userEntry, self._app.errorLog,
211                         self._constantPlugins.constants, self._operatorPlugins.operators
212                 )
213                 self._load_history()
214
215                 # Basic keyboard stuff
216                 self._handler = qtpieboard.KeyboardHandler(self._on_entry_direct)
217                 self._handler.register_command_handler("push", self._on_push)
218                 self._handler.register_command_handler("unpush", self._on_unpush)
219                 self._handler.register_command_handler("backspace", self._on_entry_backspace)
220                 self._handler.register_command_handler("clear", self._on_entry_clear)
221
222                 # Main keyboard
223                 entryKeyboardId = self._keyboardPlugins.lookup_plugin("Entry")
224                 self._keyboardPlugins.enable_plugin(entryKeyboardId)
225                 entryPlugin = self._keyboardPlugins.keyboards["Entry"].construct_keyboard()
226                 entryKeyboard = entryPlugin.setup(self._history, self._handler)
227                 self._userEntryLayout.addWidget(entryKeyboard.toplevel)
228
229                 # Plugins
230                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Builtins"))
231                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Trigonometry"))
232                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Computer"))
233                 self.enable_plugin(self._keyboardPlugins.lookup_plugin("Alphabet"))
234
235                 self._scrollTimer = QtCore.QTimer()
236                 self._scrollTimer.setInterval(0)
237                 self._scrollTimer.setSingleShot(True)
238                 self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
239                 self._scrollTimer.start()
240
241                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
242                 self.update_orientation(self._app.orientation)
243
244         def walk_children(self):
245                 return ()
246
247         def update_orientation(self, orientation):
248                 qwrappers.WindowWrapper.update_orientation(self, orientation)
249                 windowOrientation = self.idealWindowOrientation
250                 if windowOrientation == QtCore.Qt.Horizontal:
251                         defaultLayoutOrientation = QtGui.QBoxLayout.LeftToRight
252                         tabPosition = QtGui.QTabWidget.North
253                 else:
254                         defaultLayoutOrientation = QtGui.QBoxLayout.TopToBottom
255                         #tabPosition = QtGui.QTabWidget.South
256                         tabPosition = QtGui.QTabWidget.West
257                 self._layout.setDirection(defaultLayoutOrientation)
258                 self._keyboardTabs.setTabPosition(tabPosition)
259
260         def enable_plugin(self, pluginId):
261                 self._keyboardPlugins.enable_plugin(pluginId)
262                 pluginData = self._keyboardPlugins.plugin_info(pluginId)
263                 pluginName = pluginData[0]
264                 plugin = self._keyboardPlugins.keyboards[pluginName].construct_keyboard()
265                 relIcon = self._keyboardPlugins.keyboards[pluginName].icon
266                 for iconPath in self._keyboardPlugins.keyboards[pluginName].iconPaths:
267                         absIconPath = os.path.join(iconPath, relIcon)
268                         if os.path.exists(absIconPath):
269                                 icon = QtGui.QIcon(absIconPath)
270                                 break
271                 else:
272                         icon = None
273                 pluginKeyboard = plugin.setup(self._history, self._handler)
274
275                 self._activeKeyboards.append({
276                         "pluginName": pluginName,
277                         "plugin": plugin,
278                         "pluginKeyboard": pluginKeyboard,
279                 })
280                 if icon is None:
281                         self._keyboardTabs.addTab(pluginKeyboard.toplevel, pluginName)
282                 else:
283                         self._keyboardTabs.addTab(pluginKeyboard.toplevel, icon, "")
284
285         def close(self):
286                 qwrappers.WindowWrapper.close(self)
287                 self._save_history()
288
289         def _load_history(self):
290                 serialized = []
291                 try:
292                         with open(self._user_history, "rU") as f:
293                                 serialized = (
294                                         (part.strip() for part in line.split(" "))
295                                         for line in f.readlines()
296                                 )
297                 except IOError, e:
298                         if e.errno != 2:
299                                 raise
300                 self._history.deserialize_stack(serialized)
301
302         def _save_history(self):
303                 serialized = self._history.serialize_stack()
304                 with open(self._user_history, "w") as f:
305                         for lineData in serialized:
306                                 line = " ".join(data for data in lineData)
307                                 f.write("%s\n" % line)
308
309         @misc_utils.log_exception(_moduleLogger)
310         def _on_delayed_scroll_to_bottom(self):
311                 with qui_utils.notify_error(self._app.errorLog):
312                         self._historyView.scroll_to_bottom()
313
314         @misc_utils.log_exception(_moduleLogger)
315         def _on_child_close(self, something = None):
316                 with qui_utils.notify_error(self._app.errorLog):
317                         self._child = None
318
319         @misc_utils.log_exception(_moduleLogger)
320         def _on_copy(self, *args):
321                 with qui_utils.notify_error(self._app.errorLog):
322                         eqNode = self._historyView.peek()
323                         resultNode = eqNode.simplify()
324                         self._app._clipboard.setText(str(resultNode))
325
326         @misc_utils.log_exception(_moduleLogger)
327         def _on_paste(self, *args):
328                 with qui_utils.notify_error(self._app.errorLog):
329                         result = str(self._app._clipboard.text())
330                         self._userEntry.append(result)
331
332         @misc_utils.log_exception(_moduleLogger)
333         def _on_entry_direct(self, keys, modifiers):
334                 with qui_utils.notify_error(self._app.errorLog):
335                         if "shift" in modifiers:
336                                 keys = keys.upper()
337                         self._userEntry.append(keys)
338
339         @misc_utils.log_exception(_moduleLogger)
340         def _on_push(self, *args):
341                 with qui_utils.notify_error(self._app.errorLog):
342                         self._history.push_entry()
343
344         @misc_utils.log_exception(_moduleLogger)
345         def _on_unpush(self, *args):
346                 with qui_utils.notify_error(self._app.errorLog):
347                         self._historyView.unpush()
348
349         @misc_utils.log_exception(_moduleLogger)
350         def _on_entry_backspace(self, *args):
351                 with qui_utils.notify_error(self._app.errorLog):
352                         self._userEntry.pop()
353
354         @misc_utils.log_exception(_moduleLogger)
355         def _on_entry_clear(self, *args):
356                 with qui_utils.notify_error(self._app.errorLog):
357                         self._userEntry.clear()
358
359         @misc_utils.log_exception(_moduleLogger)
360         def _on_clear_all(self, *args):
361                 with qui_utils.notify_error(self._app.errorLog):
362                         self._history.clear()
363
364
365 def run():
366         try:
367                 os.makedirs(linux_utils.get_resource_path("config", constants.__app_name__))
368         except OSError, e:
369                 if e.errno != 17:
370                         raise
371         try:
372                 os.makedirs(linux_utils.get_resource_path("cache", constants.__app_name__))
373         except OSError, e:
374                 if e.errno != 17:
375                         raise
376         try:
377                 os.makedirs(linux_utils.get_resource_path("data", constants.__app_name__))
378         except OSError, e:
379                 if e.errno != 17:
380                         raise
381
382         logPath = linux_utils.get_resource_path("cache", constants.__app_name__, "%s.log" % constants.__app_name__)
383         logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
384         logging.basicConfig(level=logging.DEBUG, format=logFormat)
385         rotating = logging.handlers.RotatingFileHandler(logPath, maxBytes=512*1024, backupCount=1)
386         rotating.setFormatter(logging.Formatter(logFormat))
387         root = logging.getLogger()
388         root.addHandler(rotating)
389         _moduleLogger.info("%s %s-%s" % (constants.__app_name__, constants.__version__, constants.__build__))
390         _moduleLogger.info("OS: %s" % (os.uname()[0], ))
391         _moduleLogger.info("Kernel: %s (%s) for %s" % os.uname()[2:])
392         _moduleLogger.info("Hostname: %s" % os.uname()[1])
393
394         app = QtGui.QApplication([])
395         handle = Calculator(app)
396         qtpie.init_pies()
397         return app.exec_()
398
399
400 if __name__ == "__main__":
401         import sys
402         val = run()
403         sys.exit(val)