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