Adding support to persist the current ab
[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                 except Exception:
74                         _moduleLogger.exception("Unknown loading error")
75
76                 blobs = "", ""
77                 isFullscreen = False
78                 tabIndex = 0
79                 try:
80                         blobs = (
81                                 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
82                                 for i in xrange(len(self._mainWindow.get_default_credentials()))
83                         )
84                         isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
85                         tabIndex = config.getint(constants.__pretty_app_name__, "tab")
86                 except ConfigParser.NoOptionError, e:
87                         _moduleLogger.info(
88                                 "Settings file %s is missing option %s" % (
89                                         constants._user_settings_,
90                                         e.option,
91                                 ),
92                         )
93                 except ConfigParser.NoSectionError, e:
94                         _moduleLogger.info(
95                                 "Settings file %s is missing section %s" % (
96                                         constants._user_settings_,
97                                         e.section,
98                                 ),
99                         )
100                         return
101                 except Exception:
102                         _moduleLogger.exception("Unknown loading error")
103                         return
104
105                 creds = (
106                         base64.b64decode(blob)
107                         for blob in blobs
108                 )
109                 self._mainWindow.set_default_credentials(*creds)
110                 self._fullscreenAction.setChecked(isFullscreen)
111                 self._mainWindow.set_current_tab(tabIndex)
112                 self._mainWindow.load_settings(config)
113
114         def save_settings(self):
115                 config = ConfigParser.SafeConfigParser()
116
117                 config.add_section(constants.__pretty_app_name__)
118                 config.set(constants.__pretty_app_name__, "tab", str(self._mainWindow.get_current_tab()))
119                 config.set(constants.__pretty_app_name__, "fullscreen", str(self._fullscreenAction.isChecked()))
120                 for i, value in enumerate(self._mainWindow.get_default_credentials()):
121                         blob = base64.b64encode(value)
122                         config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
123
124                 self._mainWindow.save_settings(config)
125
126                 with open(constants._user_settings_, "wb") as configFile:
127                         config.write(configFile)
128
129         @property
130         def fsContactsPath(self):
131                 return os.path.join(constants._data_path_, "contacts")
132
133         @property
134         def fullscreenAction(self):
135                 return self._fullscreenAction
136
137         @property
138         def logAction(self):
139                 return self._logAction
140
141         @property
142         def quitAction(self):
143                 return self._quitAction
144
145         def _close_windows(self):
146                 if self._mainWindow is not None:
147                         self._mainWindow.window.destroyed.disconnect(self._on_child_close)
148                         self._mainWindow.close()
149                         self._mainWindow = None
150
151         @QtCore.pyqtSlot()
152         @QtCore.pyqtSlot(bool)
153         @misc_utils.log_exception(_moduleLogger)
154         def _on_app_quit(self, checked = False):
155                 self.save_settings()
156                 self._mainWindow.destroy()
157
158         @QtCore.pyqtSlot(QtCore.QObject)
159         @misc_utils.log_exception(_moduleLogger)
160         def _on_child_close(self, obj = None):
161                 self._mainWindow = None
162
163         @QtCore.pyqtSlot()
164         @QtCore.pyqtSlot(bool)
165         @misc_utils.log_exception(_moduleLogger)
166         def _on_toggle_fullscreen(self, checked = False):
167                 for window in self._walk_children():
168                         window.set_fullscreen(checked)
169
170         @QtCore.pyqtSlot()
171         @QtCore.pyqtSlot(bool)
172         @misc_utils.log_exception(_moduleLogger)
173         def _on_log(self, checked = False):
174                 with open(constants._user_logpath_, "r") as f:
175                         logLines = f.xreadlines()
176                         log = "".join(logLines)
177                         self._clipboard.setText(log)
178
179         @QtCore.pyqtSlot()
180         @QtCore.pyqtSlot(bool)
181         @misc_utils.log_exception(_moduleLogger)
182         def _on_quit(self, checked = False):
183                 self._close_windows()
184
185
186 class DelayedWidget(object):
187
188         def __init__(self, app, settingsNames):
189                 self._layout = QtGui.QVBoxLayout()
190                 self._widget = QtGui.QWidget()
191                 self._widget.setLayout(self._layout)
192                 self._settings = dict((name, "") for name in settingsNames)
193
194                 self._child = None
195                 self._isEnabled = True
196
197         @property
198         def toplevel(self):
199                 return self._widget
200
201         def has_child(self):
202                 return self._child is not None
203
204         def set_child(self, child):
205                 if self._child is not None:
206                         self._layout.removeWidget(self._child.toplevel)
207                 self._child = child
208                 if self._child is not None:
209                         self._layout.addWidget(self._child.toplevel)
210
211                 self._child.set_settings(self._settings)
212
213                 if self._isEnabled:
214                         self._child.enable()
215                 else:
216                         self._child.disable()
217
218         def enable(self):
219                 self._isEnabled = True
220                 if self._child is not None:
221                         self._child.enable()
222
223         def disable(self):
224                 self._isEnabled = False
225                 if self._child is not None:
226                         self._child.disable()
227
228         def clear(self):
229                 if self._child is not None:
230                         self._child.clear()
231
232         def refresh(self, force=True):
233                 if self._child is not None:
234                         self._child.refresh(force)
235
236         def get_settings(self):
237                 if self._child is not None:
238                         return self._child.get_settings()
239                 else:
240                         return self._settings
241
242         def set_settings(self, settings):
243                 if self._child is not None:
244                         self._child.set_settings(settings)
245                 else:
246                         self._settings = settings
247
248
249 def _tab_factory(tab, app, session, errorLog):
250         import gv_views
251         return gv_views.__dict__[tab](app, session, errorLog)
252
253
254 class MainWindow(object):
255
256         KEYPAD_TAB = 0
257         RECENT_TAB = 1
258         MESSAGES_TAB = 2
259         CONTACTS_TAB = 3
260         MAX_TABS = 4
261
262         _TAB_TITLES = [
263                 "Dialpad",
264                 "History",
265                 "Messages",
266                 "Contacts",
267         ]
268         assert len(_TAB_TITLES) == MAX_TABS
269
270         _TAB_CLASS = [
271                 functools.partial(_tab_factory, "Dialpad"),
272                 functools.partial(_tab_factory, "History"),
273                 functools.partial(_tab_factory, "Messages"),
274                 functools.partial(_tab_factory, "Contacts"),
275         ]
276         assert len(_TAB_CLASS) == MAX_TABS
277
278         # Hack to allow delay importing/loading of tabs
279         _TAB_SETTINGS_NAMES = [
280                 (),
281                 ("filter", ),
282                 ("status", "type"),
283                 ("selectedAddressbook", ),
284         ]
285         assert len(_TAB_SETTINGS_NAMES) == MAX_TABS
286
287         def __init__(self, parent, app):
288                 self._app = app
289                 self._session = session.Session(constants._data_path_)
290                 self._session.error.connect(self._on_session_error)
291                 self._session.loggedIn.connect(self._on_login)
292                 self._session.loggedOut.connect(self._on_logout)
293                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
294                 self._defaultCredentials = "", ""
295                 self._curentCredentials = "", ""
296
297                 self._credentialsDialog = None
298                 self._smsEntryDialog = None
299                 self._accountDialog = None
300
301                 self._errorLog = qui_utils.QErrorLog()
302                 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
303
304                 self._tabsContents = [
305                         DelayedWidget(self._app, self._TAB_SETTINGS_NAMES[i])
306                         for i in xrange(self.MAX_TABS)
307                 ]
308                 for tab in self._tabsContents:
309                         tab.disable()
310
311                 self._tabWidget = QtGui.QTabWidget()
312                 if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
313                         self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
314                 else:
315                         self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
316                 for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
317                         self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
318                 self._tabWidget.currentChanged.connect(self._on_tab_changed)
319
320                 self._layout = QtGui.QVBoxLayout()
321                 self._layout.addWidget(self._errorDisplay.toplevel)
322                 self._layout.addWidget(self._tabWidget)
323
324                 centralWidget = QtGui.QWidget()
325                 centralWidget.setLayout(self._layout)
326
327                 self._window = QtGui.QMainWindow(parent)
328                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
329                 qui_utils.set_autorient(self._window, True)
330                 qui_utils.set_stackable(self._window, True)
331                 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
332                 self._window.setCentralWidget(centralWidget)
333
334                 self._loginTabAction = QtGui.QAction(None)
335                 self._loginTabAction.setText("Login")
336                 self._loginTabAction.triggered.connect(self._on_login_requested)
337
338                 self._importTabAction = QtGui.QAction(None)
339                 self._importTabAction.setText("Import")
340                 self._importTabAction.triggered.connect(self._on_import)
341
342                 self._accountTabAction = QtGui.QAction(None)
343                 self._accountTabAction.setText("Account")
344                 self._accountTabAction.triggered.connect(self._on_account)
345
346                 self._refreshTabAction = QtGui.QAction(None)
347                 self._refreshTabAction.setText("Refresh")
348                 self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
349                 self._refreshTabAction.triggered.connect(self._on_refresh)
350
351                 self._closeWindowAction = QtGui.QAction(None)
352                 self._closeWindowAction.setText("Close")
353                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
354                 self._closeWindowAction.triggered.connect(self._on_close_window)
355
356                 if IS_MAEMO:
357                         fileMenu = self._window.menuBar().addMenu("&File")
358                         fileMenu.addAction(self._loginTabAction)
359                         fileMenu.addAction(self._refreshTabAction)
360
361                         toolsMenu = self._window.menuBar().addMenu("&Tools")
362                         toolsMenu.addAction(self._accountTabAction)
363                         toolsMenu.addAction(self._importTabAction)
364
365                         self._window.addAction(self._closeWindowAction)
366                         self._window.addAction(self._app.quitAction)
367                         self._window.addAction(self._app.fullscreenAction)
368                 else:
369                         fileMenu = self._window.menuBar().addMenu("&File")
370                         fileMenu.addAction(self._loginTabAction)
371                         fileMenu.addAction(self._refreshTabAction)
372                         fileMenu.addAction(self._closeWindowAction)
373                         fileMenu.addAction(self._app.quitAction)
374
375                         viewMenu = self._window.menuBar().addMenu("&View")
376                         viewMenu.addAction(self._app.fullscreenAction)
377
378                         toolsMenu = self._window.menuBar().addMenu("&Tools")
379                         toolsMenu.addAction(self._accountTabAction)
380                         toolsMenu.addAction(self._importTabAction)
381
382                 self._window.addAction(self._app.logAction)
383
384                 self._initialize_tab(self._tabWidget.currentIndex())
385                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
386
387         @property
388         def window(self):
389                 return self._window
390
391         def set_default_credentials(self, username, password):
392                 self._defaultCredentials = username, password
393
394         def get_default_credentials(self):
395                 return self._defaultCredentials
396
397         def walk_children(self):
398                 return ()
399
400         def start(self):
401                 assert self._session.state == self._session.LOGGEDOUT_STATE
402                 self.show()
403                 if self._defaultCredentials != ("", ""):
404                         username, password = self._defaultCredentials[0], self._defaultCredentials[1]
405                         self._curentCredentials = username, password
406                         self._session.login(username, password)
407                 else:
408                         self._prompt_for_login()
409
410         def close(self):
411                 for child in self.walk_children():
412                         child.window.destroyed.disconnect(self._on_child_close)
413                         child.close()
414                 self._window.close()
415
416         def destroy(self):
417                 if self._session.state != self._session.LOGGEDOUT_STATE:
418                         self._session.logout()
419
420         def get_current_tab(self):
421                 return self._tabWidget.currentIndex()
422
423         def set_current_tab(self, tabIndex):
424                 self._tabWidget.setCurrentIndex(tabIndex)
425
426         def load_settings(self, config):
427                 backendId = 2 # For backwards compatibility
428                 for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
429                         sectionName = "%s - %s" % (backendId, tabTitle)
430                         settings = self._tabsContents[tabIndex].get_settings()
431                         for settingName in settings.iterkeys():
432                                 try:
433                                         settingValue = config.get(sectionName, settingName)
434                                 except ConfigParser.NoOptionError, e:
435                                         _moduleLogger.info(
436                                                 "Settings file %s is missing section %s" % (
437                                                         constants._user_settings_,
438                                                         e.section,
439                                                 ),
440                                         )
441                                         return
442                                 except ConfigParser.NoSectionError, e:
443                                         _moduleLogger.info(
444                                                 "Settings file %s is missing section %s" % (
445                                                         constants._user_settings_,
446                                                         e.section,
447                                                 ),
448                                         )
449                                         return
450                                 except Exception:
451                                         _moduleLogger.exception("Unknown loading error")
452                                         return
453                                 settings[settingName] = settingValue
454                         self._tabsContents[tabIndex].set_settings(settings)
455
456         def save_settings(self, config):
457                 backendId = 2 # For backwards compatibility
458                 for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
459                         sectionName = "%s - %s" % (backendId, tabTitle)
460                         config.add_section(sectionName)
461                         tabSettings = self._tabsContents[tabIndex].get_settings()
462                         for settingName, settingValue in tabSettings.iteritems():
463                                 config.set(sectionName, settingName, settingValue)
464
465         def show(self):
466                 self._window.show()
467                 for child in self.walk_children():
468                         child.show()
469
470         def hide(self):
471                 for child in self.walk_children():
472                         child.hide()
473                 self._window.hide()
474
475         def set_fullscreen(self, isFullscreen):
476                 if isFullscreen:
477                         self._window.showFullScreen()
478                 else:
479                         self._window.showNormal()
480                 for child in self.walk_children():
481                         child.set_fullscreen(isFullscreen)
482
483         def _initialize_tab(self, index):
484                 assert index < self.MAX_TABS
485                 if not self._tabsContents[index].has_child():
486                         tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
487                         self._tabsContents[index].set_child(tab)
488                         self._tabsContents[index].refresh(force=False)
489
490         def _prompt_for_login(self):
491                 if self._credentialsDialog is None:
492                         import dialogs
493                         self._credentialsDialog = dialogs.CredentialsDialog()
494                 username, password = self._credentialsDialog.run(
495                         self._defaultCredentials[0], self._defaultCredentials[1], self.window
496                 )
497                 self._curentCredentials = username, password
498                 self._session.login(username, password)
499
500         def _show_account_dialog(self):
501                 if self._accountDialog is None:
502                         import dialogs
503                         self._accountDialog = dialogs.AccountDialog()
504                 self._accountDialog.accountNumber = self._session.get_account_number()
505                 response = self._accountDialog.run()
506                 if response == QtGui.QDialog.Accepted:
507                         if self._accountDialog.doClear():
508                                 self._session.logout_and_clear()
509                 elif response == QtGui.QDialog.Rejected:
510                         _moduleLogger.info("Cancelled")
511                 else:
512                         _moduleLogger.info("Unknown response")
513
514         @QtCore.pyqtSlot(str)
515         @misc_utils.log_exception(_moduleLogger)
516         def _on_session_error(self, message):
517                 self._errorLog.push_message(message)
518
519         @QtCore.pyqtSlot()
520         @misc_utils.log_exception(_moduleLogger)
521         def _on_login(self):
522                 if self._defaultCredentials != self._curentCredentials:
523                         self._show_account_dialog()
524                 self._defaultCredentials = self._curentCredentials
525                 for tab in self._tabsContents:
526                         tab.enable()
527
528         @QtCore.pyqtSlot()
529         @misc_utils.log_exception(_moduleLogger)
530         def _on_logout(self):
531                 for tab in self._tabsContents:
532                         tab.disable()
533
534         @QtCore.pyqtSlot()
535         @misc_utils.log_exception(_moduleLogger)
536         def _on_recipients_changed(self):
537                 if self._session.draft.get_num_contacts() == 0:
538                         return
539
540                 if self._smsEntryDialog is None:
541                         import dialogs
542                         self._smsEntryDialog = dialogs.SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
543                 pass
544
545         @QtCore.pyqtSlot()
546         @QtCore.pyqtSlot(bool)
547         @misc_utils.log_exception(_moduleLogger)
548         def _on_login_requested(self, checked = True):
549                 self._prompt_for_login()
550
551         @QtCore.pyqtSlot(int)
552         @misc_utils.log_exception(_moduleLogger)
553         def _on_tab_changed(self, index):
554                 self._initialize_tab(index)
555
556         @QtCore.pyqtSlot()
557         @QtCore.pyqtSlot(bool)
558         @misc_utils.log_exception(_moduleLogger)
559         def _on_refresh(self, checked = True):
560                 index = self._tabWidget.currentIndex()
561                 self._tabsContents[index].refresh(force=True)
562
563         @QtCore.pyqtSlot()
564         @QtCore.pyqtSlot(bool)
565         @misc_utils.log_exception(_moduleLogger)
566         def _on_import(self, checked = True):
567                 csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
568                 if not csvName:
569                         return
570                 import shutil
571                 shutil.copy2(csvName, self._app.fsContactsPath)
572                 self._tabsContents[self.CONTACTS_TAB].update_addressbooks()
573
574         @QtCore.pyqtSlot()
575         @QtCore.pyqtSlot(bool)
576         @misc_utils.log_exception(_moduleLogger)
577         def _on_account(self, checked = True):
578                 self._show_account_dialog()
579
580         @QtCore.pyqtSlot()
581         @QtCore.pyqtSlot(bool)
582         @misc_utils.log_exception(_moduleLogger)
583         def _on_close_window(self, checked = True):
584                 self.close()
585
586
587 def run():
588         app = QtGui.QApplication([])
589         handle = Dialcentral(app)
590         qtpie.init_pies()
591         return app.exec_()
592
593
594 if __name__ == "__main__":
595         import sys
596
597         logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
598         logging.basicConfig(level=logging.DEBUG, format=logFormat)
599         try:
600                 os.makedirs(constants._data_path_)
601         except OSError, e:
602                 if e.errno != 17:
603                         raise
604
605         val = run()
606         sys.exit(val)