ScolaSync  5.1
mainWindow.py
Aller à la documentation de ce fichier.
1 #!/usr/bin/python
2 # $Id: mainWindow.py 47 2011-06-13 10:20:14Z georgesk $
3 
4 licence={}
5 licence['en']="""
6  file mainWindow.py
7  this file is part of the project scolasync
8 
9  Copyright (C) 2010-2014 Georges Khaznadar <georgesk@ofset.org>
10 
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.
15 
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.
20 
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/>.
23 """
24 
25 
26 from PyQt5.QtGui import *
27 from PyQt5.QtCore import *
28 from PyQt5.QtWidgets import *
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
34 import db
35 import choixEleves
36 import nameAdrive
37 from globaldef import logFileName, _dir
38 
39 # cette donnée est globale, pour être utilisé depuis n'importe quel objet
40 qApp.available=ownedUsbDisk.Available(access="firstFat")
41 
42 activeThreads={} # donnée globale : les threads actifs
43 # cette donnée est mise à jour par des signaux émis au niveau des threads
44 # et elle est utilisée par la routine de traçage des cases du tableau
45 pastCommands={} # donnée globale : les commandes réalisées dans le passé
46 lastCommand=None # donnée globale : la toute dernière commande
47 
48 ##
49 #
50 # enregistre la commande cmd pour la partition donnée
51 # @param cmd une commande pour créer un thread t
52 # @param partition une partition
53 #
54 def registerCmd(cmd,partition):
55  global pastCommands, lastCommand
56  if cmd in pastCommands:
57  pastCommands[cmd].append(partition.owner)
58  else:
59  pastCommands[cmd]=[partition.owner]
60  lastCommand=cmd
61 
62 ##
63 #
64 # defines the main window of the application.
65 #
67  ############# custom signals ########################
68  checkAllSignal=pyqtSignal()
69  checkToggleSignal=pyqtSignal()
70  checkNoneSignal=pyqtSignal()
71  shouldNameDrive=pyqtSignal()
72  pushCmdSignal=pyqtSignal(str, str)
73  popCmdSignal=pyqtSignal(str, str)
74 
75  ##
76  #
77  # Le constructeur
78  # @param parent un QWidget
79  # @param locale la langue de l'application
80  #
81  def __init__(self, parent, locale="fr_FR"):
82  QMainWindow.__init__(self)
83  QWidget.__init__(self, parent)
84  self.locale=locale
85  from Ui_mainWindow import Ui_MainWindow
86  self.ui = Ui_MainWindow()
87  self.ui.setupUi(self)
88  QIcon.setThemeName("Tango")
89  icon=self.setThemedIcon(self.ui.fromButton,"back")
90  self.copyfromIcon=icon
91  self.movefromIcon=QIcon.fromTheme("movefrom",QIcon("/usr/share/scolasync/images/movefrom.png"))
92  self.setThemedIcon(self.ui.toButton,"forward")
93  self.setThemedIcon(self.ui.delButton,"edit-clear")
94  self.setThemedIcon(self.ui.umountButton,"top")
95  self.setThemedIcon(self.ui.redoButton,"go-jump")
96  self.setThemedIcon(self.ui.namesButton,"gtk-find")
97  self.setThemedIcon(self.ui.forceCheckButton,"multimedia-player")
98  self.setThemedIcon(self.ui.preferenceButton,"package_settings")
99  self.setThemedIcon(self.ui.helpButton,"info")
100  # crée le dialogue des nouveaux noms
101  self.namesFullIcon=QIcon.fromTheme("gtk-find-and-replace.svg")
102  self.namesEmptyIcon=QIcon.fromTheme("gtk-find")
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)
106  self.recentConnect="" # chemin dbus pour un baladeur récemment connecté
107  # initialise deux icônes
108  self.initRedoStuff()
109  # initialise le tableau
110  self.t=self.ui.tableView
111  self.proxy=QSortFilterProxyModel()
112  self.proxy.setSourceModel(self.t.model())
113  self.applyPreferences()
114  self.updateButtons()
115  self.setAvailableNames(False)
116  self.operations=[] # liste des opérations précédemment "réussies"
117  self.oldThreads=set() # threads lancés éventuellement encore vivants
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)
128  self.checkToggleSignal.connect(self.checkToggle)
129  self.checkNoneSignal.connect(self.checkNone)
130  self.shouldNameDrive.connect(self.namingADrive)
131  ## accrochage d'une fonction de rappel pour les disque ajoutés
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)
136  return
137 
138  ##
139  #
140  # Associe une icone à un bouton, dans le thème courant
141  # @param button le bouton à décorer
142  # @param name le nom de l'icone
143  # @param default un fichier PNG ; si rien n'est donné, il aura
144  # comme valeur par défaut "images/icons32/"+name+".png"
145  # @return l'objet de type QIcon qui a été associé au bouton
146  #
147  def setThemedIcon(self, button, name, default=None):
148  icon=QIcon()
149  try:
150  icon.addPixmap(QIcon.fromTheme(name).pixmap(32))
151  except:
152  icon.addPixmap("images/icons32/"+name+".png")
153  button.setIcon(icon)
154  return button.icon()
155 
156  ##
157  #
158  # fonction de rappel déclenchée par les threads (au commencement)
159  # @param owner le propriétaire du baladeur associé au thread
160  # @param cmd la commande shell effectuée sur ce baladeur
161  #
162  def pushCmd(self,owner,cmd):
163  global activeThreads, pastCommands, lastCommand
164  if owner in activeThreads:
165  activeThreads[owner].append(cmd)
166  else:
167  activeThreads[owner]=[cmd]
168  self.tm.updateOwnerColumn()
169  self.updateButtons()
170 
171  ##
172  #
173  # fonction de rappel déclenchée par les threads (à la fin)
174  # @param owner le propriétaire du baladeur associé au thread
175  # @param cmd la commande shell effectuée sur ce baladeur
176  #
177  def popCmd(self,owner, cmd):
178  global activeThreads, pastCommands, lastCommand
179  if owner in activeThreads:
180  cmd0=activeThreads[owner].pop()
181  if cmd0 in cmd:
182  msg=cmd.replace(cmd0,"")+"\n"
183  logFile=open(os.path.expanduser(logFileName),"a")
184  logFile.write(msg)
185  logFile.close()
186  else:
187  raise Exception(("mismatched commands\n%s\n%s" %(cmd,cmd0)))
188  if len(activeThreads[owner])==0:
189  activeThreads.pop(owner)
190  else:
191  raise Exception("End of command without a begin.")
192  self.tm.updateOwnerColumn()
193  if len(activeThreads)==0 :
194  self.updateButtons()
195 
196  ##
197  #
198  # @param boolfunc une fonction pour décider du futur état de la coche
199  # étant donné l'état antérieur
200  # Modifie les coches des baladeurs
201  #
202  def checkModify(self, boolFunc):
203  model=self.tm
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)
210 
211  ##
212  #
213  # Coche tous les baladeurs
214  #
215  def checkAll(self):
216  self.checkModify(lambda x: True)
217 
218  ##
219  #
220  # Inverse la coche des baladeurs
221  #
222  def checkToggle(self):
223  self.checkModify(lambda x: not x)
224 
225  ##
226  #
227  # Décoche tous les baladeurs
228  #
229  def checkNone(self):
230  self.checkModify(lambda x: False)
231 
232  ##
233  #
234  # Gère un dialogue pour renommer un baladeur désigné par
235  # self.recentConnect
236  #
237  def namingADrive(self):
238  if self.availableNames:
239  if self.recentConnect not in qApp.available.targets:
240  return
241  disk=qApp.available.targets[self.recentConnect]
242  hint=db.readStudent(disk.serial, disk.uuid, ownedUsbDisk.tattooInDir(disk.mp))
243  if hint != None:
244  oldName=hint
245  else:
246  oldName=""
247  d=nameAdrive.nameAdriveDialog(self, oldName=oldName,
248  nameList=self.namesDialog.itemStrings(),
249  driveIdent=(stickId, uuid, tattoo))
250  d.show()
251  result=d.exec_()
252  return
253 
254  ##
255  #
256  # Renvoie une fonction de rappel pour l'abonnement aux évènements de l'arrière-boutique.
257  # Il s'agit de la fonction pour les disques branchés
258  #
259  def cbAdded(self):
260  def _cbAdded(man, obj):
261  if qApp.available.modified:
262  path=safePath(obj)
263  self.recentConnect=str(path)
264  delai=0.5 # petit délai pour que targets soit à jour
265  QTimer.singleShot(delai, self.deviceAdded)
266  qApp.available.modified=False
267  return _cbAdded
268 
269  ##
270  #
271  # Renvoie une fonction de rappel pour l'abonnement aux évènements de l'arrière-boutique.
272  # Il s'agit de la fonction pour les disques débranchés
273  #
274  def cbRemoved(self):
275  def _cbRemoved(man, obj):
276  if qApp.available.modified:
277  path=safePath(obj)
278  if path in qApp.available.targets:
280  delai=0.5 # petit délai pour que targets soit à jour
281  QTimer.singleShot(delai, self.deviceRemoved)
282  qApp.available.modified=False
283  return _cbRemoved
284 
285  ##
286  #
287  # Fonction de rappel pour un medium ajouté ; se base sur la valeur de self.recentConnect
288  #
289  def deviceAdded(self):
290  if self.recentConnect not in qApp.available.targets:
291  return
292  disk=qApp.available.targets[self.recentConnect]
293  if disk.parent: # c'est une partition
294  QTimer.singleShot(0, self.namingADrive)
295  self.findAllDisks()
296 
297  ##
298  #
299  # fonction de rappel pour un medium retiré ; se base sur la valeur de self.recentDisConnect
300  #
301  def deviceRemoved(self):
302  self.findAllDisks()
303 
304  ##
305  #
306  # Initialise des données pour le bouton central (refaire/stopper)
307  #
308  def initRedoStuff(self):
309  # réserve les icônes
310  self.iconRedo = QIcon()
311  self.iconRedo.addPixmap(QIcon.fromTheme("go-jump").pixmap(32), QIcon.Normal, QIcon.Off)
312  self.iconStop = QIcon()
313  self.iconStop.addPixmap(QIcon.fromTheme("stop").pixmap(32), QIcon.Normal, QIcon.Off)
314  # réserve les phrases d'aide
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)
319 
320  ##
321  #
322  # Applique les préférences et les options de ligne de commande
323  #
324  def applyPreferences(self):
325  prefs=db.readPrefs()
326  self.schoolFile=prefs["schoolFile"]
327  self.workdir=prefs["workdir"]
328  self.manFileLocation=prefs["manfile"]
329  self.mv=prefs["mv"]
330  self.header=ownedUsbDisk.uDisk2.headers()
331  self.findAllDisks()
332  return
333 
334  ##
335  #
336  # Initialisation du catalogue des disques USB connectés, et
337  # maintenance de l'interface graphique.
338  # @param other un catalogue déjà tout prêt de disques (None par défaut)
339  #
340  def findAllDisks(self, other=None):
341  if other:
342  qApp.available=other
343  else:
344  qApp.available=ownedUsbDisk.Available(access="firstFat")
345  self.connectTableModel(qApp.available)
346  connectedCount=int(qApp.available)
347  self.ui.lcdNumber.display(connectedCount)
348  self.t.resizeColumnsToContents()
349  self.updateButtons()
350  return
351 
352  ##
353  #
354  # change le répertoire par défaut contenant les fichiers de travail
355  # @param newDir le nouveau nom de répertoire
356  #
357  def changeWd(self, newDir):
358  self.workdir=newDir
359  db.setWd(newDir)
360 
361  ##
362  #
363  # fonction de rappel pour un double clic sur un élément de la table
364  # @param idx un QModelIndex
365  #
366  def tableClicked(self, idx):
367  c=idx.column()
368  mappedIdx=self.proxy.mapFromSource(idx)
369  r=mappedIdx.row()
370  h=self.header[c]
371  if c==0:
372  self.manageCheckBoxes()
373  pass
374  elif c==1:
375  # case du propriétaire
376  self.editOwner(mappedIdx)
377  elif "mp" in h:
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])
384  w=diskFull.mainWindow(self,pcent,title=path, total=total, used=used)
385  w.show()
386  else:
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))
390 
391  ##
392  #
393  # ouvre un dialogue pour permettre de gérer les cases à cocher globalement
394  #
395  def manageCheckBoxes(self):
396  cbDialog=checkBoxDialog.CheckBoxDialog(self)
397  cbDialog.exec_()
398 
399  ##
400  #
401  # @param rowOrDev a row number in the tableView, or a device string
402  # @return a tuple dev,total,used,remain,pcent,path for the
403  # disk in the given row of the tableView
404  # (the tuple comes from the command df)
405  #
406  def diskSizeData(self, rowOrDev):
407  if type(rowOrDev)==type(0):
408  path=qApp.available[rowOrDev][self.header.index("1mp")]
409  else:
410  path=rowOrDev
411  cmd ="df '%s'" %path
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()
415  return m
416 
417 
418  ##
419  #
420  # trouve le disque qui correspond à un propriétaire, ou alors
421  # renvoie le premier disque inconnu.
422  # @param student le propriétaire du disque
423  # @return le disque correspondant à l'étudiant
424  #
425  def diskFromOwner(self,student):
426  defaultDisk=None
427  for d in ownedUsbDisk.Available(access="firstFat"):
428  s=db.readStudent(d.stickid, d.uuid, d.tattoo())
429  if s==student :
430  return d
431  elif s==None and defaultDisk==None :
432  # premier disque inconnu
433  defaultDisk=d
434  return defaultDisk
435 
436 
437  ##
438  #
439  # Édition du propriétaire d'une clé.
440  # @param idx un QModelIndex qui pointe sur le propriétaire d'une clé
441  #
442  def editOwner(self, idx):
443  student="%s" %self.tm.data(idx,Qt.DisplayRole).value()
444  # on fait une modification dans la base de donnée des propriétaires de clés
445  ownedUsbDisk.editRecord(self.diskFromOwner(student), hint=student)
446  # après quoi on relit brutalement toute la list des clés connectées
447  self.findAllDisks()
448 
449  ##
450  #
451  # Met à jour l'icône qui reflète la disponibilité de noms pour
452  # renommer automatiquement des baladeurs
453  # @param available vrai s'il y a des noms disponibles pour
454  # renommer des baladeurs.
455  #
456  def setAvailableNames(self, available):
457  self.availableNames=available
458  if available:
459  icon=self.namesFullIcon
460  msg=self.namesFullTip
461  else:
462  icon=self.namesEmptyIcon
463  msg=self.namesEmptyTip
464  self.ui.namesButton.setIcon(icon)
465  self.ui.namesButton.setToolTip(msg)
466  self.ui.namesButton.setStatusTip(msg.replace("<br />",""))
467 
468  ##
469  #
470  # Désactive ou active les flèches selon que l'option correspondante
471  # est possible ou non. Pour les flèches : ça aurait du sens de préparer
472  # une opération de copie avant même de brancher des clés, donc on les
473  # active. Par contre démonter les clés quand elles sont absentes ça
474  # n'a pas d'utilité.
475  # Change l'icône du dialogue des noms selon qu'il reste ou non des
476  # noms disponibles dans le dialogue des noms.
477  #
478  def updateButtons(self):
479  global activeThreads, lastCommand
480  active = len(qApp.available)>0
481  for button in (self.ui.toButton,
482  self.ui.fromButton,
483  self.ui.delButton,
484  self.ui.umountButton):
485  button.setEnabled(active)
486  #modifie l'icone copyfrom/movefrom
487  if self.mv:
488  self.ui.fromButton.setIcon(self.movefromIcon)
489  else:
490  self.ui.fromButton.setIcon(self.copyfromIcon)
491  # l'état du redoButton dépend de plusieurs facteurs
492  # si un thread au moins est en cours, on y affiche un STOP actif
493  # sinon on y met l'icône de lastCommand, et celle-ci sera active
494  # seulement s'il y a une commande déjà validée
495  if len(activeThreads) > 0:
496  self.ui.redoButton.setIcon(self.iconStop)
497  self.ui.redoButton.setToolTip(self.stopToolTip)
498  self.ui.redoButton.setStatusTip(self.stopStatusTip)
499  self.ui.redoButton.setEnabled(True)
500  else:
501  self.oldThreads=set() # vide l'ensemble puisque tout est fini
502  self.ui.redoButton.setIcon(self.iconRedo)
503  self.ui.redoButton.setToolTip(self.redoToolTip)
504  self.ui.redoButton.setStatusTip(self.redoStatusTip)
505  self.ui.redoButton.setEnabled(lastCommand!=None)
506  l=self.namesDialog.ui.listWidget.findItems("*",Qt.MatchWildcard)
507  if len(l)>0:
508  self.ui.namesButton.setIcon(self.namesFullIcon)
509  else:
510  self.ui.namesButton.setIcon(self.namesEmptyIcon)
511 
512  ##
513  #
514  # lance le dialogue des préférences
515  #
516  def preference(self):
518  pref.setValues(db.readPrefs())
519  pref.show()
520  pref.exec_()
521  if pref.result()==QDialog.Accepted:
522  db.writePrefs(pref.values())
523  # on applique les préférences tout de suite sans redémarrer
524  self.applyPreferences()
525 
526  ##
527  #
528  # Lance l'action de supprimer des fichiers ou des répertoires dans les clés USB
529  #
530  def delFiles(self):
531  titre1=QApplication.translate("Dialog","Choix de fichiers à supprimer",None)
532  titre2=QApplication.translate("Dialog","Choix de fichiers à supprimer (jokers autorisés)",None)
533  d=chooseInSticks.chooseDialog(self, titre1, titre2)
534  ok = d.exec_()
535  if ok:
536  pathList=d.pathList()
537  buttons=QMessageBox.Ok|QMessageBox.Cancel
538  defaultButton=QMessageBox.Cancel
539  reply=QMessageBox.warning(
540  None,
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 # pas les médias désélectionnés
548  registerCmd(cmd,p)
549  t=eval(cmd)
550  t.setDaemon(True)
551  t.start()
552  self.oldThreads.add(t)
553  return True
554  else:
555  msgBox=QMessageBox.warning(
556  None,
557  QApplication.translate("Dialog","Aucun fichier sélectionné",None),
558  QApplication.translate("Dialog","Veuillez choisir au moins un fichier",None))
559  return True
560 
561  ##
562  #
563  # Lance l'action de copier vers les clés USB
564  #
565  def copyTo(self):
566  d=copyToDialog1.copyToDialog1(parent=self, workdir=self.workdir)
567  d.exec_()
568  if d.ok==True:
569  cmd="usbThread.threadCopyToUSB(p,{selected},subdir='{subdir}', logfile='{logfile}', parent=self)".format(selected=list(d.selectedList()), subdir=self.workdir, logfile=logFileName)
570  ## !!!!!!!!!!!!!!!!! itérations dans qApp.available à revoir !
571  for p in qApp.available:
572  if not p.selected: continue # pas les médias désélectionnés
573  registerCmd(cmd,p)
574  t=eval(cmd)
575  t.setDaemon(True)
576  t.start()
577  self.oldThreads.add(t)
578  return True
579  else:
580  msgBox=QMessageBox.warning(
581  None,
582  QApplication.translate("Dialog","Aucun fichier sélectionné",None),
583  QApplication.translate("Dialog","Veuillez choisir au moins un fichier",None))
584  return True
585 
586  ##
587  #
588  # Lance l'action de copier depuis les clés USB
589  #
590  def copyFrom(self):
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)
594  d=chooseInSticks.chooseDialog(self, title1=titre1, title2=titre2, okPrompt=okPrompt)
595  d.exec_()
596  if not d.ok :
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))
600  return True
601  # bon, alors c'est OK pour le choix des fichiers à envoyer
602  pathList=d.pathList()
603  mp=d.selectedDiskMountPoint()
604  initialPath=os.path.expanduser("~")
605  destDir = QFileDialog.getExistingDirectory(
606  None,
607  QApplication.translate("Dialog","Choisir un répertoire de destination",None),
608  initialPath)
609  if destDir and len(destDir)>0 :
610  if self.mv:
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)
615  else:
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)
620 
621  for p in qApp.available:
622  if not p.selected: continue # pas les médias désélectionnés
623  # on devrait vérifier s'il y a des données à copier
624  # et s'il n'y en a pas, ajouter des lignes au journal
625  # mais on va laisser faire ça dans le thread
626  # inconvénient : ça crée quelquefois des sous-répertoires
627  # vides inutiles dans le répertoire de destination.
628  registerCmd(cmd,p)
629  t=eval(cmd)
630  t.setDaemon(True)
631  t.start()
632  self.oldThreads.add(t)
633  # on ouvre un gestionnaire de fichiers pour voir le résultat
634  buttons=QMessageBox.Ok|QMessageBox.Cancel
635  defaultButton=QMessageBox.Cancel
636  if QMessageBox.question(
637  None,
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)
642  return True
643  else:
644  msgBox=QMessageBox.warning(
645  None,
646  QApplication.translate("Dialog","Destination manquante",None),
647  QApplication.translate("Dialog","Veuillez choisir une destination pour la copie des fichiers",None))
648  return True
649 
650  ##
651  #
652  # Relance la dernière commande, mais en l'appliquant seulement aux
653  # baladeurs nouvellement branchés.
654  #
655  def redoCmd(self):
656  global lastCommand, pastCommands, activeThreads
657  if len(activeThreads)>0:
658  for thread in self.oldThreads:
659  if thread.isAlive():
660  try:
661  thread._Thread__stop()
662  print (str(thread.getName()) + ' is terminated')
663  except:
664  print (str(thread.getName()) + ' could not be terminated')
665  else:
666  if lastCommand==None:
667  return
668  if QMessageBox.question(
669  None,
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:
672  return
673  for p in qApp.available:
674  if p.owner in pastCommands[lastCommand] : continue
675  exec(compile(lastCommand,'<string>','exec'))
676  t.setDaemon(True)
677  t.start()
678  self.oldThreads.add(t)
679  pastCommands[lastCommand].append(p.owner)
680 
681  ##
682  #
683  # montre le dialogue de choix de nouveaux noms à partir d'un
684  # fichier administratif.
685  #
686  def namesCmd(self):
687  self.namesDialog.show()
688 
689  ##
690  #
691  # Affiche le widget d'aide
692  #
693  def help(self):
694  w=help.helpWindow(self)
695  w.show()
696  w.exec_()
697 
698  ##
699  #
700  # Démonte et détache les clés USB affichées
701  #
702  def umount(self):
703  buttons=QMessageBox.Ok|QMessageBox.Cancel
704  defaultButton=QMessageBox.Cancel
705  button=QMessageBox.question (
706  self,
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:
711  return
712  for d in qApp.available.disks_ud():
713  for partition in qApp.available.parts_ud(d.path):
714  if partition.mp:
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)
719  self.findAllDisks() # remet à jour le compte de disques
720  self.operations=[] # remet à zéro la liste des opérations
721 
722 
723  ##
724  #
725  # Connecte le modèle de table à la table
726  # @param data les données de la table
727  #
728  def connectTableModel(self, data):
730  for h in self.header:
731  if h in ownedUsbDisk.uDisk2._itemNames:
732  self.visibleheader.append(self.tr(ownedUsbDisk.uDisk2._itemNames[h]))
733  else:
734  self.visibleheader.append(h)
735  self.tm=usbTableModel(self, self.visibleheader, data)
736  self.t.setModel(self.tm)
737  self.t.setItemDelegateForColumn(0, CheckBoxDelegate(self))
738  self.t.setItemDelegateForColumn(1, UsbDiskDelegate(self))
739  self.t.setItemDelegateForColumn(3, DiskSizeDelegate(self))
740  self.proxy.setSourceModel(self.t.model())
741 
742 
743  ##
744  #
745  # @return True si les ensembles de uniqueId de one et two sont identiques
746  #
747  def sameDiskData(self, one, two):
748  return len(one.targets) == len(two.targets) and \
749  set([p.uniqueId() for p in one]) == set([p.uniqueId() for p in two])
750 
751 ##
752 #
753 # Un modèle de table pour des séries de clés USB
754 #
756 
757  ##
758  #
759  # @param parent un QObject
760  # @param header les en-têtes de colonnes
761  # @param donnees les données
762  #
763  def __init__(self, parent=None, header=[], donnees=None):
764  QAbstractTableModel.__init__(self,parent)
765  self.header=header
766  self.donnees=donnees
767  self.pere=parent
768 
769  ##
770  #
771  # force la mise à jour de la colonne des propriétaires
772  #
773  def updateOwnerColumn(self):
774  column=1
775  self.dataChanged.emit(self.index(0,column), self.index(len(self.donnees)-1, column))
776  self.pere.t.viewport().update()
777 
778  ##
779  #
780  # @parent un QModelIndex
781  #
782  def rowCount(self, parent):
783  return len(self.donnees)
784 
785  ##
786  #
787  # @parent un QModelIndex
788  #
789  def columnCount(self, parent):
790  return len(self.header)
791 
792  def setData(self, index, value, role):
793  if index.column()==0:
794  self.donnees[index.row()].selected=value
795  return True
796  else:
797  return QAbstractTableModel.setData(self, index, role)
798 
799  ##
800  #
801  # @param index in QModelIndex
802  # @return la partition pointée par index
803  #
804  def partition(self, index):
805  return self.donnees[index.row()][-1]
806 
807  def data(self, index, role):
808  if not index.isValid():
809  return QVariant()
810  elif role==Qt.ToolTipRole:
811  c=index.column()
812  h=self.pere.header[c]
813  if c==0:
814  return QApplication.translate("Main","Cocher ou décocher cette case en cliquant.<br><b>Double-clic</b> pour agir sur plusieurs baladeurs.",None)
815  elif c==1:
816  return QApplication.translate("Main","Propriétaire de la clé USB ou du baladeur ;<br><b>Double-clic</b> pour modifier.",None)
817  elif "mp" in h:
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)
821  elif "vendor" in h:
822  return QApplication.translate("Main","Fabricant de la clé USB ou du baladeur.",None)
823  elif "model" in h:
824  return QApplication.translate("Main","Modèle de la clé USB ou du baladeur.",None)
825  elif "stickid" in h:
826  return QApplication.translate("Main","Numéro de série de la clé USB ou du baladeur.",None)
827  else:
828  return ""
829  elif role != Qt.DisplayRole:
830  return QVariant()
831  if index.row()<len(self.donnees):
832  try:
833  return QVariant(self.donnees[index.row()][index.column()])
834  except KeyError:
835  print("Le bug du retrait de clé non détecté a encore frappé, quand sera-t-il éliminé ?")
836  self.pere.findAllDisks()
837  return QVariant("")
838 
839  else:
840  return QVariant()
841 
842  def headerData(self, section, orientation, role):
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)
847  return QVariant()
848 
849  ##
850  # Sort table by given column number.
851  # @param Ncol numéro de la colonne de tri
852  # @param order l'odre de tri, Qt.DescendingOrder par défaut
853  #
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()
860 
861 ##
862 #
863 # @param view_item_style_options des options permettant de décider de
864 # la taille d'un rectangle
865 # @return un QRect dimensionné selon les bonnes options
866 #
867 def CheckBoxRect(view_item_style_options):
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())
872 
874  def __init__(self, parent):
875  QStyledItemDelegate.__init__(self,parent)
876 
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
881  if checked:
882  check_box_style_option.state |= QStyle.State_On
883  else:
884  check_box_style_option.state |= QStyle.State_Off
885  check_box_style_option.rect = CheckBoxRect(option);
886  QApplication.style().drawControl(QStyle.CE_CheckBox, check_box_style_option, painter)
887 
888  def editorEvent(self, event, model, option, index):
889  if ((event.type() == QEvent.MouseButtonRelease) or (event.type() == QEvent.MouseButtonDblClick)):
890  if (event.button() != Qt.LeftButton or not CheckBoxRect(option).contains(event.pos())):
891  return False
892  if (event.type() == QEvent.MouseButtonDblClick):
893  return True
894  elif (event.type() == QEvent.KeyPress):
895  if event.key() != Qt.Key_Space and event.key() != Qt.Key_Select:
896  return False
897  else:
898  return False
899  checked = bool(index.model().data(index, Qt.DisplayRole))
900  result = model.setData(index, not checked, Qt.EditRole)
901  return result
902 
903 
904 ##
905 #
906 # Classe pour identifier le baladeur dans le tableau.
907 # La routine de rendu à l'écran trace une petite icône et le nom du
908 # propriétaire à côté.
909 #
911  def __init__(self, parent):
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")
915 
916  def paint(self, painter, option, index):
917  global activeThreads
918  text = index.model().data(index, Qt.DisplayRole).value()
919  rect0=QRect(option.rect)
920  rect1=QRect(option.rect)
921  h=rect0.height()
922  w=rect0.width()
923  rect0.setSize(QSize(h,h))
924  rect1.translate(h,0)
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)
930  else:
931  QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.okPixmap)
932 
933 ##
934 #
935 # Classe pour figurer la taille de la mémoire du baladeur. Trace un petit
936 # secteur représentant la place occupée, puis affiche la place avec l'unité
937 # le plus parropriée.
938 #
940  def __init__(self, parent):
941  QStyledItemDelegate.__init__(self,parent)
942 
943 
944  def paint(self, painter, option, index):
945  v=index.model().data(index, Qt.DisplayRole)
946  value = v.value()
947  text = self.val2txt(value)
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)
955  # dessin d'un petit cercle pour l'occupation
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)
961 
962  ##
963  #
964  # @return a string with a value with unit K, M, or G
965  #
966  def val2txt(self, val):
967  suffixes=["B", "KB", "MB", "GB", "TB"]
968  val*=1.0 # calcul flottant
969  i=0
970  while val > 1024 and i < len(suffixes):
971  i+=1
972  val/=1024
973  return "%4.1f %s" %(val, suffixes[i])
974 
def columnCount(self, parent)
un QModelIndex
Definition: mainWindow.py:789
def partition(self, index)
Definition: mainWindow.py:804
defines the main window of the application.
Definition: mainWindow.py:66
def updateButtons(self)
Désactive ou active les flèches selon que l'option correspondante est possible ou non...
Definition: mainWindow.py:478
def registerCmd(cmd, partition)
enregistre la commande cmd pour la partition donnée
Definition: mainWindow.py:54
def paint(self, painter, option, index)
Definition: mainWindow.py:877
def pushCmd(self, owner, cmd)
fonction de rappel déclenchée par les threads (au commencement)
Definition: mainWindow.py:162
def editOwner(self, idx)
Édition du propriétaire d'une clé.
Definition: mainWindow.py:442
def checkAll(self)
Coche tous les baladeurs.
Definition: mainWindow.py:215
def checkNone(self)
Décoche tous les baladeurs.
Definition: mainWindow.py:229
def findAllDisks
Initialisation du catalogue des disques USB connectés, et maintenance de l'interface graphique...
Definition: mainWindow.py:340
def checkModify(self, boolFunc)
Definition: mainWindow.py:202
Une classe qui fournit une collection de disques USB connectés, avec leurs propriétaires.
def diskSizeData(self, rowOrDev)
Definition: mainWindow.py:406
def setData(self, index, value, role)
Definition: mainWindow.py:792
def diskFromOwner(self, student)
trouve le disque qui correspond à un propriétaire, ou alors renvoie le premier disque inconnu...
Definition: mainWindow.py:425
def connectTableModel(self, data)
Connecte le modèle de table à la table.
Definition: mainWindow.py:728
def updateOwnerColumn(self)
force la mise à jour de la colonne des propriétaires
Definition: mainWindow.py:773
def copyTo(self)
Lance l'action de copier vers les clés USB.
Definition: mainWindow.py:565
def tableClicked(self, idx)
fonction de rappel pour un double clic sur un élément de la table
Definition: mainWindow.py:366
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.
Definition: mainWindow.py:590
def namingADrive(self)
Gère un dialogue pour renommer un baladeur désigné par self.recentConnect.
Definition: mainWindow.py:237
def __init__
Le constructeur.
Definition: mainWindow.py:81
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...
Definition: choixEleves.py:39
def __init__(self, parent)
Definition: mainWindow.py:940
def headerData(self, section, orientation, role)
Definition: mainWindow.py:842
def sameDiskData(self, one, two)
Definition: mainWindow.py:747
def rowCount(self, parent)
un QModelIndex
Definition: mainWindow.py:782
def initRedoStuff(self)
Initialise des données pour le bouton central (refaire/stopper)
Definition: mainWindow.py:308
un dialogue pour renommer un baladeur, compte tenu d'une liste de noms disponibles ...
Definition: nameAdrive.py:35
def popCmd(self, owner, cmd)
fonction de rappel déclenchée par les threads (à la fin)
Definition: mainWindow.py:177
def __init__(self, parent)
Definition: mainWindow.py:874
def setAvailableNames(self, available)
Met à jour l'icône qui reflète la disponibilité de noms pour renommer automatiquement des baladeurs...
Definition: mainWindow.py:456
def deviceRemoved(self)
fonction de rappel pour un medium retiré ; se base sur la valeur de self.recentDisConnect ...
Definition: mainWindow.py:301
def preference(self)
lance le dialogue des préférences
Definition: mainWindow.py:516
def setThemedIcon
Associe une icone à un bouton, dans le thème courant.
Definition: mainWindow.py:147
def safePath(obj)
Récupère de façon sûre le path d'une instance de UDisksObjectProxy.
Definition: usbDisk2.py:60
def changeWd(self, newDir)
change le répertoire par défaut contenant les fichiers de travail
Definition: mainWindow.py:357
def applyPreferences(self)
Applique les préférences et les options de ligne de commande.
Definition: mainWindow.py:324
def __init__(self, parent)
Definition: mainWindow.py:911
def data(self, index, role)
Definition: mainWindow.py:807
def CheckBoxRect(view_item_style_options)
Definition: mainWindow.py:867
def help(self)
Affiche le widget d'aide.
Definition: mainWindow.py:693
def umount(self)
Démonte et détache les clés USB affichées.
Definition: mainWindow.py:702
def sort
Sort table by given column number.
Definition: mainWindow.py:854
def editorEvent(self, event, model, option, index)
Definition: mainWindow.py:888
def cbRemoved(self)
Renvoie une fonction de rappel pour l'abonnement aux évènements de l'arrière-boutique.
Definition: mainWindow.py:274
Classe pour identifier le baladeur dans le tableau.
Definition: mainWindow.py:910
def namesCmd(self)
montre le dialogue de choix de nouveaux noms à partir d'un fichier administratif. ...
Definition: mainWindow.py:686
def redoCmd(self)
Relance la dernière commande, mais en l'appliquant seulement aux baladeurs nouvellement branchés...
Definition: mainWindow.py:655
def manageCheckBoxes(self)
ouvre un dialogue pour permettre de gérer les cases à cocher globalement
Definition: mainWindow.py:395
def paint(self, painter, option, index)
Definition: mainWindow.py:916
def paint(self, painter, option, index)
Definition: mainWindow.py:944
def checkToggle(self)
Inverse la coche des baladeurs.
Definition: mainWindow.py:222
Classe pour figurer la taille de la mémoire du baladeur.
Definition: mainWindow.py:939
def delFiles(self)
Lance l'action de supprimer des fichiers ou des répertoires dans les clés USB.
Definition: mainWindow.py:530
Un modèle de table pour des séries de clés USB.
Definition: mainWindow.py:755
def cbAdded(self)
Renvoie une fonction de rappel pour l'abonnement aux évènements de l'arrière-boutique.
Definition: mainWindow.py:259
def deviceAdded(self)
Fonction de rappel pour un medium ajouté ; se base sur la valeur de self.recentConnect.
Definition: mainWindow.py:289
Un dialogue pour choisir un ensemble de fichiers à copier depuis une clé USB.