#! /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 < http : / / www . gnu . org / licenses / > .
"""
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 . sortByColumnValue = 0 #by name
self . sortDescendingValue = 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.sortByColumnValue, self.sortDescendingValue)
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 . sortItems ( self . sortByColumnValue , self . sortDescendingValue )
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 . sortByColumnValue = self . treeWidget . header ( ) . sortIndicatorSection ( )
self . sortDescendingValue = self . treeWidget . header ( ) . sortIndicatorOrder ( )
def _reactSignal_restoreSorting ( self , * args ) :
""" Do not use as signal!!! Will lead to infinite recursion since Qt 5.12.2 """
#self.treeWidget.sortItems(self.sortByColumnValue, self.sortDescendingValue)
raise RuntimeError ( )
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 . sortItems ( self . sortByColumnValue , self . sortDescendingValue )