]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/qt-console/joblist/joblist.cpp
Make table widget items read only again.
[bacula/bacula] / bacula / src / qt-console / joblist / joblist.cpp
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2007-2008 Free Software Foundation Europe e.V.
5
6    The main author of Bacula is Kern Sibbald, with contributions from
7    many others, a complete list can be found in the file AUTHORS.
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version two of the GNU General Public
10    License as published by the Free Software Foundation and included
11    in the file LICENSE.
12
13    This program is distributed in the hope that it will be useful, but
14    WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16    General Public License for more details.
17
18    You should have received a copy of the GNU General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21    02110-1301, USA.
22
23    Bacula® is a registered trademark of Kern Sibbald.
24    The licensor of Bacula is the Free Software Foundation Europe
25    (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26    Switzerland, email:ftf@fsfeurope.org.
27 */
28 /*
29  *   Version $Id$
30  *
31  *   Dirk Bartley, March 2007
32  */
33  
34 #include "bat.h"
35 #include <QAbstractEventDispatcher>
36 #include <QTableWidgetItem>
37 #include "joblist.h"
38 #include "restore.h"
39 #include "joblog/joblog.h"
40 #ifdef HAVE_QWT
41 #include "jobgraphs/jobplot.h"
42 #endif
43 #include "util/fmtwidgetitem.h"
44 #include "util/comboutil.h"
45
46 /*
47  * Constructor for the class
48  */
49 JobList::JobList(const QString &mediaName, const QString &clientName,
50           const QString &jobName, const QString &filesetName, QTreeWidgetItem *parentTreeWidgetItem)
51 {
52    setupUi(this);
53    m_name = ""; /* treeWidgetName has a virtual override in this class */
54    m_mediaName = mediaName;
55    m_clientName = clientName;
56    m_jobName = jobName;
57    m_filesetName = filesetName;
58    m_filesetName = filesetName;
59    pgInitialize("", parentTreeWidgetItem);
60    QTreeWidgetItem* thisitem = mainWin->getFromHash(this);
61    thisitem->setIcon(0,QIcon(QString::fromUtf8(":images/emblem-system.png")));
62
63    m_resultCount = 0;
64    m_populated = false;
65    m_closeable = false;
66    if ((m_mediaName != "") || (m_clientName != "") || (m_jobName != "") || (m_filesetName != ""))
67       m_closeable=true;
68    m_checkCurrentWidget = true;
69    createConnections();
70
71    /* Set Defaults for check and spin for limits */
72    limitCheckBox->setCheckState(mainWin->m_recordLimitCheck ? Qt::Checked : Qt::Unchecked);
73    limitSpinBox->setValue(mainWin->m_recordLimitVal);
74    daysCheckBox->setCheckState(mainWin->m_daysLimitCheck ? Qt::Checked : Qt::Unchecked);
75    daysSpinBox->setValue(mainWin->m_daysLimitVal);
76    dockPage();
77
78    QGridLayout *gridLayout = new QGridLayout(this);
79    gridLayout->setSpacing(6);
80    gridLayout->setMargin(9);
81    gridLayout->setObjectName(QString::fromUtf8("gridLayout"));
82
83    m_splitter = new QSplitter(Qt::Vertical, this);
84    QScrollArea *area = new QScrollArea();
85    area->setObjectName(QString::fromUtf8("area"));
86    area->setWidget(frame);
87    area->setWidgetResizable(true);
88    m_splitter->addWidget(mp_tableWidget);
89    m_splitter->addWidget(area);
90
91    gridLayout->addWidget(m_splitter, 0, 0, 1, 1);
92    readSettings();
93 }
94
95 /*
96  * Write the m_splitter settings in the destructor
97  */
98 JobList::~JobList()
99 {
100    writeSettings();
101 }
102
103 /*
104  * The Meat of the class.
105  * This function will populate the QTableWidget, mp_tablewidget, with
106  * QTableWidgetItems representing the results of a query for what jobs exist on
107  * the media name passed from the constructor stored in m_mediaName.
108  */
109 void JobList::populateTable()
110 {
111    /* Can't do this in constructor because not neccesarily conected in constructor */
112    prepareFilterWidgets();
113    m_populated = true;
114
115    Freeze frz(*mp_tableWidget); /* disable updating*/
116
117    /* Set up query */
118    QString query;
119    fillQueryString(query);
120
121    /* Set up the Header for the table */
122    QStringList headerlist = (QStringList()
123       << tr("Job Id") << tr("Job Name") << tr("Client") << tr("Job Starttime") 
124       << tr("Job Type") << tr("Job Level") << tr("Job Files") 
125       << tr("Job Bytes") << tr("Job Status")  << tr("Purged") << tr("File Set"));
126
127    m_jobIdIndex = headerlist.indexOf(tr("Job Id"));
128    m_purgedIndex = headerlist.indexOf(tr("Purged"));
129    m_typeIndex = headerlist.indexOf(tr("Job Type"));
130    m_statusIndex = headerlist.indexOf(tr("Job Status"));
131    m_startIndex = headerlist.indexOf(tr("Job Starttime"));
132    m_filesIndex = headerlist.indexOf(tr("Job Files"));
133    m_bytesIndex = headerlist.indexOf(tr("Job Bytes"));
134
135    /* Initialize the QTableWidget */
136    m_checkCurrentWidget = false;
137    mp_tableWidget->clear();
138    m_checkCurrentWidget = true;
139    mp_tableWidget->setColumnCount(headerlist.size());
140    mp_tableWidget->setHorizontalHeaderLabels(headerlist);
141    mp_tableWidget->horizontalHeader()->setHighlightSections(false);
142    mp_tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
143    mp_tableWidget->setSortingEnabled(false); /* rows move on insert if sorting enabled */
144
145    if (mainWin->m_sqlDebug) {
146       Pmsg1(000, "Query cmd : %s\n",query.toUtf8().data());
147    }
148
149    QStringList results;
150    if (m_console->sql_cmd(query, results)) {
151       m_resultCount = results.count();
152
153       QStringList fieldlist;
154       mp_tableWidget->setRowCount(results.size());
155
156       int row = 0;
157       /* Iterate through the record returned from the query */
158       QString resultline;
159       foreach (resultline, results) {
160          fieldlist = resultline.split("\t");
161          if (fieldlist.size() < 12)
162             continue; /* some fields missing, ignore row */
163
164          TableItemFormatter jobitem(*mp_tableWidget, row);
165   
166          /* Iterate through fields in the record */
167          QStringListIterator fld(fieldlist);
168          int col = 0;
169
170          /* job id */
171          jobitem.setNumericFld(col++, fld.next());
172
173          /* job name */
174          jobitem.setTextFld(col++, fld.next());
175
176          /* client */
177          jobitem.setTextFld(col++, fld.next());
178
179          /* job starttime */
180          jobitem.setTextFld(col++, fld.next(), true);
181
182          /* job type */
183          jobitem.setJobTypeFld(col++, fld.next());
184
185          /* job level */
186          jobitem.setJobLevelFld(col++, fld.next());
187
188          /* job files */
189          jobitem.setNumericFld(col++, fld.next());
190
191          /* job bytes */
192          jobitem.setBytesFld(col++, fld.next());
193
194          /* job status */
195          jobitem.setJobStatusFld(col++, fld.next());
196
197          /* purged */
198          jobitem.setBoolFld(col++, fld.next());
199
200          /* fileset */
201          jobitem.setTextFld(col++, fld.next());
202
203          row++;
204       }
205    } 
206    /* set default sorting */
207    mp_tableWidget->sortByColumn(m_jobIdIndex, Qt::DescendingOrder);
208    mp_tableWidget->setSortingEnabled(true);
209    
210    /* Resize the columns */
211    mp_tableWidget->resizeColumnsToContents();
212    mp_tableWidget->resizeRowsToContents();
213    mp_tableWidget->verticalHeader()->hide();
214    if ((m_mediaName != tr("Any")) && (m_resultCount == 0)){
215       /* for context sensitive searches, let the user know if there were no
216        * results */
217       QMessageBox::warning(this, "Bat",
218           tr("The Jobs query returned no results.\n"
219          "Press OK to continue?"), QMessageBox::Ok );
220    }
221
222    /* make read only */
223    int rcnt = mp_tableWidget->rowCount();
224    int ccnt = mp_tableWidget->columnCount();
225    for(int r=0; r < rcnt; r++) {
226       for(int c=0; c < ccnt; c++) {
227          QTableWidgetItem* item = mp_tableWidget->item(r, c);
228          item->setFlags(Qt::ItemFlags(item->flags() & (~Qt::ItemIsEditable)));
229       }
230    }
231 }
232
233 void JobList::prepareFilterWidgets()
234 {
235    if (!m_populated) {
236       clientComboBox->addItem(tr("Any"));
237       clientComboBox->addItems(m_console->client_list);
238       comboSel(clientComboBox, m_clientName);
239
240       QStringList volumeList;
241       m_console->getVolumeList(volumeList);
242       volumeComboBox->addItem(tr("Any"));
243       volumeComboBox->addItems(volumeList);
244       comboSel(volumeComboBox, m_mediaName);
245
246       jobComboBox->addItem(tr("Any"));
247       jobComboBox->addItems(m_console->job_list);
248       comboSel(jobComboBox, m_jobName);
249
250       levelComboFill(levelComboBox);
251
252       boolComboFill(purgedComboBox);
253
254       fileSetComboBox->addItem(tr("Any"));
255       fileSetComboBox->addItems(m_console->fileset_list);
256       comboSel(fileSetComboBox, m_filesetName);
257
258       jobStatusComboFill(statusComboBox);
259    }
260 }
261
262 void JobList::fillQueryString(QString &query)
263 {
264    query = "";
265    int volumeIndex = volumeComboBox->currentIndex();
266    if (volumeIndex != -1)
267       m_mediaName = volumeComboBox->itemText(volumeIndex);
268    QString distinct = "";
269    if (m_mediaName != tr("Any")) { distinct = "DISTINCT "; }
270    query += "SELECT " + distinct + "Job.Jobid AS Id, Job.Name AS JobName, " 
271             " Client.Name AS Client,"
272             " Job.Starttime AS JobStart, Job.Type AS JobType,"
273             " Job.Level AS BackupLevel, Job.Jobfiles AS FileCount,"
274             " Job.JobBytes AS Bytes, Job.JobStatus AS Status,"
275             " Job.PurgedFiles AS Purged, FileSet.FileSet"
276             " FROM Job"
277             " JOIN Client ON (Client.ClientId=Job.ClientId)"
278             " LEFT OUTER JOIN FileSet ON (FileSet.FileSetId=Job.FileSetId) ";
279    QStringList conditions;
280    if (m_mediaName != tr("Any")) {
281       query += " LEFT OUTER JOIN JobMedia ON (JobMedia.JobId=Job.JobId) "
282                " LEFT OUTER JOIN Media ON (JobMedia.MediaId=Media.MediaId) ";
283       conditions.append("Media.VolumeName='" + m_mediaName + "'");
284    }
285
286    comboCond(conditions, clientComboBox, "Client.Name");
287    comboCond(conditions, jobComboBox, "Job.Name");
288    levelComboCond(conditions, levelComboBox, "Job.Level");
289    jobStatusComboCond(conditions, statusComboBox, "Job.JobStatus");
290    boolComboCond(conditions, purgedComboBox, "Job.PurgedFiles");
291    comboCond(conditions, fileSetComboBox, "FileSet.FileSet");
292
293    /* If Limit check box For limit by days is checked  */
294    if (daysCheckBox->checkState() == Qt::Checked) {
295       QDateTime stamp = QDateTime::currentDateTime().addDays(-daysSpinBox->value());
296       QString since = stamp.toString(Qt::ISODate);
297       conditions.append("Job.Starttime>'" + since + "'");
298    }
299    bool first = true;
300    foreach (QString condition, conditions) {
301       if (first) {
302          query += " WHERE " + condition;
303          first = false;
304       } else {
305          query += " AND " + condition;
306       }
307    }
308    /* Descending */
309    query += " ORDER BY Job.JobId DESC";
310    /* If Limit check box for limit records returned is checked  */
311    if (limitCheckBox->checkState() == Qt::Checked) {
312       QString limit;
313       limit.setNum(limitSpinBox->value());
314       query += " LIMIT " + limit;
315    }
316 }
317
318 /*
319  * When the treeWidgetItem in the page selector tree is singleclicked, Make sure
320  * The tree has been populated.
321  */
322 void JobList::PgSeltreeWidgetClicked()
323 {
324    if (!m_populated) {
325       populateTable();
326    }
327 }
328
329 /*
330  *  Virtual function override of pages function which is called when this page
331  *  is visible on the stack
332  */
333 void JobList::currentStackItem()
334 {
335 /*   if (!m_populated) populate every time user comes back to this object */
336       populateTable();
337 }
338
339 /*
340  * Virtual Function to return the name for the medialist tree widget
341  */
342 void JobList::treeWidgetName(QString &desc)
343 {
344    if (m_mediaName != "" ) {
345      desc = tr("JobList of Volume %1").arg(m_mediaName);
346    } else if (m_clientName != "" ) {
347      desc = tr("JobList of Client %1").arg(m_clientName);
348    } else if (m_jobName != "" ) {
349      desc = tr("JobList of Job %1").arg(m_jobName);
350    } else if (m_filesetName != "" ) {
351      desc = tr("JobList of fileset %1").arg(m_filesetName);
352    } else {
353      desc = tr("JobList");
354    }
355 }
356
357 /*
358  * This functions much line tableItemChanged for trees like the page selector,
359  * but I will do much less here
360  */
361 void JobList::tableItemChanged(QTableWidgetItem *currentItem, QTableWidgetItem * /*previousItem*/)
362 {
363    if (m_checkCurrentWidget) {
364       int row = currentItem->row();
365       QTableWidgetItem* jobitem = mp_tableWidget->item(row, 0);
366       m_currentJob = jobitem->text();
367
368       /* include purged action or not */
369       jobitem = mp_tableWidget->item(row, m_purgedIndex);
370       QString purged = jobitem->text();
371       mp_tableWidget->removeAction(actionPurgeFiles);
372       if (purged == tr("No") ) {
373          mp_tableWidget->addAction(actionPurgeFiles);
374       }
375       /* include restore from time and job action or not */
376       jobitem = mp_tableWidget->item(row, m_typeIndex);
377       QString type = jobitem->text();
378       mp_tableWidget->removeAction(actionRestoreFromJob);
379       mp_tableWidget->removeAction(actionRestoreFromTime);
380       if (type == tr("Backup")) {
381          mp_tableWidget->addAction(actionRestoreFromJob);
382          mp_tableWidget->addAction(actionRestoreFromTime);
383       }
384       /* include cancel action or not */
385       jobitem = mp_tableWidget->item(row, m_statusIndex);
386       QString status = jobitem->text();
387       mp_tableWidget->removeAction(actionCancelJob);
388       if (status == tr("Running") || status == tr("Created, not yet running")) {
389          mp_tableWidget->addAction(actionCancelJob);
390       }
391    }
392 }
393
394 /*
395  * Function to create connections for context sensitive menu for this and
396  * the page selector
397  */
398 void JobList::createConnections()
399 {
400    /* connect to the action specific to this pages class that shows up in the 
401     * page selector tree */
402    connect(actionRefreshJobList, SIGNAL(triggered()), this,
403                 SLOT(populateTable()));
404    connect(refreshButton, SIGNAL(pressed()), this, SLOT(populateTable()));
405 #ifdef HAVE_QWT
406    connect(graphButton, SIGNAL(pressed()), this, SLOT(graphTable()));
407 #else
408    graphButton->setEnabled(false);
409    graphButton->setVisible(false);
410 #endif
411    /* for the tableItemChanged to maintain m_currentJob */
412    connect(mp_tableWidget, SIGNAL(
413            currentItemChanged(QTableWidgetItem *, QTableWidgetItem *)),
414            this, SLOT(tableItemChanged(QTableWidgetItem *, QTableWidgetItem *)));
415
416    /* for the tableItemChanged to maintain a delete selection */
417    connect(mp_tableWidget, SIGNAL( itemSelectionChanged()),
418            this, SLOT(selectedJobsGet()) );
419
420    /* Do what is required for the local context sensitive menu */
421
422
423    /* setContextMenuPolicy is required */
424    mp_tableWidget->setContextMenuPolicy(Qt::ActionsContextMenu);
425
426    /* Add Actions */
427    mp_tableWidget->addAction(actionRefreshJobList);
428    mp_tableWidget->addAction(actionListJobid);
429    mp_tableWidget->addAction(actionListFilesOnJob);
430    mp_tableWidget->addAction(actionListJobMedia);
431    mp_tableWidget->addAction(actionListVolumes);
432    mp_tableWidget->addAction(actionDeleteJob);
433    mp_tableWidget->addAction(actionPurgeFiles);
434    mp_tableWidget->addAction(actionRestoreFromJob);
435    mp_tableWidget->addAction(actionRestoreFromTime);
436    mp_tableWidget->addAction(actionShowLogForJob);
437
438    /* Make Connections */
439    connect(actionListJobid, SIGNAL(triggered()), this,
440                 SLOT(consoleListJobid()));
441    connect(actionListFilesOnJob, SIGNAL(triggered()), this,
442                 SLOT(consoleListFilesOnJob()));
443    connect(actionListJobMedia, SIGNAL(triggered()), this,
444                 SLOT(consoleListJobMedia()));
445    connect(actionListVolumes, SIGNAL(triggered()), this,
446                 SLOT(consoleListVolumes()));
447    connect(actionDeleteJob, SIGNAL(triggered()), this,
448                 SLOT(consoleDeleteJob()));
449    connect(actionPurgeFiles, SIGNAL(triggered()), this,
450                 SLOT(consolePurgeFiles()));
451    connect(actionRestoreFromJob, SIGNAL(triggered()), this,
452                 SLOT(preRestoreFromJob()));
453    connect(actionRestoreFromTime, SIGNAL(triggered()), this,
454                 SLOT(preRestoreFromTime()));
455    connect(actionShowLogForJob, SIGNAL(triggered()), this,
456                 SLOT(showLogForJob()));
457    connect(actionCancelJob, SIGNAL(triggered()), this,
458                 SLOT(consoleCancelJob()));
459    connect(actionListJobTotals, SIGNAL(triggered()), this,
460                 SLOT(consoleListJobTotals()));
461
462    m_contextActions.append(actionRefreshJobList);
463    m_contextActions.append(actionListJobTotals);
464 }
465
466 /*
467  * Functions to respond to local context sensitive menu sending console commands
468  * If I could figure out how to make these one function passing a string, Yaaaaaa
469  */
470 void JobList::consoleListJobid()
471 {
472    QString cmd("list jobid=");
473    cmd += m_currentJob;
474    if (mainWin->m_longList) { cmd.prepend("l"); }
475    consoleCommand(cmd);
476 }
477 void JobList::consoleListFilesOnJob()
478 {
479    QString cmd("list files jobid=");
480    cmd += m_currentJob;
481    if (mainWin->m_longList) { cmd.prepend("l"); }
482    consoleCommand(cmd);
483 }
484 void JobList::consoleListJobMedia()
485 {
486    QString cmd("list jobmedia jobid=");
487    cmd += m_currentJob;
488    if (mainWin->m_longList) { cmd.prepend("l"); }
489    consoleCommand(cmd);
490 }
491 void JobList::consoleListVolumes()
492 {
493    QString cmd("list volumes jobid=");
494    cmd += m_currentJob;
495    if (mainWin->m_longList) { cmd.prepend("l"); }
496    consoleCommand(cmd);
497 }
498 void JobList::consoleListJobTotals()
499 {
500    QString cmd("list jobtotals");
501    if (mainWin->m_longList) { cmd.prepend("l"); }
502    consoleCommand(cmd);
503 }
504 void JobList::consoleDeleteJob()
505 {
506    if (QMessageBox::warning(this, "Bat",
507       tr("Are you sure you want to delete??  !!!.\n"
508 "This delete command is used to delete a Job record and all associated catalog"
509 " records that were created. This command operates only on the Catalog"
510 " database and has no effect on the actual data written to a Volume. This"
511 " command can be dangerous and we strongly recommend that you do not use"
512 " it unless you know what you are doing.  The Job and all its associated"
513 " records (File and JobMedia) will be deleted from the catalog."
514       "Press OK to proceed with delete operation.?"),
515       QMessageBox::Ok | QMessageBox::Cancel)
516       == QMessageBox::Cancel) { return; }
517
518    QString cmd("delete job jobid=");
519    cmd += m_selectedJobs;
520    consoleCommand(cmd);
521 }
522 void JobList::consolePurgeFiles()
523 {
524    if (QMessageBox::warning(this, "Bat",
525       tr("Are you sure you want to purge ??  !!!.\n"
526 "The Purge command will delete associated Catalog database records from Jobs and"
527 " Volumes without considering the retention period. Purge  works only on the"
528 " Catalog database and does not affect data written to Volumes. This command can"
529 " be dangerous because you can delete catalog records associated with current"
530 " backups of files, and we recommend that you do not use it unless you know what"
531 " you are doing.\n"
532       "Press OK to proceed with the purge operation?"),
533       QMessageBox::Ok | QMessageBox::Cancel)
534       == QMessageBox::Cancel) { return; }
535
536    QString cmd("purge files jobid=");
537    cmd += m_currentJob;
538    consoleCommand(cmd);
539 }
540
541 /*
542  * Subroutine to call preRestore to restore from a select job
543  */
544 void JobList::preRestoreFromJob()
545 {
546    new prerestorePage(m_currentJob, R_JOBIDLIST);
547 }
548
549 /*
550  * Subroutine to call preRestore to restore from a select job
551  */
552 void JobList::preRestoreFromTime()
553 {
554    new prerestorePage(m_currentJob, R_JOBDATETIME);
555 }
556
557 /*
558  * Subroutine to call class to show the log in the database from that job
559  */
560 void JobList::showLogForJob()
561 {
562    QTreeWidgetItem* pageSelectorTreeWidgetItem = mainWin->getFromHash(this);
563    new JobLog(m_currentJob, pageSelectorTreeWidgetItem);
564 }
565
566 /*
567  * Cancel a running job
568  */
569 void JobList::consoleCancelJob()
570 {
571    QString cmd("cancel jobid=");
572    cmd += m_currentJob;
573    consoleCommand(cmd);
574 }
575
576 /*
577  * Graph this table
578  */
579 void JobList::graphTable()
580 {
581 #ifdef HAVE_QWT
582    JobPlotPass pass;
583    pass.recordLimitCheck = limitCheckBox->checkState();
584    pass.daysLimitCheck = daysCheckBox->checkState();
585    pass.recordLimitSpin = limitSpinBox->value();
586    pass.daysLimitSpin = daysSpinBox->value();
587    pass.jobCombo = jobComboBox->currentText();
588    pass.clientCombo = clientComboBox->currentText();
589    pass.volumeCombo = volumeComboBox->currentText();
590    pass.fileSetCombo = fileSetComboBox->currentText();
591    pass.purgedCombo = purgedComboBox->currentText();
592    pass.levelCombo = levelComboBox->currentText();
593    pass.statusCombo = statusComboBox->currentText();
594    pass.use = true;
595    QTreeWidgetItem* pageSelectorTreeWidgetItem = mainWin->getFromHash(this);
596    new JobPlot(pageSelectorTreeWidgetItem, pass);
597 #endif
598 }
599
600 /*
601  * Save user settings associated with this page
602  */
603 void JobList::writeSettings()
604 {
605    QSettings settings(m_console->m_dir->name(), "bat");
606    settings.beginGroup(m_groupText);
607    settings.setValue(m_splitText, m_splitter->saveState());
608    settings.endGroup();
609 }
610
611 /*
612  * Read and restore user settings associated with this page
613  */
614 void JobList::readSettings()
615 {
616    m_groupText = "JobListPage";
617    m_splitText = "splitterSizes_1";
618    QSettings settings(m_console->m_dir->name(), "bat");
619    settings.beginGroup(m_groupText);
620    m_splitter->restoreState(settings.value(m_splitText).toByteArray());
621    settings.endGroup();
622 }
623
624 /*
625  * Function to fill m_selectedJobsCount and m_selectedJobs with selected values
626  */
627 void JobList::selectedJobsGet()
628 {
629    QList<int> rowList;
630    QList<QTableWidgetItem *> sitems = mp_tableWidget->selectedItems();
631    foreach (QTableWidgetItem *sitem, sitems) {
632       int row = sitem->row();
633       if (!rowList.contains(row)) {
634          rowList.append(row);
635       }
636    }
637
638    m_selectedJobs = "";
639    bool first = true;
640    foreach(int row, rowList) {
641       QTableWidgetItem * sitem = mp_tableWidget->item(row, m_jobIdIndex);
642       if (!first) m_selectedJobs.append(",");
643       else first = false;
644       m_selectedJobs.append(sitem->text());
645    }
646    m_selectedJobsCount = rowList.count();
647    if (m_selectedJobsCount > 1) {
648      QString text = QString( tr("Delete list of %1 Jobs")).arg(m_selectedJobsCount);
649        actionDeleteJob->setText(text);
650    } else {
651      actionDeleteJob->setText(tr("Delete Single Job"));
652    }
653 }