src/eric7/WebBrowser/Session/SessionManager.py

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

eric ide

mercurial