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