Auto-login support
[gc-dialer] / src / dialcentral_qt.py
1 #!/usr/bin/env python
2 # -*- coding: UTF8 -*-
3
4 from __future__ import with_statement
5
6 import os
7 import base64
8 import ConfigParser
9 import functools
10 import logging
11
12 from PyQt4 import QtGui
13 from PyQt4 import QtCore
14
15 import constants
16 from util import qtpie
17 from util import qui_utils
18 from util import misc as misc_utils
19
20 import session
21
22
23 _moduleLogger = logging.getLogger(__name__)
24 IS_MAEMO = True
25
26
27 class Dialcentral(object):
28
29         def __init__(self, app):
30                 self._app = app
31                 self._recent = []
32                 self._hiddenCategories = set()
33                 self._hiddenUnits = {}
34                 self._clipboard = QtGui.QApplication.clipboard()
35
36                 self._mainWindow = None
37
38                 self._fullscreenAction = QtGui.QAction(None)
39                 self._fullscreenAction.setText("Fullscreen")
40                 self._fullscreenAction.setCheckable(True)
41                 self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
42                 self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
43
44                 self._logAction = QtGui.QAction(None)
45                 self._logAction.setText("Log")
46                 self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l"))
47                 self._logAction.triggered.connect(self._on_log)
48
49                 self._quitAction = QtGui.QAction(None)
50                 self._quitAction.setText("Quit")
51                 self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q"))
52                 self._quitAction.triggered.connect(self._on_quit)
53
54                 self._app.lastWindowClosed.connect(self._on_app_quit)
55                 self._mainWindow = MainWindow(None, self)
56                 self._mainWindow.window.destroyed.connect(self._on_child_close)
57                 self.load_settings()
58                 self._mainWindow.start()
59
60         def load_settings(self):
61                 try:
62                         config = ConfigParser.SafeConfigParser()
63                         config.read(constants._user_settings_)
64                 except IOError, e:
65                         _moduleLogger.info("No settings")
66                         return
67                 except ValueError:
68                         _moduleLogger.info("Settings were corrupt")
69                         return
70                 except ConfigParser.MissingSectionHeaderError:
71                         _moduleLogger.info("Settings were corrupt")
72                         return
73
74                 try:
75                         blobs = (
76                                 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
77                                 for i in xrange(len(self._mainWindow.get_default_credentials()))
78                         )
79                         isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
80                 except ConfigParser.NoOptionError, e:
81                         _moduleLogger.exception(
82                                 "Settings file %s is missing section %s" % (
83                                         constants._user_settings_,
84                                         e.section,
85                                 ),
86                         )
87                         return
88                 except ConfigParser.NoSectionError, e:
89                         _moduleLogger.exception(
90                                 "Settings file %s is missing section %s" % (
91                                         constants._user_settings_,
92                                         e.section,
93                                 ),
94                         )
95                         return
96
97                 creds = (
98                         base64.b64decode(blob)
99                         for blob in blobs
100                 )
101                 self._mainWindow.set_default_credentials(*creds)
102                 self._fullscreenAction.setChecked(isFullscreen)
103
104         def save_settings(self):
105                 config = ConfigParser.SafeConfigParser()
106
107                 config.add_section(constants.__pretty_app_name__)
108                 config.set(constants.__pretty_app_name__, "fullscreen", str(self._fullscreenAction.isChecked()))
109                 for i, value in enumerate(self._mainWindow.get_default_credentials()):
110                         blob = base64.b64encode(value)
111                         config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
112
113                 with open(constants._user_settings_, "wb") as configFile:
114                         config.write(configFile)
115
116         @property
117         def fsContactsPath(self):
118                 return os.path.join(constants._data_path_, "contacts")
119
120         @property
121         def fullscreenAction(self):
122                 return self._fullscreenAction
123
124         @property
125         def logAction(self):
126                 return self._logAction
127
128         @property
129         def quitAction(self):
130                 return self._quitAction
131
132         def _close_windows(self):
133                 if self._mainWindow is not None:
134                         self._mainWindow.window.destroyed.disconnect(self._on_child_close)
135                         self._mainWindow.close()
136                         self._mainWindow = None
137
138         @QtCore.pyqtSlot()
139         @QtCore.pyqtSlot(bool)
140         @misc_utils.log_exception(_moduleLogger)
141         def _on_app_quit(self, checked = False):
142                 self.save_settings()
143                 self._mainWindow.destroy()
144
145         @QtCore.pyqtSlot(QtCore.QObject)
146         @misc_utils.log_exception(_moduleLogger)
147         def _on_child_close(self, obj = None):
148                 self._mainWindow = None
149
150         @QtCore.pyqtSlot()
151         @QtCore.pyqtSlot(bool)
152         @misc_utils.log_exception(_moduleLogger)
153         def _on_toggle_fullscreen(self, checked = False):
154                 for window in self._walk_children():
155                         window.set_fullscreen(checked)
156
157         @QtCore.pyqtSlot()
158         @QtCore.pyqtSlot(bool)
159         @misc_utils.log_exception(_moduleLogger)
160         def _on_log(self, checked = False):
161                 with open(constants._user_logpath_, "r") as f:
162                         logLines = f.xreadlines()
163                         log = "".join(logLines)
164                         self._clipboard.setText(log)
165
166         @QtCore.pyqtSlot()
167         @QtCore.pyqtSlot(bool)
168         @misc_utils.log_exception(_moduleLogger)
169         def _on_quit(self, checked = False):
170                 self._close_windows()
171
172
173 class DelayedWidget(object):
174
175         def __init__(self, app):
176                 self._layout = QtGui.QVBoxLayout()
177                 self._widget = QtGui.QWidget()
178                 self._widget.setLayout(self._layout)
179
180                 self._child = None
181                 self._isEnabled = True
182
183         @property
184         def toplevel(self):
185                 return self._widget
186
187         def has_child(self):
188                 return self._child is not None
189
190         def set_child(self, child):
191                 if self._child is not None:
192                         self._layout.removeWidget(self._child.toplevel)
193                 self._child = child
194                 if self._child is not None:
195                         self._layout.addWidget(self._child.toplevel)
196
197                 if self._isEnabled:
198                         self._child.enable()
199                 else:
200                         self._child.disable()
201
202         def enable(self):
203                 self._isEnabled = True
204                 if self._child is not None:
205                         self._child.enable()
206
207         def disable(self):
208                 self._isEnabled = False
209                 if self._child is not None:
210                         self._child.disable()
211
212         def clear(self):
213                 if self._child is not None:
214                         self._child.clear()
215
216         def refresh(self, force=True):
217                 if self._child is not None:
218                         self._child.refresh(force)
219
220
221 def _tab_factory(tab, app, session, errorLog):
222         import gv_views
223         return gv_views.__dict__[tab](app, session, errorLog)
224
225
226 class MainWindow(object):
227
228         KEYPAD_TAB = 0
229         RECENT_TAB = 1
230         MESSAGES_TAB = 2
231         CONTACTS_TAB = 3
232         MAX_TABS = 4
233
234         _TAB_TITLES = [
235                 "Dialpad",
236                 "History",
237                 "Messages",
238                 "Contacts",
239         ]
240         assert len(_TAB_TITLES) == MAX_TABS
241
242         _TAB_CLASS = [
243                 functools.partial(_tab_factory, "Dialpad"),
244                 functools.partial(_tab_factory, "History"),
245                 functools.partial(_tab_factory, "Messages"),
246                 functools.partial(_tab_factory, "Contacts"),
247         ]
248         assert len(_TAB_CLASS) == MAX_TABS
249
250         def __init__(self, parent, app):
251                 self._app = app
252                 self._session = session.Session(constants._data_path_)
253                 self._session.error.connect(self._on_session_error)
254                 self._session.loggedIn.connect(self._on_login)
255                 self._session.loggedOut.connect(self._on_logout)
256                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
257                 self._defaultCredentials = "", ""
258                 self._curentCredentials = "", ""
259
260                 self._credentialsDialog = None
261                 self._smsEntryDialog = None
262                 self._accountDialog = None
263
264                 self._errorLog = qui_utils.QErrorLog()
265                 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
266
267                 self._tabsContents = [
268                         DelayedWidget(self._app)
269                         for i in xrange(self.MAX_TABS)
270                 ]
271                 for tab in self._tabsContents:
272                         tab.disable()
273
274                 self._tabWidget = QtGui.QTabWidget()
275                 if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
276                         self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
277                 else:
278                         self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
279                 for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
280                         self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
281                 self._tabWidget.currentChanged.connect(self._on_tab_changed)
282
283                 self._layout = QtGui.QVBoxLayout()
284                 self._layout.addWidget(self._errorDisplay.toplevel)
285                 self._layout.addWidget(self._tabWidget)
286
287                 centralWidget = QtGui.QWidget()
288                 centralWidget.setLayout(self._layout)
289
290                 self._window = QtGui.QMainWindow(parent)
291                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
292                 qui_utils.set_autorient(self._window, True)
293                 qui_utils.set_stackable(self._window, True)
294                 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
295                 self._window.setCentralWidget(centralWidget)
296
297                 self._loginTabAction = QtGui.QAction(None)
298                 self._loginTabAction.setText("Login")
299                 self._loginTabAction.triggered.connect(self._on_login_requested)
300
301                 self._importTabAction = QtGui.QAction(None)
302                 self._importTabAction.setText("Import")
303                 self._importTabAction.triggered.connect(self._on_import)
304
305                 self._accountTabAction = QtGui.QAction(None)
306                 self._accountTabAction.setText("Account")
307                 self._accountTabAction.triggered.connect(self._on_account)
308
309                 self._refreshTabAction = QtGui.QAction(None)
310                 self._refreshTabAction.setText("Refresh")
311                 self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
312                 self._refreshTabAction.triggered.connect(self._on_refresh)
313
314                 self._closeWindowAction = QtGui.QAction(None)
315                 self._closeWindowAction.setText("Close")
316                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
317                 self._closeWindowAction.triggered.connect(self._on_close_window)
318
319                 if IS_MAEMO:
320                         fileMenu = self._window.menuBar().addMenu("&File")
321                         fileMenu.addAction(self._loginTabAction)
322                         fileMenu.addAction(self._refreshTabAction)
323
324                         toolsMenu = self._window.menuBar().addMenu("&Tools")
325                         toolsMenu.addAction(self._accountTabAction)
326                         toolsMenu.addAction(self._importTabAction)
327
328                         self._window.addAction(self._closeWindowAction)
329                         self._window.addAction(self._app.quitAction)
330                         self._window.addAction(self._app.fullscreenAction)
331                 else:
332                         fileMenu = self._window.menuBar().addMenu("&File")
333                         fileMenu.addAction(self._loginTabAction)
334                         fileMenu.addAction(self._refreshTabAction)
335                         fileMenu.addAction(self._closeWindowAction)
336                         fileMenu.addAction(self._app.quitAction)
337
338                         viewMenu = self._window.menuBar().addMenu("&View")
339                         viewMenu.addAction(self._app.fullscreenAction)
340
341                         toolsMenu = self._window.menuBar().addMenu("&Tools")
342                         toolsMenu.addAction(self._accountTabAction)
343                         toolsMenu.addAction(self._importTabAction)
344
345                 self._window.addAction(self._app.logAction)
346
347                 self._initialize_tab(self._tabWidget.currentIndex())
348                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
349
350         @property
351         def window(self):
352                 return self._window
353
354         def set_default_credentials(self, username, password):
355                 self._defaultCredentials = username, password
356
357         def get_default_credentials(self):
358                 return self._defaultCredentials
359
360         def walk_children(self):
361                 return ()
362
363         def show(self):
364                 self._window.show()
365                 for child in self.walk_children():
366                         child.show()
367
368         def start(self):
369                 assert self._session.state == self._session.LOGGEDOUT_STATE
370                 self.show()
371                 if self._defaultCredentials != ("", ""):
372                         username, password = self._defaultCredentials[0], self._defaultCredentials[1]
373                         self._curentCredentials = username, password
374                         self._session.login(username, password)
375                 else:
376                         self._prompt_for_login()
377
378         def hide(self):
379                 for child in self.walk_children():
380                         child.hide()
381                 self._window.hide()
382
383         def close(self):
384                 for child in self.walk_children():
385                         child.window.destroyed.disconnect(self._on_child_close)
386                         child.close()
387                 self._window.close()
388
389         def destroy(self):
390                 if self._session.state != self._session.LOGGEDOUT_STATE:
391                         self._session.logout()
392
393         def set_fullscreen(self, isFullscreen):
394                 if isFullscreen:
395                         self._window.showFullScreen()
396                 else:
397                         self._window.showNormal()
398                 for child in self.walk_children():
399                         child.set_fullscreen(isFullscreen)
400
401         def _initialize_tab(self, index):
402                 assert index < self.MAX_TABS
403                 if not self._tabsContents[index].has_child():
404                         tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
405                         self._tabsContents[index].set_child(tab)
406                         self._tabsContents[index].refresh(force=False)
407
408         def _prompt_for_login(self):
409                 if self._credentialsDialog is None:
410                         import dialogs
411                         self._credentialsDialog = dialogs.CredentialsDialog()
412                 username, password = self._credentialsDialog.run(
413                         self._defaultCredentials[0], self._defaultCredentials[1], self.window
414                 )
415                 self._curentCredentials = username, password
416                 self._session.login(username, password)
417
418         def _show_account_dialog(self):
419                 if self._accountDialog is None:
420                         import dialogs
421                         self._accountDialog = dialogs.AccountDialog()
422                 self._accountDialog.accountNumber = self._session.get_account_number()
423                 response = self._accountDialog.run()
424                 if response == QtGui.QDialog.Accepted:
425                         if self._accountDialog.doClear():
426                                 self._session.logout_and_clear()
427                 elif response == QtGui.QDialog.Rejected:
428                         _moduleLogger.info("Cancelled")
429                 else:
430                         _moduleLogger.info("Unknown response")
431
432         @QtCore.pyqtSlot(str)
433         @misc_utils.log_exception(_moduleLogger)
434         def _on_session_error(self, message):
435                 self._errorLog.push_message(message)
436
437         @QtCore.pyqtSlot()
438         @misc_utils.log_exception(_moduleLogger)
439         def _on_login(self):
440                 if self._defaultCredentials != self._curentCredentials:
441                         self._show_account_dialog()
442                 self._defaultCredentials = self._curentCredentials
443                 for tab in self._tabsContents:
444                         tab.enable()
445
446         @QtCore.pyqtSlot()
447         @misc_utils.log_exception(_moduleLogger)
448         def _on_logout(self):
449                 for tab in self._tabsContents:
450                         tab.disable()
451
452         @QtCore.pyqtSlot()
453         @misc_utils.log_exception(_moduleLogger)
454         def _on_recipients_changed(self):
455                 if self._session.draft.get_num_contacts() == 0:
456                         return
457
458                 if self._smsEntryDialog is None:
459                         import dialogs
460                         self._smsEntryDialog = dialogs.SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
461                 pass
462
463         @QtCore.pyqtSlot()
464         @QtCore.pyqtSlot(bool)
465         @misc_utils.log_exception(_moduleLogger)
466         def _on_login_requested(self, checked = True):
467                 self._prompt_for_login()
468
469         @QtCore.pyqtSlot(int)
470         @misc_utils.log_exception(_moduleLogger)
471         def _on_tab_changed(self, index):
472                 self._initialize_tab(index)
473
474         @QtCore.pyqtSlot()
475         @QtCore.pyqtSlot(bool)
476         @misc_utils.log_exception(_moduleLogger)
477         def _on_refresh(self, checked = True):
478                 index = self._tabWidget.currentIndex()
479                 self._tabsContents[index].refresh(force=True)
480
481         @QtCore.pyqtSlot()
482         @QtCore.pyqtSlot(bool)
483         @misc_utils.log_exception(_moduleLogger)
484         def _on_import(self, checked = True):
485                 csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
486                 if not csvName:
487                         return
488                 import shutil
489                 shutil.copy2(csvName, self._app.fsContactsPath)
490                 self._tabsContents[self.CONTACTS_TAB].update_addressbooks()
491
492         @QtCore.pyqtSlot()
493         @QtCore.pyqtSlot(bool)
494         @misc_utils.log_exception(_moduleLogger)
495         def _on_account(self, checked = True):
496                 self._show_account_dialog()
497
498         @QtCore.pyqtSlot()
499         @QtCore.pyqtSlot(bool)
500         @misc_utils.log_exception(_moduleLogger)
501         def _on_close_window(self, checked = True):
502                 self.close()
503
504
505 def run():
506         app = QtGui.QApplication([])
507         handle = Dialcentral(app)
508         qtpie.init_pies()
509         return app.exec_()
510
511
512 if __name__ == "__main__":
513         import sys
514
515         logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
516         logging.basicConfig(level=logging.DEBUG, format=logFormat)
517         try:
518                 os.makedirs(constants._data_path_)
519         except OSError, e:
520                 if e.errno != 17:
521                         raise
522
523         val = run()
524         sys.exit(val)