1 /*
2  * Copyright (C) 1999-2002 Bernd Gehrmann <bernd@mail.berlios.de>
3  * Copyright (c) 2003-2008 André Wöbbeking <Woebbeking@kde.org>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18  */
19 
20 
21 #include "updateview.h"
22 
23 #include <set>
24 
25 #include <qapplication.h>
26 #include <QHeaderView>
27 #include <qfileinfo.h>
28 #include <qstack.h>
29 #include <kconfiggroup.h>
30 #include <KLocalizedString>
31 
32 #include "cervisiasettings.h"
33 #include "updateview_items.h"
34 #include "updateview_visitors.h"
35 
36 
37 using Cervisia::Entry;
38 using Cervisia::EntryStatus;
39 
40 
UpdateView(KConfig & partConfig,QWidget * parent)41 UpdateView::UpdateView(KConfig& partConfig, QWidget *parent)
42     : QTreeWidget(parent),
43       m_partConfig(partConfig),
44       m_unfoldingTree(false)
45 {
46     setAllColumnsShowFocus(true);
47     setUniformRowHeights(true);
48     setRootIsDecorated(false);
49     header()->setSortIndicatorShown(true);
50     setSortingEnabled(true);
51     setSelectionMode(QAbstractItemView::ExtendedSelection);
52 
53     setHeaderLabels(QStringList() << i18n("File Name") << i18n("Status") << i18n("Revision")
54                                   << i18n("Tag/Date") << i18n("Timestamp"));
55 
56     header()->resizeSection(0, 280);
57     header()->resizeSection(1, 90);
58     header()->resizeSection(2, 70);
59     header()->resizeSection(3, 90);
60     header()->resizeSection(4, 120);
61 
62     setFilter(NoFilter);
63 
64     connect( this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)),
65              this, SLOT(itemExecuted(QTreeWidgetItem*,int)) );
66 
67     connect( this, SIGNAL(itemExpanded(QTreeWidgetItem*)),
68              this, SLOT(itemExpandedSlot(QTreeWidgetItem*)) );
69 
70     KConfigGroup cg(&m_partConfig, "UpdateView");
71     QByteArray state = cg.readEntry<QByteArray>("Columns", QByteArray());
72     header()->restoreState(state);
73 }
74 
75 
~UpdateView()76 UpdateView::~UpdateView()
77 {
78     KConfigGroup cg(&m_partConfig, "UpdateView");
79     cg.writeEntry("Columns", header()->saveState());
80 }
81 
82 
setFilter(Filter filter)83 void UpdateView::setFilter(Filter filter)
84 {
85     filt = filter;
86 
87     if (UpdateDirItem* item = static_cast<UpdateDirItem*>(topLevelItem(0)))
88     {
89         ApplyFilterVisitor applyFilterVisitor(filter);
90         item->accept(applyFilterVisitor);
91     }
92 }
93 
94 
filter() const95 UpdateView::Filter UpdateView::filter() const
96 {
97     return filt;
98 }
99 
100 
101 // returns true iff exactly one UpdateFileItem is selected
hasSingleSelection() const102 bool UpdateView::hasSingleSelection() const
103 {
104     const QList<QTreeWidgetItem *>& listSelectedItems(selectedItems());
105 
106     return (listSelectedItems.count() == 1) && isFileItem(listSelectedItems.first());
107 }
108 
109 
getSingleSelection(QString * filename,QString * revision) const110 void UpdateView::getSingleSelection(QString *filename, QString *revision) const
111 {
112     const QList<QTreeWidgetItem *>& listSelectedItems(selectedItems());
113 
114     QString tmpFileName;
115     QString tmpRevision;
116     if ((listSelectedItems.count() == 1) && isFileItem(listSelectedItems.first()))
117     {
118         UpdateFileItem* fileItem(static_cast<UpdateFileItem*>(listSelectedItems.first()));
119         tmpFileName = fileItem->filePath();
120         tmpRevision = fileItem->entry().m_revision;
121     }
122 
123     *filename = tmpFileName;
124     if (revision)
125         *revision = tmpRevision;
126 }
127 
128 
multipleSelection() const129 QStringList UpdateView::multipleSelection() const
130 {
131     QStringList res;
132 
133     const QList<QTreeWidgetItem *>& listSelectedItems(selectedItems());
134     foreach (QTreeWidgetItem *item, listSelectedItems)
135     {
136         if (!item->isHidden())
137             res.append(static_cast<UpdateItem*>(item)->filePath());
138     }
139 
140     return res;
141 }
142 
143 
fileSelection() const144 QStringList UpdateView::fileSelection() const
145 {
146     QStringList res;
147 
148     const QList<QTreeWidgetItem *>& listSelectedItems(selectedItems());
149     foreach (QTreeWidgetItem *item, listSelectedItems)
150     {
151         if (isFileItem(item) && !item->isHidden())
152             res.append(static_cast<UpdateFileItem*>(item)->filePath());
153     }
154 
155     return res;
156 }
157 
158 
conflictColor() const159 const QColor& UpdateView::conflictColor() const
160 {
161     return m_conflictColor;
162 }
163 
164 
localChangeColor() const165 const QColor& UpdateView::localChangeColor() const
166 {
167     return m_localChangeColor;
168 }
169 
170 
remoteChangeColor() const171 const QColor& UpdateView::remoteChangeColor() const
172 {
173     return m_remoteChangeColor;
174 }
175 
176 
notInCvsColor() const177 const QColor& UpdateView::notInCvsColor() const
178 {
179     return m_notInCvsColor;
180 }
181 
182 
isUnfoldingTree() const183 bool UpdateView::isUnfoldingTree() const
184 {
185     return m_unfoldingTree;
186 }
187 
188 
189 // updates internal data
replaceItem(QTreeWidgetItem * oldItem,QTreeWidgetItem * newItem)190 void UpdateView::replaceItem(QTreeWidgetItem *oldItem,
191                              QTreeWidgetItem *newItem)
192 {
193     const int index(relevantSelection.indexOf(oldItem));
194     if (index >= 0)
195         relevantSelection.replace(index, newItem);
196 }
197 
198 
unfoldSelectedFolders()199 void UpdateView::unfoldSelectedFolders()
200 {
201     QApplication::setOverrideCursor(Qt::WaitCursor);
202 
203     int previousDepth = 0;
204     bool isUnfolded = false;
205 
206     QStringList selection = multipleSelection();
207 
208     // setup name of selected folder
209     QString selectedItem = selection.first();
210     if( selectedItem.contains('/') )
211         selectedItem.remove(0, selectedItem.lastIndexOf('/')+1);
212 
213     // avoid flicker
214     const bool _updatesEnabled = updatesEnabled();
215     setUpdatesEnabled(false);
216 
217     QTreeWidgetItemIterator it(this);
218     while ( QTreeWidgetItem *item = (*it) )
219     {
220         if ( isDirItem(item) )
221         {
222             UpdateDirItem* dirItem = static_cast<UpdateDirItem*>(item);
223 
224             // below selected folder?
225             if( previousDepth && dirItem->depth() > previousDepth )
226             {
227                 // if this dir wasn't scanned already scan it recursive
228                 // (this is only a hack to reduce the processEvents() calls,
229                 // setOpen() would scan the dir too)
230                 if (dirItem->wasScanned() == false)
231                 {
232                     const bool recursive = true;
233                     dirItem->maybeScanDir(recursive);
234 
235                     // scanning can take some time so keep the gui alive
236                     qApp->processEvents();
237                 }
238 
239                 dirItem->setOpen(!isUnfolded);
240             }
241             // selected folder?
242             else if( selectedItem == dirItem->entry().m_name )
243             {
244                 previousDepth = dirItem->depth();
245                 isUnfolded = dirItem->isExpanded();
246 
247                 // if this dir wasn't scanned already scan it recursive
248                 // (this is only a hack to reduce the processEvents() calls,
249                 // setOpen() would scan the dir too)
250                 if (dirItem->wasScanned() == false)
251                 {
252                     const bool recursive = true;
253                     dirItem->maybeScanDir(recursive);
254 
255                     // scanning can take some time so keep the gui alive
256                     qApp->processEvents();
257                 }
258 
259                 dirItem->setOpen(!isUnfolded);
260             }
261             // back to the level of the selected folder or above?
262             else if( previousDepth && dirItem->depth() >= previousDepth )
263             {
264                 previousDepth = 0;
265             }
266         }
267 
268         ++it;
269     }
270 
271     // maybe some UpdateDirItem was opened the first time so check the whole tree
272     setFilter(filter());
273 
274     setUpdatesEnabled(_updatesEnabled);
275     viewport()->update();
276 
277     QApplication::restoreOverrideCursor();
278 }
279 
280 
unfoldTree()281 void UpdateView::unfoldTree()
282 {
283     QApplication::setOverrideCursor(Qt::WaitCursor);
284 
285     m_unfoldingTree = true;
286 
287     const bool _updatesEnabled = updatesEnabled();
288 
289     setUpdatesEnabled(false);
290 
291     QTreeWidgetItemIterator it(this);
292     while (QTreeWidgetItem *item = (*it) )
293     {
294         if (isDirItem(item))
295         {
296             UpdateDirItem* dirItem(static_cast<UpdateDirItem*>(item));
297 
298             // if this dir wasn't scanned already scan it recursive
299             // (this is only a hack to reduce the processEvents() calls,
300             // setOpen() would scan the dir too)
301             if (dirItem->wasScanned() == false)
302             {
303                 const bool recursive(true);
304                 dirItem->maybeScanDir(recursive);
305 
306                 // scanning can take some time so keep the gui alive
307                 qApp->processEvents();
308             }
309 
310             dirItem->setOpen(true);
311         }
312 
313         ++it;
314     }
315 
316     // maybe some UpdateDirItem was opened the first time so check the whole tree
317     setFilter(filter());
318 
319     setUpdatesEnabled(_updatesEnabled);
320 
321     viewport()->update();
322 
323     m_unfoldingTree = false;
324 
325     QApplication::restoreOverrideCursor();
326 }
327 
328 
foldTree()329 void UpdateView::foldTree()
330 {
331     QTreeWidgetItemIterator it(this);
332     while (QTreeWidgetItem* item = (*it) )
333     {
334         // don't close the top level directory
335         if (isDirItem(item) && item->parent())
336             item->setExpanded(false);
337 
338         ++it;
339     }
340 }
341 
342 
343 /**
344  * Clear the tree view and insert the directory dirname
345  * into it as the new root item
346  */
openDirectory(const QString & dirName)347 void UpdateView::openDirectory(const QString& dirName)
348 {
349     clear();
350 
351     // do this each time as the configuration could be changed
352     updateColors();
353 
354     Entry entry;
355     entry.m_name = dirName;
356     entry.m_type = Entry::Dir;
357 
358     UpdateDirItem *item = new UpdateDirItem(this, entry);
359     item->setExpanded(true);
360     setCurrentItem(item);
361     item->setSelected(true);
362 }
363 
364 
365 /**
366  * Start a job. We want to be able to change the status field
367  * correctly afterwards, so we have to remember the current
368  * selection (which the user may change during the update).
369  * In the recursive case, we collect all relevant directories.
370  * Furthermore, we have to change the items to undefined state.
371  */
prepareJob(bool recursive,Action action)372 void UpdateView::prepareJob(bool recursive, Action action)
373 {
374     act = action;
375 
376     // Scan recursively all entries - there's no way around this here
377     if (recursive)
378         static_cast<UpdateDirItem*>(topLevelItem(0))->maybeScanDir(true);
379 
380     rememberSelection(recursive);
381     if (act != Add)
382         markUpdated(false, false);
383 }
384 
385 
386 /**
387  * Finishes a job. What we do depends a bit on
388  * whether the command was successful or not.
389  */
finishJob(bool normalExit,int exitStatus)390 void UpdateView::finishJob(bool normalExit, int exitStatus)
391 {
392     // cvs exitStatus == 1 only means that there're conflicts
393     // ... which is not correct (e.g. server not reachable also returns 1)
394     const bool success(normalExit && (exitStatus == 0));
395 
396     if (act != Add)
397         markUpdated(true, success);
398     syncSelection();
399 
400     // maybe some new items were created or
401     // visibility of items changed so check the whole tree
402     setFilter(filter());
403 }
404 
405 
406 /**
407  * Marking non-selected items in a directory updated (as a consequence
408  * of not appearing in 'cvs update' output) is done in two steps: In the
409  * first, they are marked as 'indefinite', so that their status on the screen
410  * isn't misrepresented. In the second step, they are either set
411  * to 'UpToDate' (success=true) or 'Unknown'.
412  */
markUpdated(bool laststage,bool success)413 void UpdateView::markUpdated(bool laststage, bool success)
414 {
415     foreach (QTreeWidgetItem* it, relevantSelection)
416     {
417         if (isDirItem(it))
418         {
419             for (int i = 0; i < it->childCount(); i++)
420             {
421                 QTreeWidgetItem *item = it->child(i);
422                 if (isFileItem(item))
423                 {
424                     UpdateFileItem* fileItem = static_cast<UpdateFileItem*>(item);
425                     fileItem->markUpdated(laststage, success);
426                 }
427             }
428         }
429         else
430         {
431             UpdateFileItem* fileItem = static_cast<UpdateFileItem*>(it);
432             fileItem->markUpdated(laststage, success);
433         }
434     }
435 }
436 
437 
438 /**
439  * Remember the selection, see prepareJob()
440  */
rememberSelection(bool recursive)441 void UpdateView::rememberSelection(bool recursive)
442 {
443     std::set<QTreeWidgetItem*> setItems;
444     for (QTreeWidgetItemIterator it(this); *it; ++it)
445     {
446         QTreeWidgetItem* item(*it);
447 
448         // if this item is selected and if it was not inserted already
449         // and if we work recursive and if it is a dir item then insert
450         // all sub dirs
451         // DON'T CHANGE TESTING ORDER
452         if (item->isSelected()
453             && setItems.insert(item).second
454             && recursive
455             && isDirItem(item))
456         {
457             QStack<QTreeWidgetItem *> s;
458             int childNum = 0;
459             QTreeWidgetItem *startItem = item;
460             QTreeWidgetItem *childItem = startItem->child(childNum);
461             while ( childItem )
462             {
463                 // if this item is a dir item and if it was not
464                 // inserted already then insert all sub dirs
465                 // DON'T CHANGE TESTING ORDER
466                 if (isDirItem(childItem) && setItems.insert(childItem).second)
467                 {
468                     if (QTreeWidgetItem *childChildItem = childItem->child(0))
469                         s.push(childChildItem);
470                 }
471 
472                 if ( ++childNum < startItem->childCount() )
473                     childItem = startItem->child(childNum);
474                 else
475                 {
476                     if ( s.isEmpty() )
477                         break;
478                     else
479                     {
480                         childItem = s.pop();
481                         startItem = childItem->parent();
482                         childNum = 0;
483                     }
484                 }
485             }
486         }
487     }
488 
489     // Copy the set to the list
490     relevantSelection.clear();
491     std::set<QTreeWidgetItem *>::const_iterator const itItemEnd = setItems.end();
492     for (std::set<QTreeWidgetItem *>::const_iterator itItem = setItems.begin();
493          itItem != itItemEnd; ++itItem)
494         relevantSelection.append(*itItem);
495 
496 #if 0
497     qDebug() << "Relevant:";
498     foreach (QTreeWidgetItem * item, relevantSelection)
499         qDebug() << "  " << item->text(0);
500     qDebug() << "End";
501 #endif
502 }
503 
504 
505 /**
506  * Use the remembered selection to resynchronize
507  * with the actual directory and Entries content.
508  */
syncSelection()509 void UpdateView::syncSelection()
510 {
511     // compute all directories which are selected or contain a selected file
512     // (in recursive mode this includes all sub directories)
513     std::set<UpdateDirItem*> setDirItems;
514     foreach (QTreeWidgetItem* item, relevantSelection)
515     {
516         UpdateDirItem *dirItem(0);
517         if (isDirItem(item))
518             dirItem = static_cast<UpdateDirItem*>(item);
519         else if (QTreeWidgetItem *parentItem = item->parent())
520             dirItem = static_cast<UpdateDirItem*>(parentItem);
521 
522         if (dirItem)
523             setDirItems.insert(dirItem);
524     }
525 
526     QApplication::setOverrideCursor(Qt::WaitCursor);
527 
528     std::set<UpdateDirItem*>::const_iterator const itDirItemEnd = setDirItems.end();
529     for (std::set<UpdateDirItem*>::const_iterator itDirItem = setDirItems.begin();
530          itDirItem != itDirItemEnd; ++itDirItem)
531     {
532         UpdateDirItem* dirItem = *itDirItem;
533 
534         dirItem->syncWithDirectory();
535         dirItem->syncWithEntries();
536 
537         qApp->processEvents();
538     }
539 
540     QApplication::restoreOverrideCursor();
541 }
542 
543 
544 /**
545  * Get the colors from the configuration each time the list view items
546  * are created.
547  */
updateColors()548 void UpdateView::updateColors()
549 {
550     KConfigGroup cs(&m_partConfig, "Colors");
551 
552     m_conflictColor = cs.readEntry("Conflict", QColor(255, 130, 130) );
553     m_localChangeColor = cs.readEntry("LocalChange", QColor(130, 130, 255));
554     m_remoteChangeColor = cs.readEntry("RemoteChange", QColor(70, 210, 70) );
555 
556     m_notInCvsColor = CervisiaSettings::notInCvsColor();
557 }
558 
559 
560 /**
561  * Process one line from the output of 'cvs update'. If parseAsStatus
562  * is true, it is assumed that the output is from a command
563  * 'cvs update -n', i.e. cvs actually changes no files.
564  */
processUpdateLine(QString str)565 void UpdateView::processUpdateLine(QString str)
566 {
567     if (str.length() > 2 && str[1] == ' ')
568     {
569         EntryStatus status(Cervisia::Unknown);
570         switch (str[0].toLatin1())
571         {
572         case 'C':
573             status = Cervisia::Conflict;
574             break;
575         case 'A':
576             status = Cervisia::LocallyAdded;
577             break;
578         case 'R':
579             status = Cervisia::LocallyRemoved;
580             break;
581         case 'M':
582             status = Cervisia::LocallyModified;
583             break;
584         case 'U':
585             status = (act == UpdateNoAct) ? Cervisia::NeedsUpdate : Cervisia::Updated;
586             break;
587         case 'P':
588             status = (act == UpdateNoAct) ? Cervisia::NeedsPatch : Cervisia::Patched;
589             break;
590         case '?':
591             status = Cervisia::NotInCVS;
592             break;
593         default:
594             return;
595         }
596         updateItem(str.mid(2), status, false);
597     }
598 
599     const QString removedFileStart(QLatin1String("cvs server: "));
600     const QString removedFileEnd(QLatin1String(" is no longer in the repository"));
601     if (str.startsWith(removedFileStart) && str.endsWith(removedFileEnd))
602     {
603     }
604 
605 #if 0
606     else if (str.left(21) == "cvs server: Updating " ||
607              str.left(21) == "cvs update: Updating ")
608         updateItem(str.right(str.length()-21), Unknown, true);
609 #endif
610 }
611 
612 
updateItem(const QString & filePath,EntryStatus status,bool isdir)613 void UpdateView::updateItem(const QString& filePath, EntryStatus status, bool isdir)
614 {
615     if (isdir && filePath == QLatin1String("."))
616         return;
617 
618     const QFileInfo fileInfo(filePath);
619 
620     UpdateDirItem* rootItem = static_cast<UpdateDirItem*>(topLevelItem(0));
621     UpdateDirItem* dirItem = findOrCreateDirItem(fileInfo.path(), rootItem);
622 
623     dirItem->updateChildItem(fileInfo.fileName(), status, isdir);
624 }
625 
626 
itemExecuted(QTreeWidgetItem * item,int)627 void UpdateView::itemExecuted(QTreeWidgetItem *item, int)
628 {
629     if (isFileItem(item))
630         emit fileOpened(static_cast<UpdateFileItem*>(item)->filePath());
631 }
632 
itemExpandedSlot(QTreeWidgetItem * item)633 void UpdateView::itemExpandedSlot(QTreeWidgetItem *item)
634 {
635     static_cast<UpdateItem *>(item)->setOpen(true);
636 }
637 
638 
639 // Local Variables:
640 // c-basic-offset: 4
641 // End:
642