eric6/WebBrowser/Session/SessionManager.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2017 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the session manager.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13 import json
14
15 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QTimer, QDir, \
16 QFile, QFileInfo, QFileSystemWatcher, QByteArray, QDateTime
17 from PyQt5.QtWidgets import QActionGroup, QApplication, \
18 QInputDialog, QLineEdit, QDialog, QDialogButtonBox, QLabel, QComboBox, \
19 QVBoxLayout
20
21 from E5Gui import E5MessageBox
22
23 import Utilities
24 import Preferences
25
26
27 class SessionMetaData(object):
28 """
29 Class implementing a data structure to store meta data for a session.
30 """
31 def __init__(self):
32 """
33 Constructor
34 """
35 self.name = ""
36 self.filePath = ""
37 self.isActive = False
38 self.isDefault = False
39 self.isBackup = False
40
41
42 class SessionManager(QObject):
43 """
44 Class implementing the session manager.
45
46 @signal sessionsMetaDataChanged() emitted to indicate a change of the
47 list of session meta data
48 """
49 sessionsMetaDataChanged = pyqtSignal()
50
51 SwitchSession = 1
52 CloneSession = 2
53 ReplaceSession = SwitchSession | 4
54 RestoreSession = 8
55
56 def __init__(self, parent=None):
57 """
58 Constructor
59
60 @param parent reference to the parent object
61 @type QObject
62 """
63 super(SessionManager, self).__init__(parent)
64
65 sessionsDirName = self.getSessionsDirectory()
66 sessionsDir = QDir(sessionsDirName)
67 if not sessionsDir.exists():
68 sessionsDir.mkpath(sessionsDirName)
69
70 self.__sessionMetaData = []
71 # list containing meta data about saved sessions
72
73 self.__sessionDefault = os.path.join(sessionsDirName, "session.json")
74 self.__sessionBackup1 = os.path.join(sessionsDirName,
75 "session.json.old")
76 self.__sessionBackup2 = os.path.join(sessionsDirName,
77 "session.json.old1")
78
79 self.__lastActiveSession = Preferences.getWebBrowser(
80 "SessionLastActivePath")
81 if not QFile.exists(self.__lastActiveSession):
82 self.__lastActiveSession = self.__sessionDefault
83
84 self.__sessionsDirectoryWatcher = \
85 QFileSystemWatcher([self.getSessionsDirectory()], self)
86 self.__sessionsDirectoryWatcher.directoryChanged.connect(
87 self.__sessionDirectoryChanged)
88
89 self.__backupSavedSession()
90
91 self.__autoSaveTimer = None
92 self.__shutdown = False
93
94 def activateTimer(self):
95 """
96 Public method to activate the session save timer.
97 """
98 if self.__autoSaveTimer is None:
99 self.__autoSaveTimer = QTimer()
100 self.__autoSaveTimer.setSingleShot(True)
101 self.__autoSaveTimer.timeout.connect(self.__autoSaveSession)
102 self.__initSessionSaveTimer()
103
104 def preferencesChanged(self):
105 """
106 Public slot to react upon changes of the settings.
107 """
108 self.__initSessionSaveTimer()
109
110 def getSessionsDirectory(self):
111 """
112 Public method to get the directory sessions are stored in.
113
114 @return name of the sessions directory
115 @rtype str
116 """
117 return os.path.join(Utilities.getConfigDir(),
118 "web_browser", "sessions")
119
120 def defaultSessionFile(self):
121 """
122 Public method to get the name of the default session file.
123
124 @return name of the default session file
125 @rtype str
126 """
127 return self.__sessionDefault
128
129 def lastActiveSessionFile(self):
130 """
131 Public method to get the name of the last active session file.
132
133 @return name of the last active session file
134 @rtype str
135 """
136 return self.__lastActiveSession
137
138 def shutdown(self):
139 """
140 Public method to perform any shutdown actions.
141 """
142 self.__autoSaveTimer.stop()
143 if not self.__shutdown:
144 self.__autoSaveSession(startTimer=False)
145 self.__shutdown = True
146
147 def autoSaveSession(self):
148 """
149 Public method to save the current session state.
150 """
151 self.__autoSaveSession(startTimer=False)
152
153 def __initSessionSaveTimer(self):
154 """
155 Private slot to initialize the auto save timer.
156 """
157 self.__autoSaveInterval = Preferences.getWebBrowser(
158 "SessionAutoSaveInterval") * 1000
159
160 if Preferences.getWebBrowser("SessionAutoSave"):
161 if not self.__autoSaveTimer.isActive():
162 self.__autoSaveTimer.start(self.__autoSaveInterval)
163 else:
164 self.__autoSaveTimer.stop()
165
166 @pyqtSlot()
167 def __autoSaveSession(self, startTimer=True):
168 """
169 Private slot to save the current session state.
170
171 @param startTimer flag indicating to restart the timer
172 @type bool
173 """
174 from WebBrowser.WebBrowserWindow import WebBrowserWindow
175
176 if not WebBrowserWindow.isPrivate():
177 Preferences.setWebBrowser("SessionLastActivePath",
178 self.__lastActiveSession)
179 self.writeCurrentSession(self.__lastActiveSession)
180
181 if startTimer:
182 self.__autoSaveTimer.start(self.__autoSaveInterval)
183
184 def writeCurrentSession(self, sessionFileName):
185 """
186 Public method to write the current session to the given file name.
187
188 @param sessionFileName file name of the session
189 @type str
190 """
191 from WebBrowser.WebBrowserWindow import WebBrowserWindow
192
193 sessionData = {"Windows": []}
194
195 activeWindow = WebBrowserWindow.getWindow()
196 for window in WebBrowserWindow.mainWindows():
197 data = window.tabWidget().getSessionData()
198
199 # add window geometry
200 geometry = window.saveGeometry()
201 data["WindowGeometry"] = bytes(geometry.toBase64()).decode("ascii")
202
203 sessionData["Windows"].append(data)
204
205 if window is activeWindow:
206 sessionData["CurrentWindowIndex"] = \
207 len(sessionData["Windows"]) - 1
208
209 if sessionData["Windows"]:
210 sessionFile = open(sessionFileName, "w")
211 json.dump(sessionData, sessionFile, indent=2)
212 sessionFile.close()
213
214 @classmethod
215 def readSessionFromFile(cls, sessionFileName):
216 """
217 Class method to read the session data from a file.
218
219 @param sessionFileName file name of the session file
220 @type str
221 @return dictionary containing the session data
222 @rtype dict
223 """
224 try:
225 sessionFile = open(sessionFileName, "r")
226 sessionData = json.load(sessionFile)
227 sessionFile.close()
228 if not cls.isValidSession(sessionData):
229 sessionData = {}
230 except (IOError, OSError):
231 sessionData = {}
232
233 return sessionData
234
235 @classmethod
236 def isValidSession(cls, session):
237 """
238 Class method to check the validity of a session.
239
240 @param session dictionary containing the session data
241 @type dict
242 @return flag indicating validity
243 @rtype bool
244 """
245 if not session:
246 return False
247
248 if "Windows" not in session:
249 return False
250
251 if not session["Windows"]:
252 return False
253
254 return True
255
256 def __backupSavedSession(self):
257 """
258 Private method to backup the most recently saved session.
259 """
260 if QFile.exists(self.__lastActiveSession):
261
262 if QFile.exists(self.__sessionBackup1):
263 QFile.remove(self.__sessionBackup2)
264 QFile.copy(self.__sessionBackup1, self.__sessionBackup2)
265
266 QFile.remove(self.__sessionBackup1)
267 QFile.copy(self.__lastActiveSession, self.__sessionBackup1)
268
269 def sessionMetaData(self, includeBackups=False):
270 """
271 Public method to get the sessions meta data.
272
273 @param includeBackups flag indicating to include backup sessions
274 @type bool
275 @return list of session meta data
276 @rtype list of SessionMetaData
277 """
278 self.__fillMetaDataList()
279
280 metaDataList = self.__sessionMetaData[:]
281
282 if includeBackups and QFile.exists(self.__sessionBackup1):
283 data = SessionMetaData()
284 data.name = self.tr("Backup 1")
285 data.filePath = self.__sessionBackup1
286 data.isBackup = True
287 metaDataList.append(data)
288
289 if includeBackups and QFile.exists(self.__sessionBackup2):
290 data = SessionMetaData()
291 data.name = self.tr("Backup 2")
292 data.filePath = self.__sessionBackup2
293 data.isBackup = True
294 metaDataList.append(data)
295
296 return metaDataList
297
298 def __fillMetaDataList(self):
299 """
300 Private method to fill the sessions meta data list.
301
302 The sessions meta data list is only populated, if the variable holding
303 it is empty (i.e. it is populated on demand).
304 """
305 if self.__sessionMetaData:
306 return
307
308 sessionFilesInfoList = QDir(self.getSessionsDirectory()).entryInfoList(
309 ["*.json"], QDir.Files, QDir.Time)
310
311 for sessionFileInfo in sessionFilesInfoList:
312 sessionData = self.readSessionFromFile(
313 sessionFileInfo.absoluteFilePath())
314 if not sessionData or not sessionData["Windows"]:
315 continue
316
317 data = SessionMetaData()
318 data.name = sessionFileInfo.baseName()
319 data.filePath = sessionFileInfo.canonicalFilePath()
320
321 if sessionFileInfo == QFileInfo(self.defaultSessionFile()):
322 data.name = self.tr("Default Session")
323 data.isDefault = True
324
325 if self.__isActive(sessionFileInfo):
326 data.isActive = True
327
328 if data.isDefault:
329 # default session is always first
330 self.__sessionMetaData.insert(0, data)
331 else:
332 self.__sessionMetaData.append(data)
333
334 def __isActive(self, filePath):
335 """
336 Private method to check, if a given file is the active one.
337
338 @param filePath path of the session file to be checked
339 @type str or QFileInfo
340 @return flag indicating the active file
341 @rtype bool
342 """
343 return QFileInfo(filePath) == QFileInfo(self.__lastActiveSession)
344
345 @pyqtSlot()
346 def __sessionDirectoryChanged(self):
347 """
348 Private slot handling changes of the sessions directory.
349 """
350 self.__sessionMetaData = []
351
352 self.sessionsMetaDataChanged.emit()
353
354 @pyqtSlot()
355 def aboutToShowSessionsMenu(self, menu):
356 """
357 Public slot to populate the sessions selection menu.
358
359 @param menu reference to the menu about to be shown
360 @type QMenu
361 """
362 menu.clear()
363
364 actionGroup = QActionGroup(menu)
365 sessions = self.sessionMetaData(includeBackups=False)
366 for session in sessions:
367 act = menu.addAction(session.name)
368 act.setCheckable(True)
369 act.setChecked(session.isActive)
370 act.setData(session.filePath)
371 actionGroup.addAction(act)
372 act.triggered.connect(lambda: self.__sessionActTriggered(act))
373
374 @pyqtSlot()
375 def __sessionActTriggered(self, act):
376 """
377 Private slot to handle the menu selection of a session.
378
379 @param act reference to the action that triggered
380 @type QAction
381 """
382 path = act.data()
383 self.switchToSession(path)
384
385 def openSession(self, sessionFilePath, flags=0):
386 """
387 Public method to open a session from a given session file.
388
389 @param sessionFilePath name of the session file to get session from
390 @type str
391 @param flags flags determining the open mode
392 @type int
393 """
394 if self.__isActive(sessionFilePath):
395 return
396
397 sessionData = self.readSessionFromFile(sessionFilePath)
398 if not sessionData or not sessionData["Windows"]:
399 return
400
401 from WebBrowser.WebBrowserWindow import WebBrowserWindow
402 window = WebBrowserWindow.mainWindow()
403
404 if ((flags & SessionManager.SwitchSession) ==
405 SessionManager.SwitchSession):
406 # save the current session
407 self.writeCurrentSession(self.__lastActiveSession)
408
409 # create new window for the new session
410 window = window.newWindow(restoreSession=True)
411
412 # close all existing windows
413 for win in WebBrowserWindow.mainWindows()[:]:
414 if win is not window:
415 win.forceClose()
416
417 if not ((flags & SessionManager.ReplaceSession) ==
418 SessionManager.ReplaceSession):
419 self.__lastActiveSession = \
420 QFileInfo(sessionFilePath).canonicalFilePath()
421 self.__sessionMetaData = []
422
423 self.restoreSessionFromData(window, sessionData)
424
425 @classmethod
426 def restoreSessionFromData(cls, window=None, sessionData=None):
427 """
428 Class method to restore a session from a session data dictionary.
429
430 @param window reference to main window to restore to
431 @type WebBrowserWindow
432 @param sessionData dictionary containing the session data
433 """
434 from WebBrowser.WebBrowserWindow import WebBrowserWindow
435 if window is None:
436 window = WebBrowserWindow.mainWindow()
437
438 QApplication.setOverrideCursor(Qt.WaitCursor)
439 # restore session for first window
440 data = sessionData["Windows"].pop(0)
441 window.tabWidget().loadFromSessionData(data)
442 if "WindowGeometry" in data:
443 geometry = QByteArray.fromBase64(
444 data["WindowGeometry"].encode("ascii"))
445 window.restoreGeometry(geometry)
446 QApplication.processEvents()
447
448 # restore additional windows
449 for data in sessionData["Windows"]:
450 window = WebBrowserWindow.mainWindow()\
451 .newWindow(restoreSession=True)
452 window.tabWidget().loadFromSessionData(data)
453 if "WindowGeometry" in data:
454 geometry = QByteArray.fromBase64(
455 data["WindowGeometry"].encode("ascii"))
456 window.restoreGeometry(geometry)
457 QApplication.processEvents()
458 QApplication.restoreOverrideCursor()
459
460 if "CurrentWindowIndex" in sessionData:
461 currentWindowIndex = sessionData["CurrentWindowIndex"]
462 try:
463 currentWindow = \
464 WebBrowserWindow.mainWindows()[currentWindowIndex]
465 QTimer.singleShot(0, lambda: currentWindow.raise_())
466 except IndexError:
467 # ignore it
468 pass
469
470 def renameSession(self, sessionFilePath, flags=0):
471 """
472 Public method to rename or clone a session.
473
474 @param sessionFilePath name of the session file
475 @type str
476 @param flags flags determining a rename or clone operation
477 @type int
478 """
479 from WebBrowser.WebBrowserWindow import WebBrowserWindow
480
481 suggestedName = QFileInfo(sessionFilePath).baseName()
482 if flags & SessionManager.CloneSession:
483 suggestedName += "_cloned"
484 title = self.tr("Clone Session")
485 else:
486 suggestedName += "_renamed"
487 title = self.tr("Rename Session")
488 newName, ok = QInputDialog.getText(
489 WebBrowserWindow.getWindow(),
490 title,
491 self.tr("Please enter a new name:"),
492 QLineEdit.Normal,
493 suggestedName)
494
495 if not ok:
496 return
497
498 if not newName.endswith(".json"):
499 newName += ".json"
500
501 newSessionPath = os.path.join(self.getSessionsDirectory(), newName)
502 if os.path.exists(newSessionPath):
503 E5MessageBox.information(
504 WebBrowserWindow.getWindow(),
505 title,
506 self.tr("""The session file "{0}" exists already. Please"""
507 """ enter another name.""").format(newName))
508 self.renameSession(sessionFilePath, flags)
509 return
510
511 if flags & SessionManager.CloneSession:
512 if not QFile.copy(sessionFilePath, newSessionPath):
513 E5MessageBox.critical(
514 WebBrowserWindow.getWindow(),
515 title,
516 self.tr("""An error occurred while cloning the session"""
517 """ file."""))
518 return
519 else:
520 if not QFile.rename(sessionFilePath, newSessionPath):
521 E5MessageBox.critical(
522 WebBrowserWindow.getWindow(),
523 title,
524 self.tr("""An error occurred while renaming the session"""
525 """ file."""))
526 return
527 if self.__isActive(sessionFilePath):
528 self.__lastActiveSession = newSessionPath
529 self.__sessionMetaData = []
530
531 def saveSession(self):
532 """
533 Public method to save the current session.
534 """
535 from WebBrowser.WebBrowserWindow import WebBrowserWindow
536 newName, ok = QInputDialog.getText(
537 WebBrowserWindow.getWindow(),
538 self.tr("Save Session"),
539 self.tr("Please enter a name for the session:"),
540 QLineEdit.Normal,
541 self.tr("Saved Session ({0})").format(
542 QDateTime.currentDateTime().toString("yyyy-MM-dd HH-mm-ss")))
543
544 if not ok:
545 return
546
547 if not newName.endswith(".json"):
548 newName += ".json"
549
550 newSessionPath = os.path.join(self.getSessionsDirectory(), newName)
551 if os.path.exists(newSessionPath):
552 E5MessageBox.information(
553 WebBrowserWindow.getWindow(),
554 self.tr("Save Session"),
555 self.tr("""The session file "{0}" exists already. Please"""
556 """ enter another name.""").format(newName))
557 self.saveSession()
558 return
559
560 self.writeCurrentSession(newSessionPath)
561
562 def replaceSession(self, sessionFilePath):
563 """
564 Public method to replace the current session with the given one.
565
566 @param sessionFilePath file name of the session file to replace with
567 @type str
568 @return flag indicating success
569 @rtype bool
570 """
571 from WebBrowser.WebBrowserWindow import WebBrowserWindow
572 res = E5MessageBox.yesNo(
573 WebBrowserWindow.getWindow(),
574 self.tr("Restore Backup"),
575 self.tr("""Are you sure you want to replace the current"""
576 """ session?"""))
577 if res:
578 self.openSession(sessionFilePath, SessionManager.ReplaceSession)
579 return True
580 else:
581 return False
582
583 def switchToSession(self, sessionFilePath):
584 """
585 Public method to switch the current session to the given one.
586
587 @param sessionFilePath file name of the session file to switch to
588 @type str
589 @return flag indicating success
590 @rtype bool
591 """
592 self.openSession(sessionFilePath, SessionManager.SwitchSession)
593 return True
594
595 def cloneSession(self, sessionFilePath):
596 """
597 Public method to clone a session.
598
599 @param sessionFilePath file name of the session file to be cloned
600 @type str
601 """
602 self.renameSession(sessionFilePath, SessionManager.CloneSession)
603
604 def deleteSession(self, sessionFilePath):
605 """
606 Public method to delete a session.
607
608 @param sessionFilePath file name of the session file to be deleted
609 @type str
610 """
611 from WebBrowser.WebBrowserWindow import WebBrowserWindow
612 res = E5MessageBox.yesNo(
613 WebBrowserWindow.getWindow(),
614 self.tr("Delete Session"),
615 self.tr("""Are you sure you want to delete session "{0}"?""")
616 .format(QFileInfo(sessionFilePath).baseName()))
617 if res:
618 QFile.remove(sessionFilePath)
619
620 def newSession(self):
621 """
622 Public method to start a new session.
623 """
624 from WebBrowser.WebBrowserWindow import WebBrowserWindow
625 newName, ok = QInputDialog.getText(
626 WebBrowserWindow.getWindow(),
627 self.tr("New Session"),
628 self.tr("Please enter a name for the new session:"),
629 QLineEdit.Normal,
630 self.tr("New Session ({0})").format(
631 QDateTime.currentDateTime().toString("yyyy-MM-dd HH-mm-ss")))
632
633 if not ok:
634 return
635
636 if not newName.endswith(".json"):
637 newName += ".json"
638
639 newSessionPath = os.path.join(self.getSessionsDirectory(), newName)
640 if os.path.exists(newSessionPath):
641 E5MessageBox.information(
642 WebBrowserWindow.getWindow(),
643 self.tr("New Session"),
644 self.tr("""The session file "{0}" exists already. Please"""
645 """ enter another name.""").format(newName))
646 self.newSession()
647 return
648
649 self.writeCurrentSession(self.__lastActiveSession)
650
651 # create new window for the new session and close all existing windows
652 window = WebBrowserWindow.mainWindow().newWindow()
653 for win in WebBrowserWindow.mainWindows():
654 if win is not window:
655 win.forceClose()
656
657 self.__lastActiveSession = newSessionPath
658 self.__autoSaveSession()
659
660 def showSessionManagerDialog(self):
661 """
662 Public method to show the session manager dialog.
663 """
664 from WebBrowser.WebBrowserWindow import WebBrowserWindow
665 from .SessionManagerDialog import SessionManagerDialog
666
667 dlg = SessionManagerDialog(WebBrowserWindow.getWindow())
668 dlg.open()
669
670 def selectSession(self):
671 """
672 Public method to select a session to be restored.
673
674 @return name of the session file to be restored
675 @rtype str
676 """
677 from WebBrowser.WebBrowserWindow import WebBrowserWindow
678
679 self.__fillMetaDataList()
680
681 if self.__sessionMetaData:
682 # skip, if no session file available
683 dlg = QDialog(WebBrowserWindow.getWindow(),
684 Qt.WindowStaysOnTopHint)
685 lbl = QLabel(self.tr("Please select the startup session:"))
686 combo = QComboBox(dlg)
687 buttonBox = QDialogButtonBox(
688 QDialogButtonBox.Ok | QDialogButtonBox.Cancel, dlg)
689 buttonBox.accepted.connect(dlg.accept)
690 buttonBox.rejected.connect(dlg.reject)
691
692 layout = QVBoxLayout()
693 layout.addWidget(lbl)
694 layout.addWidget(combo)
695 layout.addWidget(buttonBox)
696 dlg.setLayout(layout)
697
698 lastActiveSessionFileInfo = QFileInfo(self.__lastActiveSession)
699
700 for metaData in self.__sessionMetaData:
701 if QFileInfo(metaData.filePath) != lastActiveSessionFileInfo:
702 combo.addItem(metaData.name, metaData.filePath)
703 else:
704 combo.insertItem(
705 0,
706 self.tr("{0} (last session)").format(metaData.name),
707 metaData.filePath
708 )
709 combo.setCurrentIndex(0)
710
711 if dlg.exec_() == QDialog.Accepted:
712 session = combo.currentData()
713 if session is None:
714 self.__lastActiveSession = self.__sessionDefault
715 else:
716 self.__lastActiveSession = session
717
718 return self.__lastActiveSession

eric ide

mercurial