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