#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2020, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). This application is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library #Third Party from PyQt5 import QtCore, QtGui, QtWidgets #Engine import engine.api as api #Qt from .helper import sizeof_fmt from .projectname import ProjectNameWidget from .projectname import NewSessionDialog class DirectoryItem(QtWidgets.QTreeWidgetItem): """A plain directory with no session content""" def __lt__(self, other): """Treeview uses only less than. less equal, both greater and equal are not used. We always want to be on top, no matter what column or sort order """ if type(other) is SessionItem: if self.treeWidget().header().sortIndicatorOrder(): #descending? return False else: return True else: return QtWidgets.QTreeWidgetItem.__lt__(self, other) class SessionItem(QtWidgets.QTreeWidgetItem): """Subclass to enable sorting of size by actual value, not by human readable display. entry["nsmSessionName"] = projectName entry["name"] = os.path.basename(projectName) entry["lastSavedDate"] = "2016-05-21 16:36" entry["fullPath"] = actual path entry["sizeInBytes"] = 623623 entry["numberOfClients"] = 3 entry["hasSymlinks"] = True entry["parents"] = [] """ allItems = {} #nsmSessionName : SessionItem def __init__(self, sessionDict): SessionItem.allItems[sessionDict["nsmSessionName"]] = self self.sessionDict = sessionDict symlinks = "Yes" if sessionDict["hasSymlinks"] else "No" #TODO: Translate parameterList = [sessionDict["name"], sessionDict["lastSavedDate"], str(sessionDict["numberOfClients"]), sizeof_fmt(sessionDict["sizeInBytes"]), symlinks, sessionDict["fullPath"], ] super().__init__(parameterList, type=1000) #type 0 is default qt type. 1000 is subclassed user type) self.setTextAlignment(2, QtCore.Qt.AlignHCenter) #clients self.setTextAlignment(4, QtCore.Qt.AlignHCenter) #symlinks self.setLocked(sessionDict["locked"]) def updateData(self): """Actively queries the api for new data""" sessionDict = api.sessionQuery(self.sessionDict["nsmSessionName"]) self.sessionDict = sessionDict self.setText(0, sessionDict["name"]) self.setText(1, sessionDict["lastSavedDate"]) self.setText(2, str(sessionDict["numberOfClients"])) self.setText(3, sizeof_fmt(sessionDict["sizeInBytes"])) self.setText(4, "Yes" if sessionDict["hasSymlinks"] else "No") #TODO: Translate self.setText(5, sessionDict["fullPath"]) def setLocked(self, state:bool): """Number of clients, symlinks and size change frequently while a session is open/locked. We deactivate the display of these values while locked""" return if not state == self.isDisabled(): self.setDisabled(state) if state: self.setText(2, "") #number of clients self.setText(3, "") #Symlinks self.setText(4, "") #Size else: self.updateData() def updateTimestamp(self, timestamp:str): #Column 1 "Last Save" self.setText(1, timestamp) def __lt__(self, other): """Treeview uses only less than. less equal, both greater and equal are not used. There is no check between two directory-items here because these are standard WidgetItems """ if type(other) is DirectoryItem: #Just a dir return False #we are "greater"=later column = self.treeWidget().sortColumn() if column == 3: #bytes return self.sessionDict["sizeInBytes"] > other.sessionDict["sizeInBytes"] elif column == 2: #number of clients return self.sessionDict["numberOfClients"] > other.sessionDict["numberOfClients"] else: return QtWidgets.QTreeWidgetItem.__lt__(self, other) class SessionTreeController(object): """Controls a treeWidget, but does not subclass""" def __init__(self, mainWindow): self.mainWindow = mainWindow self.treeWidget = mainWindow.ui.session_tree self._cachedSessionDicts = None self.mainWindow.ui.checkBoxNested.stateChanged.connect(self._reactSignal_nestedFlatChanged) self._reactSignal_nestedFlatChanged(self.mainWindow.ui.checkBoxNested.isChecked()) #initial state #Configure the treewidget #columns: name, path (relative from session dir), number of programs, disk size (links resolved?) self.treeWidget.setColumnCount(4) self.headerLabels = [ QtCore.QCoreApplication.translate("SessionTree", "Name"), QtCore.QCoreApplication.translate("SessionTree", "Last Save"), QtCore.QCoreApplication.translate("SessionTree", "Clients"), QtCore.QCoreApplication.translate("SessionTree", "Size"), QtCore.QCoreApplication.translate("SessionTree", "Symlinks"), QtCore.QCoreApplication.translate("SessionTree", "Path"), ] self.treeWidget.setHeaderLabels(self.headerLabels) self.treeWidget.setSortingEnabled(True) self.treeWidget.setAlternatingRowColors(True) self.treeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) #TODO: save sorting in user-wide qt application settings #We remember sorting via signals layoutAboutToBeChanged and restore via layoutChanged self.sortByColumn = 0 #by name self.sortDescending = 0 # Qt::SortOrder which is 0 for ascending and 1 for descending self.treeWidget.header().setSortIndicator(0,0) #Hack/Workaround. On startup it is not enough to set sorting. New items will be added in a random position. Maybe that is our async network adding. #self.treeWidget.sortByColumn(self.sortByColumn, self.sortDescending) api.callbacks.sessionsChanged.append(self._reactCallback_sessionsChanged) api.callbacks.sessionLocked.append(self._reactCallback_sessionLocked) api.callbacks.sessionFileChanged.append(self._reactCallback_sessionFileChanged) self.treeWidget.itemDoubleClicked.connect(self._reactSignal_itemDoubleClicked) self.treeWidget.customContextMenuRequested.connect(self.contextMenu) self.treeWidget.model().layoutAboutToBeChanged.connect(self._reactSignal_rememberSorting) self.treeWidget.model().layoutChanged.connect(self._reactSignal_restoreSorting) self.mainWindow.ui.button_new_session.clicked.connect(self._reactSignal_newSession) self.mainWindow.ui.button_load_selected_session.clicked.connect(self._reactSignal_openSelected) logger.info("Full View Session Chooser ready") def _reactCallback_sessionFileChanged(self, name:str, timestamp:str): """Timestamp of "last saved" changed""" SessionItem.allItems[name].updateTimestamp(timestamp) def _reactCallback_sessionLocked(self, name:str, state:bool): SessionItem.allItems[name].setLocked(state) def _reactCallback_sessionsChanged(self, sessionDicts:list): """Main callback for new, added, removed, moved sessions etc. We also get this for every client change so we can update our numbers""" self.treeWidget.clear() self._cachedSessionDicts = sessionDicts #in case we change the flat/nested mode. for sessionDict in sessionDicts: self.addSessionItem(sessionDict) for i in range(len(self.headerLabels)): self.treeWidget.resizeColumnToContents(i) #Make the name column a few pixels wider self.treeWidget.setColumnWidth(0, self.treeWidget.columnWidth(0) + 25) self.treeWidget.sortByColumn(self.sortByColumn, self.sortDescending) def _addItemNested(self, sessionDict:dict): assert sessionDict, sessionDict item = SessionItem(sessionDict) if sessionDict["parents"]: #These are already a hirarchy, sorted from parents to children last = None #first is toplevel for parentDir in sessionDict["parents"]: alreadyExist = self.treeWidget.findItems(parentDir, QtCore.Qt.MatchExactly|QtCore.Qt.MatchRecursive, column=0) if alreadyExist: directoryItem = alreadyExist[0] else: directoryItem = DirectoryItem([parentDir]) #directoryItem = QtWidgets.QTreeWidgetItem([parentDir], 0) #type 0 is qt default if last: last.addChild(directoryItem) else: self.treeWidget.addTopLevelItem(directoryItem) last = directoryItem #After the loop: All subdirs built. Now add the item to the last one last.addChild(item) else: self.treeWidget.addTopLevelItem(item) def _addItemFlat(self, sessionDict:dict): assert sessionDict, sessionDict sessionDict["name"] = sessionDict["nsmSessionName"] item = SessionItem(sessionDict) self.treeWidget.addTopLevelItem(item) def addSessionItem(self, sessionDict:dict): if self.mode == "nested": self._addItemNested(sessionDict) elif self.mode == "flat": self._addItemFlat(sessionDict) else: raise ValueError("Unknown SessionTree display mode") def deleteSessionItem(self, item:SessionItem): """Instruct the engine to fully delete a complete session item. Will show a warning before.""" text = QtCore.QCoreApplication.translate("SessionTree", "About to delete Session {}").format(item.sessionDict["nsmSessionName"]) informativeText = QtCore.QCoreApplication.translate("SessionTree", "All files in the project directory will be irreversibly deleted.") title = QtCore.QCoreApplication.translate("SessionTree", "All files in the project directory will be irreversibly deleted.") title = QtCore.QCoreApplication.translate("SessionTree", "About to delete Session {}").format(item.sessionDict["nsmSessionName"]) box = QtWidgets.QMessageBox(self.treeWidget) box.setIcon(box.Warning) box.setText(text) box.setWindowTitle(title) box.setInformativeText(informativeText) keep = box.addButton(QtCore.QCoreApplication.translate("SessionTree", "Keep Session"), box.RejectRole) box.addButton(QtCore.QCoreApplication.translate("SessionTree", "Delete!"), box.AcceptRole) box.setDefaultButton(keep) ret = box.exec() #0 or 1. Return values are NOT the button roles. if ret: #Delete api.sessionDelete(item.sessionDict["nsmSessionName"]) def contextMenu(self, qpoint): item = self.treeWidget.itemAt(qpoint) if not type(item) is SessionItem: return menu = QtWidgets.QMenu() listOfLabelsAndFunctions = [ (QtCore.QCoreApplication.translate("SessionTree", "Copy Session"), lambda: self._askForCopyAndCopy(item.sessionDict["nsmSessionName"])) ] if item.isDisabled(): listOfLabelsAndFunctions.append((QtCore.QCoreApplication.translate("SessionTree", "Force Lock Removal"), lambda: api.sessionForceLiftLock(item.sessionDict["nsmSessionName"]))) else: listOfLabelsAndFunctions.append((QtCore.QCoreApplication.translate("SessionTree", "Rename Session"), lambda: self._askForNameAndRenameSession(item.sessionDict["nsmSessionName"]))) #Delete should be the bottom item. listOfLabelsAndFunctions.append((QtCore.QCoreApplication.translate("SessionTree", "Delete Session"), lambda: self.deleteSessionItem(item))) for text, function in listOfLabelsAndFunctions: if function is None: l = QtWidgets.QLabel(text) l.setAlignment(QtCore.Qt.AlignCenter) a = QtWidgets.QWidgetAction(menu) a.setDefaultWidget(l) menu.addAction(a) else: a = QtWidgets.QAction(text, menu) menu.addAction(a) a.triggered.connect(function) pos = QtGui.QCursor.pos() pos.setY(pos.y() + 5) menu.exec_(pos) #GUI Signals def _reactSignal_itemDoubleClicked(self, item:QtWidgets.QTreeWidgetItem, column:int): if not item.isDisabled() and type(item) is SessionItem: api.sessionOpen(item.sessionDict["nsmSessionName"]) def _reactSignal_openSelected(self): item = self.treeWidget.currentItem() if item: self._reactSignal_itemDoubleClicked(item, column=0) def _reactSignal_newSession(self): widget = NewSessionDialog(parent=self.treeWidget, startwith="") #widget = ProjectNameWidget(parent=self.treeWidget, startwith="") if widget.result: #result = {"name":str, "startclients":list} api.sessionNew(widget.result["name"], widget.result["startclients"]) def _askForCopyAndCopy(self, nsmSessionName:str): """Called by button and context menu""" widget = ProjectNameWidget(parent=self.treeWidget, startwith=nsmSessionName+"-copy") if widget.result: api.sessionCopy(nsmSessionName, widget.result) def _askForNameAndRenameSession(self, nsmSessionName:str): """Only for non-locked sessions. Context menu is only available if not locked.""" widget = ProjectNameWidget(parent=self.treeWidget, startwith=nsmSessionName) if widget.result and not widget.result == nsmSessionName: api.sessionRename(nsmSessionName, widget.result) def _reactSignal_rememberSorting(self, *args): self.sortByColumn = self.treeWidget.header().sortIndicatorSection() self.sortDescending = self.treeWidget.header().sortIndicatorOrder() def _reactSignal_restoreSorting(self, *args): self.treeWidget.sortByColumn(self.sortByColumn, self.sortDescending) def _reactSignal_nestedFlatChanged(self, checkStatus:bool): """#flat does not create directory items but changes the session name to dir/foo/bar""" if checkStatus: self.mode = "nested" else: self.mode = "flat" #And rebuild the items without fetching new data. if self._cachedSessionDicts: #not startup self._reactCallback_sessionsChanged(self._cachedSessionDicts) self.treeWidget.sortByColumn(self.sortByColumn, self.sortDescending)