]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/qt-console/joblist/joblist.cpp
Attempt to fix bat seg faults
[bacula/bacula] / bacula / src / qt-console / joblist / joblist.cpp
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2007-2009 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          if (item) {
229             item->setFlags(Qt::ItemFlags(item->flags() & (~Qt::ItemIsEditable)));
230          }
231       }
232    }
233 }
234
235 void JobList::prepareFilterWidgets()
236 {
237    if (!m_populated) {
238       clientComboBox->addItem(tr("Any"));
239       clientComboBox->addItems(m_console->client_list);
240       comboSel(clientComboBox, m_clientName);
241
242       QStringList volumeList;
243       getVolumeList(volumeList);
244       volumeComboBox->addItem(tr("Any"));
245       volumeComboBox->addItems(volumeList);
246       comboSel(volumeComboBox, m_mediaName);
247
248       jobComboBox->addItem(tr("Any"));
249       jobComboBox->addItems(m_console->job_list);
250       comboSel(jobComboBox, m_jobName);
251
252       levelComboFill(levelComboBox);
253
254       boolComboFill(purgedComboBox);
255
256       fileSetComboBox->addItem(tr("Any"));
257       fileSetComboBox->addItems(m_console->fileset_list);
258       comboSel(fileSetComboBox, m_filesetName);
259
260       jobStatusComboFill(statusComboBox);
261    }
262 }
263
264 void JobList::fillQueryString(QString &query)
265 {
266    query = "";
267    int volumeIndex = volumeComboBox->currentIndex();
268    if (volumeIndex != -1)
269       m_mediaName = volumeComboBox->itemText(volumeIndex);
270    QString distinct = "";
271    if (m_mediaName != tr("Any")) { distinct = "DISTINCT "; }
272    query += "SELECT " + distinct + "Job.Jobid AS Id, Job.Name AS JobName, " 
273             " Client.Name AS Client,"
274             " Job.Starttime AS JobStart, Job.Type AS JobType,"
275             " Job.Level AS BackupLevel, Job.Jobfiles AS FileCount,"
276             " Job.JobBytes AS Bytes, Job.JobStatus AS Status,"
277             " Job.PurgedFiles AS Purged, FileSet.FileSet"
278             " FROM Job"
279             " JOIN Client ON (Client.ClientId=Job.ClientId)"
280             " LEFT OUTER JOIN FileSet ON (FileSet.FileSetId=Job.FileSetId) ";
281    QStringList conditions;
282    if (m_mediaName != tr("Any")) {
283       query += " LEFT OUTER JOIN JobMedia ON (JobMedia.JobId=Job.JobId) "
284                " LEFT OUTER JOIN Media ON (JobMedia.MediaId=Media.MediaId) ";
285       conditions.append("Media.VolumeName='" + m_mediaName + "'");
286    }
287
288    comboCond(conditions, clientComboBox, "Client.Name");
289    comboCond(conditions, jobComboBox, "Job.Name");
290    levelComboCond(conditions, levelComboBox, "Job.Level");
291    jobStatusComboCond(conditions, statusComboBox, "Job.JobStatus");
292    boolComboCond(conditions, purgedComboBox, "Job.PurgedFiles");
293    comboCond(conditions, fileSetComboBox, "FileSet.FileSet");
294
295    /* If Limit check box For limit by days is checked  */
296    if (daysCheckBox->checkState() == Qt::Checked) {
297       QDateTime stamp = QDateTime::currentDateTime().addDays(-daysSpinBox->value());
298       QString since = stamp.toString(Qt::ISODate);
299       conditions.append("Job.Starttime>'" + since + "'");
300    }
301    bool first = true;
302    foreach (QString condition, conditions) {
303       if (first) {
304          query += " WHERE " + condition;
305          first = false;
306       } else {
307          query += " AND " + condition;
308       }
309    }
310    /* Descending */
311    query += " ORDER BY Job.JobId DESC";
312    /* If Limit check box for limit records returned is checked  */
313    if (limitCheckBox->checkState() == Qt::Checked) {
314       QString limit;
315       limit.setNum(limitSpinBox->value());
316       query += " LIMIT " + limit;
317    }
318 }
319
320 /*
321  * When the treeWidgetItem in the page selector tree is singleclicked, Make sure
322  * The tree has been populated.
323  */
324 void JobList::PgSeltreeWidgetClicked()
325 {
326    if (!m_populated) {
327       populateTable();
328    }
329 }
330
331 /*
332  *  Virtual function override of pages function which is called when this page
333  *  is visible on the stack
334  */
335 void JobList::currentStackItem()
336 {
337 /*   if (!m_populated) populate every time user comes back to this object */
338       populateTable();
339 }
340
341 /*
342  * Virtual Function to return the name for the medialist tree widget
343  */
344 void JobList::treeWidgetName(QString &desc)
345 {
346    if (m_mediaName != "" ) {
347      desc = tr("JobList of Volume %1").arg(m_mediaName);
348    } else if (m_clientName != "" ) {
349      desc = tr("JobList of Client %1").arg(m_clientName);
350    } else if (m_jobName != "" ) {
351      desc = tr("JobList of Job %1").arg(m_jobName);
352    } else if (m_filesetName != "" ) {
353      desc = tr("JobList of fileset %1").arg(m_filesetName);
354    } else {
355      desc = tr("JobList");
356    }
357 }
358
359 /*
360  * This functions much line tableItemChanged for trees like the page selector,
361  * but I will do much less here
362  */
363 void JobList::tableItemChanged(QTableWidgetItem *currentItem, QTableWidgetItem * /*previousItem*/)
364 {
365    if (m_checkCurrentWidget) {
366       int row = currentItem->row();
367       QTableWidgetItem* jobitem = mp_tableWidget->item(row, 0);
368       m_currentJob = jobitem->text();
369
370       /* include purged action or not */
371       jobitem = mp_tableWidget->item(row, m_purgedIndex);
372       QString purged = jobitem->text();
373       mp_tableWidget->removeAction(actionPurgeFiles);
374       if (purged == tr("No") ) {
375          mp_tableWidget->addAction(actionPurgeFiles);
376       }
377       /* include restore from time and job action or not */
378       jobitem = mp_tableWidget->item(row, m_typeIndex);
379       QString type = jobitem->text();
380       mp_tableWidget->removeAction(actionRestoreFromJob);
381       mp_tableWidget->removeAction(actionRestoreFromTime);
382       if (type == tr("Backup")) {
383          mp_tableWidget->addAction(actionRestoreFromJob);
384          mp_tableWidget->addAction(actionRestoreFromTime);
385       }
386       /* include cancel action or not */
387       jobitem = mp_tableWidget->item(row, m_statusIndex);
388       QString status = jobitem->text();
389       mp_tableWidget->removeAction(actionCancelJob);
390       if (status == tr("Running") || status == tr("Created, not yet running")) {
391          mp_tableWidget->addAction(actionCancelJob);
392       }
393    }
394 }
395
396 /*
397  * Function to create connections for context sensitive menu for this and
398  * the page selector
399  */
400 void JobList::createConnections()
401 {
402    /* connect to the action specific to this pages class that shows up in the 
403     * page selector tree */
404    connect(actionRefreshJobList, SIGNAL(triggered()), this,
405                 SLOT(populateTable()));
406    connect(refreshButton, SIGNAL(pressed()), this, SLOT(populateTable()));
407 #ifdef HAVE_QWT
408    connect(graphButton, SIGNAL(pressed()), this, SLOT(graphTable()));
409 #else
410    graphButton->setEnabled(false);
411    graphButton->setVisible(false);
412 #endif
413    /* for the tableItemChanged to maintain m_currentJob */
414    connect(mp_tableWidget, SIGNAL(
415            currentItemChanged(QTableWidgetItem *, QTableWidgetItem *)),
416            this, SLOT(tableItemChanged(QTableWidgetItem *, QTableWidgetItem *)));
417
418    /* for the tableItemChanged to maintain a delete selection */
419    connect(mp_tableWidget, SIGNAL( itemSelectionChanged()),
420            this, SLOT(selectedJobsGet()) );
421
422    /* Do what is required for the local context sensitive menu */
423
424
425    /* setContextMenuPolicy is required */
426    mp_tableWidget->setContextMenuPolicy(Qt::ActionsContextMenu);
427
428    /* Add Actions */
429    mp_tableWidget->addAction(actionRefreshJobList);
430    mp_tableWidget->addAction(actionListJobid);
431    mp_tableWidget->addAction(actionListFilesOnJob);
432    mp_tableWidget->addAction(actionListJobMedia);
433    mp_tableWidget->addAction(actionListVolumes);
434    mp_tableWidget->addAction(actionDeleteJob);
435    mp_tableWidget->addAction(actionPurgeFiles);
436    mp_tableWidget->addAction(actionRestoreFromJob);
437    mp_tableWidget->addAction(actionRestoreFromTime);
438    mp_tableWidget->addAction(actionShowLogForJob);
439
440    /* Make Connections */
441    connect(actionListJobid, SIGNAL(triggered()), this,
442                 SLOT(consoleListJobid()));
443    connect(actionListFilesOnJob, SIGNAL(triggered()), this,
444                 SLOT(consoleListFilesOnJob()));
445    connect(actionListJobMedia, SIGNAL(triggered()), this,
446                 SLOT(consoleListJobMedia()));
447    connect(actionListVolumes, SIGNAL(triggered()), this,
448                 SLOT(consoleListVolumes()));
449    connect(actionDeleteJob, SIGNAL(triggered()), this,
450                 SLOT(consoleDeleteJob()));
451    connect(actionPurgeFiles, SIGNAL(triggered()), this,
452                 SLOT(consolePurgeFiles()));
453    connect(actionRestoreFromJob, SIGNAL(triggered()), this,
454                 SLOT(preRestoreFromJob()));
455    connect(actionRestoreFromTime, SIGNAL(triggered()), this,
456                 SLOT(preRestoreFromTime()));
457    connect(actionShowLogForJob, SIGNAL(triggered()), this,
458                 SLOT(showLogForJob()));
459    connect(actionCancelJob, SIGNAL(triggered()), this,
460                 SLOT(consoleCancelJob()));
461    connect(actionListJobTotals, SIGNAL(triggered()), this,
462                 SLOT(consoleListJobTotals()));
463
464    m_contextActions.append(actionRefreshJobList);
465    m_contextActions.append(actionListJobTotals);
466 }
467
468 /*
469  * Functions to respond to local context sensitive menu sending console commands
470  * If I could figure out how to make these one function passing a string, Yaaaaaa
471  */
472 void JobList::consoleListJobid()
473 {
474    QString cmd("list jobid=");
475    cmd += m_currentJob;
476    if (mainWin->m_longList) { cmd.prepend("l"); }
477    consoleCommand(cmd);
478 }
479 void JobList::consoleListFilesOnJob()
480 {
481    QString cmd("list files jobid=");
482    cmd += m_currentJob;
483    if (mainWin->m_longList) { cmd.prepend("l"); }
484    consoleCommand(cmd);
485 }
486 void JobList::consoleListJobMedia()
487 {
488    QString cmd("list jobmedia jobid=");
489    cmd += m_currentJob;
490    if (mainWin->m_longList) { cmd.prepend("l"); }
491    consoleCommand(cmd);
492 }
493 void JobList::consoleListVolumes()
494 {
495    QString cmd("list volumes jobid=");
496    cmd += m_currentJob;
497    if (mainWin->m_longList) { cmd.prepend("l"); }
498    consoleCommand(cmd);
499 }
500 void JobList::consoleListJobTotals()
501 {
502    QString cmd("list jobtotals");
503    if (mainWin->m_longList) { cmd.prepend("l"); }
504    consoleCommand(cmd);
505 }
506 void JobList::consoleDeleteJob()
507 {
508    if (QMessageBox::warning(this, "Bat",
509       tr("Are you sure you want to delete??  !!!.\n"
510 "This delete command is used to delete a Job record and all associated catalog"
511 " records that were created. This command operates only on the Catalog"
512 " database and has no effect on the actual data written to a Volume. This"
513 " command can be dangerous and we strongly recommend that you do not use"
514 " it unless you know what you are doing.  The Job and all its associated"
515 " records (File and JobMedia) will be deleted from the catalog."
516       "Press OK to proceed with delete operation.?"),
517       QMessageBox::Ok | QMessageBox::Cancel)
518       == QMessageBox::Cancel) { return; }
519
520    QString cmd("delete job jobid=");
521    cmd += m_selectedJobs;
522    consoleCommand(cmd);
523 }
524 void JobList::consolePurgeFiles()
525 {
526    if (QMessageBox::warning(this, "Bat",
527       tr("Are you sure you want to purge ??  !!!.\n"
528 "The Purge command will delete associated Catalog database records from Jobs and"
529 " Volumes without considering the retention period. Purge  works only on the"
530 " Catalog database and does not affect data written to Volumes. This command can"
531 " be dangerous because you can delete catalog records associated with current"
532 " backups of files, and we recommend that you do not use it unless you know what"
533 " you are doing.\n"
534       "Press OK to proceed with the purge operation?"),
535       QMessageBox::Ok | QMessageBox::Cancel)
536       == QMessageBox::Cancel) { return; }
537
538    QString cmd("purge files jobid=");
539    cmd += m_currentJob;
540    consoleCommand(cmd);
541 }
542
543 /*
544  * Subroutine to call preRestore to restore from a select job
545  */
546 void JobList::preRestoreFromJob()
547 {
548    new prerestorePage(m_currentJob, R_JOBIDLIST);
549 }
550
551 /*
552  * Subroutine to call preRestore to restore from a select job
553  */
554 void JobList::preRestoreFromTime()
555 {
556    new prerestorePage(m_currentJob, R_JOBDATETIME);
557 }
558
559 /*
560  * Subroutine to call class to show the log in the database from that job
561  */
562 void JobList::showLogForJob()
563 {
564    QTreeWidgetItem* pageSelectorTreeWidgetItem = mainWin->getFromHash(this);
565    new JobLog(m_currentJob, pageSelectorTreeWidgetItem);
566 }
567
568 /*
569  * Cancel a running job
570  */
571 void JobList::consoleCancelJob()
572 {
573    QString cmd("cancel jobid=");
574    cmd += m_currentJob;
575    consoleCommand(cmd);
576 }
577
578 /*
579  * Graph this table
580  */
581 void JobList::graphTable()
582 {
583 #ifdef HAVE_QWT
584    JobPlotPass pass;
585    pass.recordLimitCheck = limitCheckBox->checkState();
586    pass.daysLimitCheck = daysCheckBox->checkState();
587    pass.recordLimitSpin = limitSpinBox->value();
588    pass.daysLimitSpin = daysSpinBox->value();
589    pass.jobCombo = jobComboBox->currentText();
590    pass.clientCombo = clientComboBox->currentText();
591    pass.volumeCombo = volumeComboBox->currentText();
592    pass.fileSetCombo = fileSetComboBox->currentText();
593    pass.purgedCombo = purgedComboBox->currentText();
594    pass.levelCombo = levelComboBox->currentText();
595    pass.statusCombo = statusComboBox->currentText();
596    pass.use = true;
597    QTreeWidgetItem* pageSelectorTreeWidgetItem = mainWin->getFromHash(this);
598    new JobPlot(pageSelectorTreeWidgetItem, pass);
599 #endif
600 }
601
602 /*
603  * Save user settings associated with this page
604  */
605 void JobList::writeSettings()
606 {
607    QSettings settings(m_console->m_dir->name(), "bat");
608    settings.beginGroup(m_groupText);
609    settings.setValue(m_splitText, m_splitter->saveState());
610    settings.endGroup();
611 }
612
613 /*
614  * Read and restore user settings associated with this page
615  */
616 void JobList::readSettings()
617 {
618    m_groupText = "JobListPage";
619    m_splitText = "splitterSizes_1";
620    QSettings settings(m_console->m_dir->name(), "bat");
621    settings.beginGroup(m_groupText);
622    m_splitter->restoreState(settings.value(m_splitText).toByteArray());
623    settings.endGroup();
624 }
625
626 /*
627  * Function to fill m_selectedJobsCount and m_selectedJobs with selected values
628  */
629 void JobList::selectedJobsGet()
630 {
631    QList<int> rowList;
632    QList<QTableWidgetItem *> sitems = mp_tableWidget->selectedItems();
633    foreach (QTableWidgetItem *sitem, sitems) {
634       int row = sitem->row();
635       if (!rowList.contains(row)) {
636          rowList.append(row);
637       }
638    }
639
640    m_selectedJobs = "";
641    bool first = true;
642    foreach(int row, rowList) {
643       QTableWidgetItem * sitem = mp_tableWidget->item(row, m_jobIdIndex);
644       if (!first) m_selectedJobs.append(",");
645       else first = false;
646       m_selectedJobs.append(sitem->text());
647    }
648    m_selectedJobsCount = rowList.count();
649    if (m_selectedJobsCount > 1) {
650      QString text = QString( tr("Delete list of %1 Jobs")).arg(m_selectedJobsCount);
651        actionDeleteJob->setText(text);
652    } else {
653      actionDeleteJob->setText(tr("Delete Single Job"));
654    }
655 }