1 /*=========================================================================
2 
3   Library:   CTK
4 
5   Copyright (c) Brigham and Women's Hospital (BWH).
6 
7   Licensed under the Apache License, Version 2.0 (the "License");
8   you may not use this file except in compliance with the License.
9   You may obtain a copy of the License at
10 
11       http://www.apache.org/licenses/LICENSE-2.0.txt
12 
13   Unless required by applicable law or agreed to in writing, software
14   distributed under the License is distributed on an "AS IS" BASIS,
15   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   See the License for the specific language governing permissions and
17   limitations under the License.
18 
19 =========================================================================*/
20 
21 // ctkDICOMWidgets includes
22 #include "ctkDICOMObjectListWidget.h"
23 #include "ui_ctkDICOMObjectListWidget.h"
24 
25 // Qt includes
26 #include <QApplication>
27 #include <QClipboard>
28 #include <QDesktopServices>
29 #include <QSortFilterProxyModel>
30 #include <QString>
31 #include <QStringList>
32 #include <QUrl>
33 
34 //CTK includes
35 #include <ctkDICOMObjectModel.h>
36 #include <ctkLogger.h>
37 static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMObjectListWidget");
38 
39 class qRecursiveTreeProxyFilter : public QSortFilterProxyModel
40 {
41 public:
qRecursiveTreeProxyFilter(QObject * parent=NULL)42   qRecursiveTreeProxyFilter(QObject *parent = NULL):
43     QSortFilterProxyModel(parent)
44   {
45   }
46 
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const47   bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
48   {
49     if (filterRegExp().isEmpty())
50       {
51       return true;
52       }
53     QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
54     return filterAcceptsIndex(index);
55   }
56 
57 private:
filterAcceptsIndex(const QModelIndex index) const58   bool filterAcceptsIndex(const QModelIndex index) const
59   {
60     // Accept item if its tag, attribute, or value text matches
61     if ((sourceModel()->data(sourceModel()->index(index.row(), ctkDICOMObjectModel::TagColumn,
62       index.parent()), Qt::DisplayRole).toString().contains(filterRegExp()))
63       || (sourceModel()->data(sourceModel()->index(index.row(), ctkDICOMObjectModel::AttributeColumn,
64       index.parent()), Qt::DisplayRole).toString().contains(filterRegExp()))
65       || (sourceModel()->data(sourceModel()->index(index.row(), ctkDICOMObjectModel::ValueColumn,
66       index.parent()), Qt::DisplayRole).toString().contains(filterRegExp())))
67       {
68       return true;
69       }
70     // Accept item if any child matches
71     for (int row = 0; row < sourceModel()->rowCount(index); row++)
72       {
73       QModelIndex childIndex = sourceModel()->index(row, 0, index);
74       if (!childIndex.isValid())
75         {
76         break;
77         }
78       if (filterAcceptsIndex(childIndex))
79         {
80         return true;
81         }
82       }
83     return false;
84   }
85 };
86 
87 //----------------------------------------------------------------------------
88 class ctkDICOMObjectListWidgetPrivate: public Ui_ctkDICOMObjectListWidget
89 {
90 public:
91   ctkDICOMObjectListWidgetPrivate();
92   ~ctkDICOMObjectListWidgetPrivate();
93   void populateDICOMObjectTreeView(const QString& fileName);
94   void setPathLabel(const QString& currentFile);
95   QString dicomObjectModelAsString(QAbstractItemModel* dicomObjectModel, QModelIndex parent = QModelIndex(), int indent = 0, QString rowPrefix = QString());
96   void setFilterExpressionInModel(qRecursiveTreeProxyFilter* filterModel, const QString& expr);
97 
98   QString endOfLine;
99   QString currentFile;
100   QStringList fileList;
101   ctkDICOMObjectModel* dicomObjectModel;
102   qRecursiveTreeProxyFilter* filterModel;
103   QString filterExpression;
104 };
105 
106 //----------------------------------------------------------------------------
107 // ctkDICOMObjectListWidgetPrivate methods
108 
109 //----------------------------------------------------------------------------
ctkDICOMObjectListWidgetPrivate()110 ctkDICOMObjectListWidgetPrivate::ctkDICOMObjectListWidgetPrivate()
111 {
112 #ifdef WIN32
113   this->endOfLine = "\r\n";
114 #else
115   this->endOfLine = "\n";
116 #endif
117   this->dicomObjectModel = 0;
118   this->filterModel = 0;
119 }
120 
121 //----------------------------------------------------------------------------
~ctkDICOMObjectListWidgetPrivate()122 ctkDICOMObjectListWidgetPrivate::~ctkDICOMObjectListWidgetPrivate()
123 {
124 }
125 
126 //----------------------------------------------------------------------------
setFilterExpressionInModel(qRecursiveTreeProxyFilter * filterModel,const QString & expr)127 void ctkDICOMObjectListWidgetPrivate::setFilterExpressionInModel(qRecursiveTreeProxyFilter* filterModel, const QString& expr)
128 {
129   const QString regexpPrefix("regexp:");
130   if (expr.startsWith(regexpPrefix))
131     {
132     filterModel->setFilterCaseSensitivity(Qt::CaseSensitive);
133     filterModel->setFilterRegExp(expr.right(expr.length() - regexpPrefix.length()));
134     }
135   else
136     {
137     filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
138     filterModel->setFilterWildcard(expr);
139     }
140 }
141 
142 //----------------------------------------------------------------------------
populateDICOMObjectTreeView(const QString & fileName)143 void ctkDICOMObjectListWidgetPrivate::populateDICOMObjectTreeView(const QString& fileName)
144 {
145   this->dicomObjectModel->setFile(fileName);
146   this->filterModel->invalidate();
147   this->dcmObjectTreeView->setModel(this->filterModel);
148   this->dcmObjectTreeView->expandAll();
149 }
150 
151 // --------------------------------------------------------------------------
setPathLabel(const QString & currentFile)152 void ctkDICOMObjectListWidgetPrivate::setPathLabel(const QString& currentFile)
153 {
154   currentPathLabel->setText(currentFile);
155 }
156 
157 // --------------------------------------------------------------------------
dicomObjectModelAsString(QAbstractItemModel * aDicomObjectModel,QModelIndex parent,int indent,QString rowPrefix)158 QString ctkDICOMObjectListWidgetPrivate::dicomObjectModelAsString(QAbstractItemModel* aDicomObjectModel, QModelIndex parent /*=QModelIndex()*/, int indent /*=0*/, QString rowPrefix /*=QString()*/)
159 {
160   QString dump;
161   QString indentString(indent, '\t'); // add tab characters, (indent) number of times
162   for (int r = 0; r < aDicomObjectModel->rowCount(parent); ++r)
163     {
164     for (int c = 0; c < aDicomObjectModel->columnCount(); ++c)
165       {
166       QModelIndex index = aDicomObjectModel->index(r, c, parent);
167       QString name = aDicomObjectModel->data(index).toString();
168       if (c == 0)
169         {
170         // Replace round brackets by square brackets.
171         // If the text is copied into Excel, Excel would recognize tag (0008,0012)
172         // as a negative number (-80,012). Instead, [0008,0012] is displayed fine.
173         name.replace('(', '[');
174         name.replace(')', ']');
175         dump += rowPrefix + indentString + name;
176         }
177       else
178         {
179         dump += "\t" + name;
180         }
181       }
182     dump += endOfLine;
183     // Print children
184     QModelIndex index0 = aDicomObjectModel->index(r, 0, parent);
185     if (aDicomObjectModel->hasChildren(index0))
186       {
187       dump += dicomObjectModelAsString(aDicomObjectModel, index0, indent + 1, rowPrefix);
188       }
189     }
190   return dump;
191 }
192 
193 //----------------------------------------------------------------------------
194 // ctkDICOMObjectListWidget methods
195 
196 //----------------------------------------------------------------------------
ctkDICOMObjectListWidget(QWidget * _parent)197 ctkDICOMObjectListWidget::ctkDICOMObjectListWidget(QWidget* _parent):Superclass(_parent),
198   d_ptr(new ctkDICOMObjectListWidgetPrivate)
199 {
200   Q_D(ctkDICOMObjectListWidget);
201 
202   d->setupUi(this);
203 
204   d->metadataSearchBox->setAlwaysShowClearIcon(true);
205   d->metadataSearchBox->setShowSearchIcon(true);
206 
207   d->dicomObjectModel = new ctkDICOMObjectModel(this);
208   d->filterModel = new qRecursiveTreeProxyFilter(this);
209   d->filterModel->setSourceModel(d->dicomObjectModel);
210 
211   d->fileSliderWidget->setMaximum(1);
212   d->fileSliderWidget->setMinimum(1);
213   d->fileSliderWidget->setPageStep(1);
214 
215   d->currentPathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
216   connect(d->fileSliderWidget, SIGNAL(valueChanged(double)), this, SLOT(updateWidget()));
217   connect(d->dcmObjectTreeView, SIGNAL(doubleClicked(const QModelIndex&)),
218     this, SLOT(itemDoubleClicked(const QModelIndex&)));
219   connect(d->copyPathPushButton , SIGNAL(clicked(bool)),this, SLOT(copyPath()));
220 
221   connect(d->expandAllPushButton, SIGNAL(clicked(bool)), d->dcmObjectTreeView, SLOT(expandAll()));
222   connect(d->collapseAllPushButton, SIGNAL(clicked(bool)), d->dcmObjectTreeView, SLOT(collapseAll()));
223   connect(d->copyMetadataPushButton, SIGNAL(clicked(bool)), this, SLOT(copyMetadata()));
224   connect(d->copyAllFilesMetadataPushButton, SIGNAL(clicked(bool)), this, SLOT(copyAllFilesMetadata()));
225 
226   QObject::connect(d->metadataSearchBox, SIGNAL(textChanged(QString)), this, SLOT(setFilterExpression(QString)));
227   QObject::connect(d->metadataSearchBox, SIGNAL(textChanged(QString)), this, SLOT(onFilterChanged()));
228 }
229 
230 //----------------------------------------------------------------------------
~ctkDICOMObjectListWidget()231 ctkDICOMObjectListWidget::~ctkDICOMObjectListWidget()
232 {
233   Q_D(ctkDICOMObjectListWidget);
234   d->dicomObjectModel->deleteLater();
235   d->filterModel->deleteLater();
236 }
237 
238 //----------------------------------------------------------------------------
setCurrentFile(const QString & newFileName)239 void ctkDICOMObjectListWidget::setCurrentFile(const QString& newFileName)
240 {
241   Q_D(ctkDICOMObjectListWidget);
242   d->setPathLabel(newFileName);
243 }
244 
245 // --------------------------------------------------------------------------
setFileList(const QStringList & fileList)246 void ctkDICOMObjectListWidget::setFileList(const QStringList& fileList)
247 {
248   Q_D(ctkDICOMObjectListWidget);
249   d->fileList = fileList;
250   if (d->fileList.size() > 0)
251     {
252     d->currentFile = d->fileList[0];
253 
254     d->populateDICOMObjectTreeView(d->currentFile);
255     d->fileSliderWidget->setMaximum(fileList.size());
256     d->fileSliderWidget->setSuffix(QString(" / %1").arg(fileList.size()));
257     for (int columnIndex = 0; columnIndex < d->dicomObjectModel->columnCount(); ++columnIndex)
258       {
259       d->dcmObjectTreeView->resizeColumnToContents(columnIndex);
260       }
261     }
262   else
263     {
264     d->currentFile.clear();
265     d->dicomObjectModel->clear();
266     }
267 
268   d->setPathLabel(d->currentFile);
269   d->fileSliderWidget->setVisible(d->fileList.size() > 1);
270 }
271 
272 // --------------------------------------------------------------------------
currentFile()273 QString ctkDICOMObjectListWidget::currentFile()
274 {
275   Q_D(ctkDICOMObjectListWidget);
276   return d->currentFile;
277 }
278 
279 // --------------------------------------------------------------------------
fileList()280 QStringList ctkDICOMObjectListWidget::fileList()
281 {
282   Q_D(ctkDICOMObjectListWidget);
283   return d->fileList;
284 }
285 
286 // --------------------------------------------------------------------------
openLookupUrl(QString tag)287 void ctkDICOMObjectListWidget::openLookupUrl(QString tag)
288 {
289   QString lookupUrl = "http://dicomlookup.com/lookup.asp?sw=Tnumber&q=" + tag;
290   QUrl url(lookupUrl);
291   QDesktopServices::openUrl(url);
292 }
293 
294 // --------------------------------------------------------------------------
itemDoubleClicked(const QModelIndex & index)295 void ctkDICOMObjectListWidget::itemDoubleClicked(const QModelIndex& index)
296 {
297   Q_D(ctkDICOMObjectListWidget);
298   QModelIndex tagIndex = d->filterModel->index(index.row(), 0, index.parent());
299   QString tag = d->filterModel->data(tagIndex).toString();
300   openLookupUrl(tag);
301 }
302 
303 // --------------------------------------------------------------------------
updateWidget()304 void ctkDICOMObjectListWidget::updateWidget()
305 {
306   Q_D(ctkDICOMObjectListWidget);
307   d->currentFile = d->fileList[static_cast<int>(d->fileSliderWidget->value())-1];
308   d->setPathLabel(d->currentFile);
309   d->populateDICOMObjectTreeView(d->currentFile);
310  }
311 
312 // --------------------------------------------------------------------------
copyPath()313 void ctkDICOMObjectListWidget::copyPath()
314 {
315   Q_D(ctkDICOMObjectListWidget);
316   QClipboard *clipboard = QApplication::clipboard();
317   clipboard->setText(d->currentFile);
318 }
319 
320 // --------------------------------------------------------------------------
metadataAsText(bool allFiles)321 QString ctkDICOMObjectListWidget::metadataAsText(bool allFiles /*=false*/)
322 {
323   Q_D(ctkDICOMObjectListWidget);
324   QString metadata;
325   if (allFiles)
326     {
327     foreach(QString fileName, d->fileList)
328       {
329       // copy metadata of all files
330 
331       ctkDICOMObjectModel* aDicomObjectModel = new ctkDICOMObjectModel();
332       aDicomObjectModel->setFile(fileName);
333 
334       qRecursiveTreeProxyFilter* afilterModel = new qRecursiveTreeProxyFilter();
335       afilterModel->setSourceModel(aDicomObjectModel);
336       d->setFilterExpressionInModel(afilterModel, d->filterExpression);
337 
338       QString thisFileMetadata = d->dicomObjectModelAsString(afilterModel, QModelIndex(), 0, fileName + "\t");
339 
340       if (!thisFileMetadata.isEmpty())
341         {
342         metadata += thisFileMetadata;
343         }
344       else
345         {
346         metadata += fileName + "\t(none)" + d->endOfLine;
347         }
348 
349       delete afilterModel;
350       delete aDicomObjectModel;
351       }
352     }
353   else
354     {
355     // single file
356     metadata = d->dicomObjectModelAsString(d->filterModel);
357     }
358   return metadata;
359 }
360 
361 // --------------------------------------------------------------------------
copyMetadata()362 void ctkDICOMObjectListWidget::copyMetadata()
363 {
364   QClipboard *clipboard = QApplication::clipboard();
365   clipboard->setText(metadataAsText());
366 }
367 
368 // --------------------------------------------------------------------------
copyAllFilesMetadata()369 void ctkDICOMObjectListWidget::copyAllFilesMetadata()
370 {
371   QApplication::setOverrideCursor(QCursor(Qt::BusyCursor));
372   QClipboard *clipboard = QApplication::clipboard();
373   clipboard->setText(metadataAsText(true));
374   QApplication::restoreOverrideCursor();
375 }
376 
377 //------------------------------------------------------------------------------
onFilterChanged()378 void ctkDICOMObjectListWidget::onFilterChanged()
379 {
380   Q_D(ctkDICOMObjectListWidget);
381 
382   // Change the searchbox background to yellow
383   // if there are no matches
384   bool showWarning = (d->filterModel->rowCount() == 0 &&
385     d->dicomObjectModel->rowCount() != 0);
386   QPalette palette;
387   if (showWarning)
388     {
389     palette.setColor(QPalette::Base, Qt::yellow);
390     }
391   else
392     {
393     palette.setColor(QPalette::Base, Qt::white);
394     }
395   d->metadataSearchBox->setPalette(palette);
396 }
397 
398 //------------------------------------------------------------------------------
setFilterExpression(const QString & expr)399 void ctkDICOMObjectListWidget::setFilterExpression(const QString& expr)
400 {
401   Q_D(ctkDICOMObjectListWidget);
402   d->filterExpression = expr;
403   d->setFilterExpressionInModel(d->filterModel, expr);
404 }
405 
406 //------------------------------------------------------------------------------
filterExpression()407 QString ctkDICOMObjectListWidget::filterExpression()
408 {
409   Q_D(ctkDICOMObjectListWidget);
410   return d->filterExpression;
411 }
412