eric7/WebBrowser/Session/SessionManager.py

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

eric ide

mercurial