8 this file is part of the project scolasync
10 Copyright (C) 2010 Georges Khaznadar <georgesk@ofset.org>
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.
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.
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/>.
28 import ownedUsbDisk, help, copyToDialog1, chooseInSticks, usbThread
29 import diskFull, preferences, checkBoxDialog
30 import os.path, operator, subprocess, dbus, re, time, copy
31 from notification
import Notification
36 from globaldef
import logFileName, _dir
54 global pastCommands, lastCommand
55 if pastCommands.has_key(cmd):
56 pastCommands[cmd].append(partition.owner)
58 pastCommands[cmd]=[partition.owner]
69 def __init__(self, parent, opts, locale="fr_FR"):
70 QMainWindow.__init__(self)
71 QWidget.__init__(self, parent)
73 from Ui_mainWindow
import Ui_MainWindow
74 self.
ui = Ui_MainWindow()
77 self.
namesFullIcon=QIcon(
"/usr/share/icons/Tango/scalable/actions/gtk-find-and-replace.svg")
78 self.
namesEmptyIcon=QIcon(
"/usr/share/icons/Tango/scalable/actions/gtk-find.svg")
79 self.
namesFullTip=QApplication.translate(
"MainWindow",
"<br />Des noms sont disponibles pour renommer les prochains baladeurs que vous brancherez",
None, QApplication.UnicodeUTF8)
80 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)
86 self.
t=self.ui.tableView
87 self.
proxy=QSortFilterProxyModel()
88 self.proxy.setSourceModel(self.t.model())
99 self.flashTimer.setSingleShot(
True)
101 QObject.connect(self.ui.forceCheckButton, SIGNAL(
"clicked()"), self.
checkDisks)
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)
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)
163 stickId, tattoo, uuid = self.listener.identify(self.
recentConnect)
164 hint=db.readStudent(stickId, uuid, tattoo)
170 nameList=self.namesDialog.itemStrings(),
171 driveIdent=(stickId, uuid, tattoo))
182 vfatPath = self.listener.vfatUsbPath(str(s))
197 if qApp.diskData.hasDev(s):
207 self.iconRedo.addPixmap(QPixmap(
"/usr/share/icons/Tango/scalable/actions/go-jump.svg"), QIcon.Normal, QIcon.Off)
209 self.iconStop.addPixmap(QPixmap(
"/usr/share/icons/Tango/scalable/actions/stop.svg"), QIcon.Normal, QIcon.Off)
211 self.
redoToolTip=QApplication.translate(
"MainWindow",
"Refaire à nouveau",
None, QApplication.UnicodeUTF8)
212 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)
213 self.
stopToolTip=QApplication.translate(
"MainWindow",
"Arrêter les opérations en cours",
None, QApplication.UnicodeUTF8)
214 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)
223 result=QMainWindow.showEvent(self, ev)
275 mappedIdx=self.proxy.mapFromSource(idx)
282 elif c==1
or (c==0
and not self.
checkable):
285 elif "device-mount-paths" in h:
286 cmd=
u"nautilus '%s'" %idx.data().toString ()
287 subprocess.call(cmd, shell=
True)
288 elif "device-size" in h:
289 mount=idx.model().partition(idx).mountPoint()
290 dev,total,used,remain,pcent,path = self.
diskSizeData(mount)
291 pcent=int(pcent[:-1])
295 QMessageBox.warning(
None,
296 QApplication.translate(
"Dialog",
"Double-clic non pris en compte",
None, QApplication.UnicodeUTF8),
297 QApplication.translate(
"Dialog",
"pas d'action pour l'attribut %1",
None, QApplication.UnicodeUTF8).arg(h))
315 if type(rowOrDev)==type(0):
316 path=qApp.diskData[rowOrDev][self.header.index(
"1device-mount-paths")]
319 cmd =
u"df '%s'" %path
320 dfOutput=subprocess.Popen(cmd, shell=
True, stdout=subprocess.PIPE).communicate()[0].split(
"\n")[-2]
321 m = re.match(
"(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*", dfOutput).groups()
333 for d
in qApp.diskData.disks.keys():
339 if d.owner==
None or len(d.owner)==0:
353 student=
u"%s" %self.tm.data(idx,Qt.DisplayRole).toString()
354 ownedUsbDisk.editRecord(self.
diskFromOwner(student), hint=student)
375 self.ui.namesButton.setIcon(icon)
376 self.ui.namesButton.setToolTip(msg)
377 self.ui.namesButton.setStatusTip(msg.remove(
"<br />"))
390 global activeThreads, lastCommand
391 active = len(qApp.diskData)>0
392 for button
in (self.ui.toButton,
395 self.ui.umountButton):
396 button.setEnabled(active)
401 if len(activeThreads) > 0:
402 self.ui.redoButton.setIcon(self.
iconStop)
405 self.ui.redoButton.setEnabled(
True)
408 self.ui.redoButton.setIcon(self.
iconRedo)
411 self.ui.redoButton.setEnabled(lastCommand!=
None)
412 l=self.namesDialog.ui.listWidget.findItems(
"*",Qt.MatchWildcard)
424 pref.setValues(db.readPrefs())
427 if pref.result()==QDialog.Accepted:
428 db.writePrefs(pref.values())
437 titre1=QApplication.translate(
"Dialog",
"Choix de fichiers à supprimer",
None, QApplication.UnicodeUTF8)
438 titre2=QApplication.translate(
"Dialog",
"Choix de fichiers à supprimer (jokers autorisés)",
None, QApplication.UnicodeUTF8)
442 pathList=map(
lambda x:
u"%s" %x, d.pathList())
443 buttons=QMessageBox.Ok|QMessageBox.Cancel
444 defaultButton=QMessageBox.Cancel
445 reply=QMessageBox.warning(
447 QApplication.translate(
"Dialog",
"Vous allez effacer plusieurs baladeurs",
None, QApplication.UnicodeUTF8),
448 QApplication.translate(
"Dialog",
"Etes-vous certain de vouloir effacer : "+
"\n".join(pathList),
None, QApplication.UnicodeUTF8),
449 buttons, defaultButton)
450 if reply == QMessageBox.Ok:
451 cmd=
't=usbThread.threadDeleteInUSB(p,%s,subdir="Travail", logfile="%s", parent=self.tm)' %(pathList,logFileName)
452 for p
in qApp.diskData:
453 if not p.selected:
continue
455 exec(compile(cmd,
'<string>',
'exec'))
458 self.oldThreads.add(t)
461 msgBox=QMessageBox.warning(
463 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None, QApplication.UnicodeUTF8),
464 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None, QApplication.UnicodeUTF8))
475 cmd=
't=usbThread.threadCopyToUSB(p,%s,subdir="%s", logfile="%s", parent=self.tm)' %(d.selectedList(), self.
workdir, logFileName)
476 for p
in qApp.diskData:
477 if not p.selected:
continue
479 exec(compile(cmd,
'<string>',
'exec'))
482 self.oldThreads.add(t)
485 msgBox=QMessageBox.warning(
487 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None, QApplication.UnicodeUTF8),
488 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None, QApplication.UnicodeUTF8))
496 titre1=QApplication.translate(
"Dialog",
"Choix de fichiers à copier",
None, QApplication.UnicodeUTF8)
497 titre2=QApplication.translate(
"Dialog",
"Choix de fichiers à copier depuis les baladeurs",
None, QApplication.UnicodeUTF8)
498 ok=QApplication.translate(
"Dialog",
"Choix de la destination ...",
None, QApplication.UnicodeUTF8)
501 if not ok
or len(d.pathList())==0 :
502 msgBox=QMessageBox.warning(
None,
503 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None, QApplication.UnicodeUTF8),
504 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None, QApplication.UnicodeUTF8))
507 pathList=map(
lambda x:
u"%s" %x, d.pathList())
508 mp=d.selectedDiskMountPoint()
509 initialPath=os.path.expanduser(
"~")
510 destDir = QFileDialog.getExistingDirectory(
512 QApplication.translate(
"Dialog",
"Choisir un répertoire de destination",
None, QApplication.UnicodeUTF8),
514 if destDir
and len(destDir)>0 :
516 cmd=
u"""t=usbThread.threadMoveFromUSB(
517 p,%s,subdir=self.workdir,
518 rootPath="%s", dest="%s", logfile="%s",
519 parent=self.tm)""" %(pathList, mp, destDir, logFileName)
521 cmd=
u"""t=usbThread.threadCopyFromUSB(
522 p,%s,subdir=self.workdir,
523 rootPath="%s", dest="%s", logfile="%s",
524 parent=self.tm)""" %(pathList, mp, destDir, logFileName)
526 for p
in qApp.diskData:
527 if not p.selected:
continue
534 exec(compile(cmd,
'<string>',
'exec'))
537 self.oldThreads.add(t)
539 buttons=QMessageBox.Ok|QMessageBox.Cancel
540 defaultButton=QMessageBox.Cancel
541 if QMessageBox.question(
543 QApplication.translate(
"Dialog",
"Voir les copies",
None, QApplication.UnicodeUTF8),
544 QApplication.translate(
"Dialog",
"Voulez-vous voir les fichiers copiés ?",
None, QApplication.UnicodeUTF8),
545 buttons, defaultButton)==QMessageBox.Ok:
546 subprocess.call(
"nautilus '%s'" %destDir,shell=
True)
549 msgBox=QMessageBox.warning(
551 QApplication.translate(
"Dialog",
"Destination manquante",
None, QApplication.UnicodeUTF8),
552 QApplication.translate(
"Dialog",
"Veuillez choisir une destination pour la copie des fichiers",
None, QApplication.UnicodeUTF8))
561 global lastCommand, pastCommands, activeThreads
562 if len(activeThreads)>0:
566 thread._Thread__stop()
567 print str(thread.getName()) +
' is terminated'
569 print str(thread.getName()) +
' could not be terminated'
571 if lastCommand==
None:
573 if QMessageBox.question(
575 QApplication.translate(
"Dialog",
"Réitérer la dernière commande",
None, QApplication.UnicodeUTF8),
576 QApplication.translate(
"Dialog",
"La dernière commande était<br>%1<br>Voulez-vous la relancer avec les nouveaux baladeurs ?",
None, QApplication.UnicodeUTF8).arg(lastCommand))==QMessageBox.Cancel:
578 for p
in qApp.diskData:
579 if p.owner
in pastCommands[lastCommand] :
continue
580 exec(compile(lastCommand,
'<string>',
'exec'))
583 self.oldThreads.add(t)
584 pastCommands[lastCommand].append(p.owner)
592 self.namesDialog.show()
608 buttons=QMessageBox.Ok|QMessageBox.Cancel
609 defaultButton=QMessageBox.Cancel
610 button=QMessageBox.question (
612 QApplication.translate(
"Main",
"Démontage des baladeurs",
None, QApplication.UnicodeUTF8),
613 QApplication.translate(
"Main",
"Êtes-vous sûr de vouloir démonter tous les baladeurs cochés de la liste ?",
None, QApplication.UnicodeUTF8),
614 buttons,defaultButton)
615 if button!=QMessageBox.Ok:
618 for p
in qApp.diskData:
620 for d
in qApp.diskData.disks.keys():
621 if p
in qApp.diskData.disks[d]
and p.selected:
623 for partition
in qApp.diskData.disks[d]:
624 devfile=partition.getProp(
"device-file-by-id")
625 if isinstance(devfile, dbus.Array):
627 if partition.isMounted():
628 subprocess.call(
"udisks --unmount %s" %devfile, shell=
True)
630 devfile_disk=d.getProp(
"device-file-by-id")
631 if isinstance(devfile_disk, dbus.Array):
632 devfile_disk=devfile_disk[0]
633 subprocess.call(
"udisks --detach %s" %devfile_disk, shell=
True)
647 if h
in ownedUsbDisk.uDisk._itemNames:
648 self.visibleheader.append(self.tr(ownedUsbDisk.uDisk._itemNames[h]))
650 self.visibleheader.append(h)
652 self.t.setModel(self.
tm)
660 self.proxy.setSourceModel(self.t.model())
683 diskDict=self.listener.connectedVolumes,
685 if force
or not self.
sameDiskData(qApp.diskData, other):
687 connectedCount=int(other)
690 self.t.resizeColumnsToContents()
691 self.ui.lcdNumber.display(connectedCount)
698 self.t.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder);
699 self.t.setSortingEnabled(
True)
700 self.t.resizeColumnsToContents()
709 return set([p.uniqueId()
for p
in one]) == set([p.uniqueId()
for p
in two])
716 self.ui.lcdNumber.setBackgroundRole(QPalette.Highlight)
717 self.flashTimer.start(250)
724 self.ui.lcdNumber.setBackgroundRole(QPalette.Window)
739 def __init__(self, parent=None, header=[], donnees=None, checkable=False):
740 QAbstractTableModel.__init__(self,parent)
745 self.connect(self, SIGNAL(
"pushCmd(QString, QString)"), self.
pushCmd)
746 self.connect(self, SIGNAL(
"popCmd(QString, QString)"), self.
popCmd)
755 global activeThreads, pastCommands, lastCommand
757 owner=owner.encode(
"utf-8")
758 if activeThreads.has_key(owner):
759 activeThreads[owner].append(cmd)
761 activeThreads[owner]=[cmd]
763 self.pere.updateButtons()
772 global activeThreads, pastCommands, lastCommand
774 owner=owner.encode(
"utf-8")
775 if activeThreads.has_key(owner):
776 cmd0=activeThreads[owner].pop()
778 msg=cmd.replace(cmd0,
"")+
"\n"
779 logFile=open(os.path.expanduser(logFileName),
"a")
783 raise Exception, (
u"mismatched commands\n%s\n%s" %(cmd,cmd0)).encode(
"utf-8")
784 if len(activeThreads[owner])==0:
785 activeThreads.pop(owner)
787 raise Exception,
"End of command without a begin."
790 if len(activeThreads)==0 :
791 self.pere.updateButtons()
802 self.emit(SIGNAL(
"dataChanged(QModelIndex, QModelIndex)"), self.index(0,column), self.index(len(self.
donnees)-1, column))
803 self.pere.t.viewport().update()
821 self.
donnees[index.row()].selected=value
824 return QAbstractTableModel.setData(self, index, role)
832 return self.
donnees[index.row()][-1]
835 if not index.isValid():
837 elif role==Qt.ToolTipRole:
839 h=self.pere.header[c]
841 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)
843 return QApplication.translate(
"Main",
"Propriétaire de la clé USB ou du baladeur ;<br><b>Double-clic</b> pour modifier.",
None, QApplication.UnicodeUTF8)
844 elif "device-mount-paths" in h:
845 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)
846 elif "device-size" in h:
847 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)
848 elif "drive-vendor" in h:
849 return QApplication.translate(
"Main",
"Fabricant de la clé USB ou du baladeur.",
None, QApplication.UnicodeUTF8)
850 elif "drive-model" in h:
851 return QApplication.translate(
"Main",
"Modèle de la clé USB ou du baladeur.",
None, QApplication.UnicodeUTF8)
852 elif "drive-serial" in h:
853 return QApplication.translate(
"Main",
"Numéro de série de la clé USB ou du baladeur.",
None, QApplication.UnicodeUTF8)
856 elif role != Qt.DisplayRole:
858 if index.row()<len(self.
donnees):
859 return QVariant(self.
donnees[index.row()][index.column()])
864 if orientation == Qt.Horizontal
and role == Qt.DisplayRole:
865 return QVariant(self.
header[section])
866 elif orientation == Qt.Vertical
and role == Qt.DisplayRole:
867 return QVariant(section+1)
875 def sort(self, Ncol, order=Qt.DescendingOrder):
876 self.emit(SIGNAL(
"layoutAboutToBeChanged()"))
877 self.
donnees = sorted(self.
donnees, key=operator.itemgetter(Ncol))
878 if order == Qt.DescendingOrder:
879 self.donnees.reverse()
880 self.emit(SIGNAL(
"layoutChanged()"))
889 check_box_style_option=QStyleOptionButton()
890 check_box_rect = QApplication.style().subElementRect(QStyle.SE_CheckBoxIndicator,check_box_style_option)
891 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)
892 return QRect(check_box_point, check_box_rect.size())
896 QStyledItemDelegate.__init__(self,parent)
898 def paint(self, painter, option, index):
899 checked = index.model().data(index, Qt.DisplayRole).toBool()
900 check_box_style_option=QStyleOptionButton()
901 check_box_style_option.state |= QStyle.State_Enabled
903 check_box_style_option.state |= QStyle.State_On
905 check_box_style_option.state |= QStyle.State_Off
907 QApplication.style().drawControl(QStyle.CE_CheckBox, check_box_style_option, painter)
910 if ((event.type() == QEvent.MouseButtonRelease)
or (event.type() == QEvent.MouseButtonDblClick)):
911 if (event.button() != Qt.LeftButton
or not CheckBoxRect(option).contains(event.pos())):
913 if (event.type() == QEvent.MouseButtonDblClick):
915 elif (event.type() == QEvent.KeyPress):
916 if event.key() != Qt.Key_Space
and event.key() != Qt.Key_Select:
920 checked = index.model().data(index, Qt.DisplayRole).toBool()
921 result = model.setData(index,
not checked, Qt.EditRole)
933 QStyledItemDelegate.__init__(self,parent)
934 self.
okPixmap=QPixmap(
"/usr/share/icons/Tango/16x16/status/weather-clear.png")
935 self.
busyPixmap=QPixmap(
"/usr/share/icons/Tango/16x16/actions/view-refresh.png")
937 def paint(self, painter, option, index):
939 text = index.model().data(index, Qt.DisplayRole).toString()
940 rect0=QRect(option.rect)
941 rect1=QRect(option.rect)
944 rect0.setSize(QSize(h,h))
946 rect1.setSize(QSize(w-h,h))
947 QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette,
True, text)
948 QApplication.style().drawItemText (painter, rect0, Qt.AlignCenter, option.palette,
True, QString(
"O"))
949 text=(
u"%s" %text).encode(
"utf-8")
950 if activeThreads.has_key(text):
951 QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.
busyPixmap)
953 QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.
okPixmap)
963 QStyledItemDelegate.__init__(self,parent)
966 def paint(self, painter, option, index):
967 value = int(index.model().data(index, Qt.DisplayRole).toString())
969 rect0=QRect(option.rect)
970 rect1=QRect(option.rect)
971 rect0.translate(2,(rect0.height()-16)/2)
972 rect0.setSize(QSize(16,16))
973 rect1.translate(20,0)
974 rect1.setWidth(rect1.width()-20)
975 QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette,
True, text)
977 mount=index.model().partition(index).mountPoint()
978 dev,total,used,remain,pcent,path = self.parent().diskSizeData(mount)
979 pcent=int(pcent[:-1])
980 painter.setBrush(QBrush(QColor(
"slateblue")))
981 painter.drawPie(rect0,0,16*360*pcent/100)
988 suffixes=[
"B",
"KB",
"MB",
"GB",
"TB"]
991 while val > 1024
and i < len(suffixes):
994 return "%4.1f %s" %(val, suffixes[i])