7 this file is part of the project scolasync
9 Copyright (C) 2010-2014 Georges Khaznadar <georgesk@ofset.org>
11 This program is free software: you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation, either version3 of the License, or
14 (at your option) any later version.
16 This program is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with this program. If not, see <http://www.gnu.org/licenses/>.
29 import ownedUsbDisk, help, copyToDialog1, chooseInSticks, usbThread
30 import diskFull, preferences, checkBoxDialog
31 import os.path, operator, subprocess, dbus, re, time, copy
32 from notification
import Notification
33 from usbDisk2
import safePath
37 from globaldef
import logFileName, _dir
55 global pastCommands, lastCommand
56 if cmd
in pastCommands:
57 pastCommands[cmd].append(partition.owner)
59 pastCommands[cmd]=[partition.owner]
68 checkAllSignal=pyqtSignal()
69 checkToggleSignal=pyqtSignal()
70 checkNoneSignal=pyqtSignal()
71 shouldNameDrive=pyqtSignal()
72 pushCmdSignal=pyqtSignal(str, str)
73 popCmdSignal=pyqtSignal(str, str)
82 QMainWindow.__init__(self)
83 QWidget.__init__(self, parent)
85 from Ui_mainWindow
import Ui_MainWindow
86 self.
ui = Ui_MainWindow()
88 QIcon.setThemeName(
"Tango")
91 self.
movefromIcon=QIcon.fromTheme(
"movefrom",QIcon(
"/usr/share/scolasync/images/movefrom.png"))
97 self.
setThemedIcon(self.ui.forceCheckButton,
"multimedia-player")
98 self.
setThemedIcon(self.ui.preferenceButton,
"package_settings")
103 self.
namesFullTip=QApplication.translate(
"MainWindow",
"<br />Des noms sont disponibles pour renommer les prochains baladeurs que vous brancherez",
None)
104 self.
namesEmptyTip=QApplication.translate(
"MainWindow",
"<br />Cliquez sur ce bouton pour préparer une liste de noms afin de renommer les prochains baladeurs que vous brancherez",
None)
110 self.
t=self.ui.tableView
112 self.proxy.setSourceModel(self.t.model())
118 self.ui.helpButton.clicked.connect(self.
help)
119 self.ui.umountButton.clicked.connect(self.
umount)
120 self.ui.toButton.clicked.connect(self.
copyTo)
121 self.ui.fromButton.clicked.connect(self.
copyFrom)
122 self.ui.delButton.clicked.connect(self.
delFiles)
123 self.ui.redoButton.clicked.connect(self.
redoCmd)
124 self.ui.namesButton.clicked.connect(self.
namesCmd)
125 self.ui.preferenceButton.clicked.connect(self.
preference)
126 self.ui.tableView.doubleClicked.connect(self.
tableClicked)
127 self.checkAllSignal.connect(self.
checkAll)
129 self.checkNoneSignal.connect(self.
checkNone)
132 qApp.available.addHook(
'object-added', self.
cbAdded())
133 qApp.available.addHook(
'object-removed', self.
cbRemoved())
134 self.pushCmdSignal.connect(self.
pushCmd)
135 self.popCmdSignal.connect(self.
popCmd)
150 icon.addPixmap(QIcon.fromTheme(name).pixmap(32))
152 icon.addPixmap(
"images/icons32/"+name+
".png")
163 global activeThreads, pastCommands, lastCommand
164 if owner
in activeThreads:
165 activeThreads[owner].append(cmd)
167 activeThreads[owner]=[cmd]
168 self.tm.updateOwnerColumn()
178 global activeThreads, pastCommands, lastCommand
179 if owner
in activeThreads:
180 cmd0=activeThreads[owner].pop()
182 msg=cmd.replace(cmd0,
"")+
"\n"
183 logFile=open(os.path.expanduser(logFileName),
"a")
187 raise Exception((
"mismatched commands\n%s\n%s" %(cmd,cmd0)))
188 if len(activeThreads[owner])==0:
189 activeThreads.pop(owner)
191 raise Exception(
"End of command without a begin.")
192 self.tm.updateOwnerColumn()
193 if len(activeThreads)==0 :
204 index0=model.createIndex(0,0)
205 index1=model.createIndex(len(model.donnees)-1,0)
206 srange=QItemSelectionRange(index0,index1)
207 for i
in srange.indexes():
208 checked=bool(i.model().data(i,Qt.DisplayRole))
209 model.setData(i, boolFunc(checked),Qt.EditRole)
242 hint=db.readStudent(disk.serial, disk.uuid, ownedUsbDisk.tattooInDir(disk.mp))
248 nameList=self.namesDialog.itemStrings(),
249 driveIdent=(stickId, uuid, tattoo))
260 def _cbAdded(man, obj):
261 if qApp.available.modified:
266 qApp.available.modified=
False
275 def _cbRemoved(man, obj):
276 if qApp.available.modified:
278 if path
in qApp.available.targets:
282 qApp.available.modified=
False
311 self.iconRedo.addPixmap(QIcon.fromTheme(
"go-jump").pixmap(32), QIcon.Normal, QIcon.Off)
313 self.iconStop.addPixmap(QIcon.fromTheme(
"stop").pixmap(32), QIcon.Normal, QIcon.Off)
315 self.
redoToolTip=QApplication.translate(
"MainWindow",
"Refaire à nouveau",
None)
316 self.
redoStatusTip=QApplication.translate(
"MainWindow",
"Refaire à nouveau la dernière opération réussie, avec les baladeurs connectés plus récemment",
None)
317 self.
stopToolTip=QApplication.translate(
"MainWindow",
"Arrêter les opérations en cours",
None)
318 self.
stopStatusTip=QApplication.translate(
"MainWindow",
"Essaie d'arrêter les opérations en cours. À faire seulement si celles-ci durent trop longtemps",
None)
330 self.
header=ownedUsbDisk.uDisk2.headers()
346 connectedCount=int(qApp.available)
347 self.ui.lcdNumber.display(connectedCount)
348 self.t.resizeColumnsToContents()
368 mappedIdx=self.proxy.mapFromSource(idx)
378 cmd=
"xdg-open '%s'" %idx.data()
379 subprocess.call(cmd, shell=
True)
380 elif "capacity" in h:
381 mount=idx.model().partition(idx).mountPoint()
382 dev,total,used,remain,pcent,path = self.
diskSizeData(mount)
383 pcent=int(pcent[:-1])
387 QMessageBox.warning(
None,
388 QApplication.translate(
"Dialog",
"Double-clic non pris en compte",
None),
389 QApplication.translate(
"Dialog",
"pas d'action pour l'attribut {a}",
None).format(a=h))
407 if type(rowOrDev)==type(0):
408 path=qApp.available[rowOrDev][self.header.index(
"1mp")]
412 dfOutput=subprocess.Popen(cmd, shell=
True, stdout=subprocess.PIPE).communicate()[0]
413 dfOutput=str(dfOutput.split(b
"\n")[-2])
414 m = re.match(
"(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*", dfOutput).groups()
428 s=db.readStudent(d.stickid, d.uuid, d.tattoo())
431 elif s==
None and defaultDisk==
None :
443 student=
"%s" %self.tm.data(idx,Qt.DisplayRole).value()
445 ownedUsbDisk.editRecord(self.
diskFromOwner(student), hint=student)
464 self.ui.namesButton.setIcon(icon)
465 self.ui.namesButton.setToolTip(msg)
466 self.ui.namesButton.setStatusTip(msg.replace(
"<br />",
""))
479 global activeThreads, lastCommand
480 active = len(qApp.available)>0
481 for button
in (self.ui.toButton,
484 self.ui.umountButton):
485 button.setEnabled(active)
495 if len(activeThreads) > 0:
496 self.ui.redoButton.setIcon(self.
iconStop)
499 self.ui.redoButton.setEnabled(
True)
502 self.ui.redoButton.setIcon(self.
iconRedo)
505 self.ui.redoButton.setEnabled(lastCommand!=
None)
506 l=self.namesDialog.ui.listWidget.findItems(
"*",Qt.MatchWildcard)
518 pref.setValues(db.readPrefs())
521 if pref.result()==QDialog.Accepted:
522 db.writePrefs(pref.values())
531 titre1=QApplication.translate(
"Dialog",
"Choix de fichiers à supprimer",
None)
532 titre2=QApplication.translate(
"Dialog",
"Choix de fichiers à supprimer (jokers autorisés)",
None)
536 pathList=d.pathList()
537 buttons=QMessageBox.Ok|QMessageBox.Cancel
538 defaultButton=QMessageBox.Cancel
539 reply=QMessageBox.warning(
541 QApplication.translate(
"Dialog",
"Vous allez effacer plusieurs baladeurs",
None),
542 QApplication.translate(
"Dialog",
"Etes-vous certain de vouloir effacer : "+
"\n".join(pathList),
None),
543 buttons, defaultButton)
544 if reply == QMessageBox.Ok:
545 cmd=
"usbThread.threadDeleteInUSB(p,{paths},subdir='Travail', logfile='{log}', parent=self)".format(paths=pathList,log=logFileName)
546 for p
in qApp.available:
547 if not p.selected:
continue
552 self.oldThreads.add(t)
555 msgBox=QMessageBox.warning(
557 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None),
558 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None))
569 cmd=
"usbThread.threadCopyToUSB(p,{selected},subdir='{subdir}', logfile='{logfile}', parent=self)".format(selected=list(d.selectedList()), subdir=self.
workdir, logfile=logFileName)
571 for p
in qApp.available:
572 if not p.selected:
continue
577 self.oldThreads.add(t)
580 msgBox=QMessageBox.warning(
582 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None),
583 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None))
591 titre1=QApplication.translate(
"Dialog",
"Choix de fichiers à copier",
None)
592 titre2=QApplication.translate(
"Dialog",
"Choix de fichiers à copier depuis les baladeurs",
None)
593 okPrompt=QApplication.translate(
"Dialog",
"Choix de la destination ...",
None)
597 msgBox=QMessageBox.warning(
None,
598 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None),
599 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None))
602 pathList=d.pathList()
603 mp=d.selectedDiskMountPoint()
604 initialPath=os.path.expanduser(
"~")
605 destDir = QFileDialog.getExistingDirectory(
607 QApplication.translate(
"Dialog",
"Choisir un répertoire de destination",
None),
609 if destDir
and len(destDir)>0 :
611 cmd=
"""usbThread.threadMoveFromUSB(
612 p,{paths},subdir=self.workdir,
613 rootPath='{mp}', dest='{dest}', logfile='{log}',
614 parent=self)""".format(paths=pathList, mp=mp, dest=destDir, log=logFileName)
616 cmd=
"""usbThread.threadCopyFromUSB(
617 p,{paths},subdir=self.workdir,
618 rootPath='{mp}', dest='{dest}', logfile='{log}',
619 parent=self)""".format(paths=pathList, mp=mp, dest=destDir, log=logFileName)
621 for p
in qApp.available:
622 if not p.selected:
continue
632 self.oldThreads.add(t)
634 buttons=QMessageBox.Ok|QMessageBox.Cancel
635 defaultButton=QMessageBox.Cancel
636 if QMessageBox.question(
638 QApplication.translate(
"Dialog",
"Voir les copies",
None),
639 QApplication.translate(
"Dialog",
"Voulez-vous voir les fichiers copiés ?",
None),
640 buttons, defaultButton)==QMessageBox.Ok:
641 subprocess.call(
"xdg-open '%s'" %destDir,shell=
True)
644 msgBox=QMessageBox.warning(
646 QApplication.translate(
"Dialog",
"Destination manquante",
None),
647 QApplication.translate(
"Dialog",
"Veuillez choisir une destination pour la copie des fichiers",
None))
656 global lastCommand, pastCommands, activeThreads
657 if len(activeThreads)>0:
661 thread._Thread__stop()
662 print (str(thread.getName()) +
' is terminated')
664 print (str(thread.getName()) +
' could not be terminated')
666 if lastCommand==
None:
668 if QMessageBox.question(
670 QApplication.translate(
"Dialog",
"Réitérer la dernière commande",
None),
671 QApplication.translate(
"Dialog",
"La dernière commande était<br>{cmd}<br>Voulez-vous la relancer avec les nouveaux baladeurs ?",
None).format(cmd=lastCommand))==QMessageBox.Cancel:
673 for p
in qApp.available:
674 if p.owner
in pastCommands[lastCommand] :
continue
675 exec(compile(lastCommand,
'<string>',
'exec'))
678 self.oldThreads.add(t)
679 pastCommands[lastCommand].append(p.owner)
687 self.namesDialog.show()
703 buttons=QMessageBox.Ok|QMessageBox.Cancel
704 defaultButton=QMessageBox.Cancel
705 button=QMessageBox.question (
707 QApplication.translate(
"Main",
"Démontage des baladeurs",
None),
708 QApplication.translate(
"Main",
"Êtes-vous sûr de vouloir démonter tous les baladeurs cochés de la liste ?",
None),
709 buttons,defaultButton)
710 if button!=QMessageBox.Ok:
712 for d
in qApp.available.disks_ud():
713 for partition
in qApp.available.parts_ud(d.path):
715 cmd=
"umount {0}".format(partition.mp)
716 subprocess.call(cmd, shell=
True)
717 cmd=
"udisks --detach {0}".format(d.devStuff)
718 subprocess.call(cmd, shell=
True)
731 if h
in ownedUsbDisk.uDisk2._itemNames:
732 self.visibleheader.append(self.tr(ownedUsbDisk.uDisk2._itemNames[h]))
734 self.visibleheader.append(h)
736 self.t.setModel(self.
tm)
740 self.proxy.setSourceModel(self.t.model())
748 return len(one.targets) == len(two.targets)
and \
749 set([p.uniqueId()
for p
in one]) == set([p.uniqueId()
for p
in two])
763 def __init__(self, parent=None, header=[], donnees=None):
764 QAbstractTableModel.__init__(self,parent)
775 self.dataChanged.emit(self.index(0,column), self.index(len(self.
donnees)-1, column))
776 self.pere.t.viewport().update()
793 if index.column()==0:
794 self.
donnees[index.row()].selected=value
797 return QAbstractTableModel.setData(self, index, role)
805 return self.
donnees[index.row()][-1]
808 if not index.isValid():
810 elif role==Qt.ToolTipRole:
812 h=self.pere.header[c]
814 return QApplication.translate(
"Main",
"Cocher ou décocher cette case en cliquant.<br><b>Double-clic</b> pour agir sur plusieurs baladeurs.",
None)
816 return QApplication.translate(
"Main",
"Propriétaire de la clé USB ou du baladeur ;<br><b>Double-clic</b> pour modifier.",
None)
818 return QApplication.translate(
"Main",
"Point de montage de la clé USB ou du baladeur ;<br><b>Double-clic</b> pour voir les fichiers.",
None)
819 elif "capacity" in h:
820 return QApplication.translate(
"Main",
"Capacité de la clé USB ou du baladeur en kO ;<br><b>Double-clic</b> pour voir la place occupée.",
None)
822 return QApplication.translate(
"Main",
"Fabricant de la clé USB ou du baladeur.",
None)
824 return QApplication.translate(
"Main",
"Modèle de la clé USB ou du baladeur.",
None)
826 return QApplication.translate(
"Main",
"Numéro de série de la clé USB ou du baladeur.",
None)
829 elif role != Qt.DisplayRole:
831 if index.row()<len(self.
donnees):
833 return QVariant(self.
donnees[index.row()][index.column()])
835 print(
"Le bug du retrait de clé non détecté a encore frappé, quand sera-t-il éliminé ?")
836 self.pere.findAllDisks()
843 if orientation == Qt.Horizontal
and role == Qt.DisplayRole:
844 return QVariant(self.
header[section])
845 elif orientation == Qt.Vertical
and role == Qt.DisplayRole:
846 return QVariant(section+1)
854 def sort(self, Ncol, order=Qt.DescendingOrder):
855 self.layoutAboutToBeChanged.emit()
856 self.
donnees = sorted(self.
donnees, key=operator.itemgetter(Ncol))
857 if order == Qt.DescendingOrder:
858 self.donnees.reverse()
859 self.layoutChanged.emit()
868 check_box_style_option=QStyleOptionButton()
869 check_box_rect = QApplication.style().subElementRect(QStyle.SE_CheckBoxIndicator,check_box_style_option)
870 check_box_point=QPoint(view_item_style_options.rect.x() + view_item_style_options.rect.width() / 2 - check_box_rect.width() / 2, view_item_style_options.rect.y() + view_item_style_options.rect.height() / 2 - check_box_rect.height() / 2)
871 return QRect(check_box_point, check_box_rect.size())
875 QStyledItemDelegate.__init__(self,parent)
877 def paint(self, painter, option, index):
878 checked = bool(index.model().data(index, Qt.DisplayRole))
879 check_box_style_option=QStyleOptionButton()
880 check_box_style_option.state |= QStyle.State_Enabled
882 check_box_style_option.state |= QStyle.State_On
884 check_box_style_option.state |= QStyle.State_Off
886 QApplication.style().drawControl(QStyle.CE_CheckBox, check_box_style_option, painter)
889 if ((event.type() == QEvent.MouseButtonRelease)
or (event.type() == QEvent.MouseButtonDblClick)):
890 if (event.button() != Qt.LeftButton
or not CheckBoxRect(option).contains(event.pos())):
892 if (event.type() == QEvent.MouseButtonDblClick):
894 elif (event.type() == QEvent.KeyPress):
895 if event.key() != Qt.Key_Space
and event.key() != Qt.Key_Select:
899 checked = bool(index.model().data(index, Qt.DisplayRole))
900 result = model.setData(index,
not checked, Qt.EditRole)
912 QStyledItemDelegate.__init__(self,parent)
913 self.
okPixmap=QPixmap(
"/usr/share/icons/Tango/16x16/status/weather-clear.png")
914 self.
busyPixmap=QPixmap(
"/usr/share/icons/Tango/16x16/actions/view-refresh.png")
916 def paint(self, painter, option, index):
918 text = index.model().data(index, Qt.DisplayRole).value()
919 rect0=QRect(option.rect)
920 rect1=QRect(option.rect)
923 rect0.setSize(QSize(h,h))
925 rect1.setSize(QSize(w-h,h))
926 QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette,
True, text)
927 QApplication.style().drawItemText (painter, rect0, Qt.AlignCenter, option.palette,
True,
"O")
928 if text
in activeThreads:
929 QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.
busyPixmap)
931 QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.
okPixmap)
941 QStyledItemDelegate.__init__(self,parent)
944 def paint(self, painter, option, index):
945 v=index.model().data(index, Qt.DisplayRole)
948 rect0=QRect(option.rect)
949 rect1=QRect(option.rect)
950 rect0.translate(2,(rect0.height()-16)/2)
951 rect0.setSize(QSize(16,16))
952 rect1.translate(20,0)
953 rect1.setWidth(rect1.width()-20)
954 QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette,
True, text)
956 mount=index.model().partition(index).mountPoint()
957 dev,total,used,remain,pcent,path = self.parent().diskSizeData(mount)
958 pcent=int(pcent[:-1])
959 painter.setBrush(QBrush(QColor(
"slateblue")))
960 painter.drawPie(rect0,0,16*360*pcent/100)
967 suffixes=[
"B",
"KB",
"MB",
"GB",
"TB"]
970 while val > 1024
and i < len(suffixes):
973 return "%4.1f %s" %(val, suffixes[i])
def columnCount(self, parent)
un QModelIndex
def partition(self, index)
defines the main window of the application.
def updateButtons(self)
Désactive ou active les flèches selon que l'option correspondante est possible ou non...
def registerCmd(cmd, partition)
enregistre la commande cmd pour la partition donnée
def paint(self, painter, option, index)
def pushCmd(self, owner, cmd)
fonction de rappel déclenchée par les threads (au commencement)
def editOwner(self, idx)
Édition du propriétaire d'une clé.
def checkAll(self)
Coche tous les baladeurs.
def checkNone(self)
Décoche tous les baladeurs.
def findAllDisks
Initialisation du catalogue des disques USB connectés, et maintenance de l'interface graphique...
def checkModify(self, boolFunc)
Une classe qui fournit une collection de disques USB connectés, avec leurs propriétaires.
def diskSizeData(self, rowOrDev)
def setData(self, index, value, role)
def diskFromOwner(self, student)
trouve le disque qui correspond à un propriétaire, ou alors renvoie le premier disque inconnu...
def connectTableModel(self, data)
Connecte le modèle de table à la table.
def updateOwnerColumn(self)
force la mise à jour de la colonne des propriétaires
def copyTo(self)
Lance l'action de copier vers les clés USB.
def tableClicked(self, idx)
fonction de rappel pour un double clic sur un élément de la table
Un dialogue pour choisir un ensemble de fichiers à transférer vers une collection de clés USB...
def copyFrom(self)
Lance l'action de copier depuis les clés USB.
def namingADrive(self)
Gère un dialogue pour renommer un baladeur désigné par self.recentConnect.
def __init__
Le constructeur.
Un dialogue pour gérer les cases à cocher de l'application.
implémente un dialogue permettant de choisir des élèves les propriétés importantes sont self...
def __init__(self, parent)
def headerData(self, section, orientation, role)
def sameDiskData(self, one, two)
def rowCount(self, parent)
un QModelIndex
def initRedoStuff(self)
Initialise des données pour le bouton central (refaire/stopper)
un dialogue pour renommer un baladeur, compte tenu d'une liste de noms disponibles ...
def popCmd(self, owner, cmd)
fonction de rappel déclenchée par les threads (à la fin)
def __init__(self, parent)
def setAvailableNames(self, available)
Met à jour l'icône qui reflète la disponibilité de noms pour renommer automatiquement des baladeurs...
def deviceRemoved(self)
fonction de rappel pour un medium retiré ; se base sur la valeur de self.recentDisConnect ...
def preference(self)
lance le dialogue des préférences
def setThemedIcon
Associe une icone à un bouton, dans le thème courant.
def safePath(obj)
Récupère de façon sûre le path d'une instance de UDisksObjectProxy.
def changeWd(self, newDir)
change le répertoire par défaut contenant les fichiers de travail
def applyPreferences(self)
Applique les préférences et les options de ligne de commande.
def __init__(self, parent)
def data(self, index, role)
def CheckBoxRect(view_item_style_options)
def help(self)
Affiche le widget d'aide.
def umount(self)
Démonte et détache les clés USB affichées.
def sort
Sort table by given column number.
def editorEvent(self, event, model, option, index)
def cbRemoved(self)
Renvoie une fonction de rappel pour l'abonnement aux évènements de l'arrière-boutique.
Classe pour identifier le baladeur dans le tableau.
def namesCmd(self)
montre le dialogue de choix de nouveaux noms à partir d'un fichier administratif. ...
def redoCmd(self)
Relance la dernière commande, mais en l'appliquant seulement aux baladeurs nouvellement branchés...
def manageCheckBoxes(self)
ouvre un dialogue pour permettre de gérer les cases à cocher globalement
def paint(self, painter, option, index)
def paint(self, painter, option, index)
def checkToggle(self)
Inverse la coche des baladeurs.
Classe pour figurer la taille de la mémoire du baladeur.
def delFiles(self)
Lance l'action de supprimer des fichiers ou des répertoires dans les clés USB.
Un modèle de table pour des séries de clés USB.
def cbAdded(self)
Renvoie une fonction de rappel pour l'abonnement aux évènements de l'arrière-boutique.
def deviceAdded(self)
Fonction de rappel pour un medium ajouté ; se base sur la valeur de self.recentConnect.
Un dialogue pour choisir un ensemble de fichiers à copier depuis une clé USB.