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