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