kio Library API Documentation

kfiletreeview.cpp

00001 /* This file is part of the KDEproject 00002 Copyright (C) 2000 David Faure <faure@kde.org> 00003 2000 Carsten Pfeiffer <pfeiffer@kde.org> 00004 00005 This library is free software; you can redistribute it and/or 00006 modify it under the terms of the GNU Library General Public 00007 License version 2 as published by the Free Software Foundation. 00008 00009 This library is distributed in the hope that it will be useful, 00010 but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 00012 Library General Public License for more details. 00013 00014 You should have received a copy of the GNU Library General Public License 00015 along with this library; see the file COPYING.LIB. If not, write to 00016 the Free Software Foundation, Inc., 59 Temple Place - Suite 330, 00017 Boston, MA 02111-1307, USA. 00018 */ 00019 00020 #include <qapplication.h> 00021 #include <qheader.h> 00022 #include <qtimer.h> 00023 #include <kdebug.h> 00024 #include <kdirnotify_stub.h> 00025 #include <kglobalsettings.h> 00026 #include <kfileitem.h> 00027 #include <kfileview.h> 00028 #include <kmimetype.h> 00029 #include <kstandarddirs.h> 00030 #include <stdlib.h> 00031 #include <assert.h> 00032 #include <kio/job.h> 00033 #include <kio/global.h> 00034 #include <kurldrag.h> 00035 #include <kiconloader.h> 00036 00037 00038 #include "kfiletreeview.h" 00039 #include "kfiletreebranch.h" 00040 #include "kfiletreeviewitem.h" 00041 00042 KFileTreeView::KFileTreeView( QWidget *parent, const char *name ) 00043 : KListView( parent, name ), 00044 m_wantOpenFolderPixmaps( true ), 00045 m_toolTip( this ) 00046 { 00047 setDragEnabled(true); 00048 setSelectionModeExt( KListView::Single ); 00049 00050 m_animationTimer = new QTimer( this ); 00051 connect( m_animationTimer, SIGNAL( timeout() ), 00052 this, SLOT( slotAnimation() ) ); 00053 00054 m_currentBeforeDropItem = 0; 00055 m_dropItem = 0; 00056 00057 m_autoOpenTimer = new QTimer( this ); 00058 connect( m_autoOpenTimer, SIGNAL( timeout() ), 00059 this, SLOT( slotAutoOpenFolder() ) ); 00060 00061 /* The executed-Slot only opens a path, while the expanded-Slot populates it */ 00062 connect( this, SIGNAL( executed( QListViewItem * ) ), 00063 this, SLOT( slotExecuted( QListViewItem * ) ) ); 00064 connect( this, SIGNAL( expanded ( QListViewItem *) ), 00065 this, SLOT( slotExpanded( QListViewItem *) )); 00066 connect( this, SIGNAL( collapsed( QListViewItem *) ), 00067 this, SLOT( slotCollapsed( QListViewItem* ))); 00068 00069 00070 /* connections from the konqtree widget */ 00071 connect( this, SIGNAL( selectionChanged() ), 00072 this, SLOT( slotSelectionChanged() ) ); 00073 connect( this, SIGNAL( onItem( QListViewItem * )), 00074 this, SLOT( slotOnItem( QListViewItem * ) ) ); 00075 connect( this, SIGNAL(itemRenamed(QListViewItem*, const QString &, int)), 00076 this, SLOT(slotItemRenamed(QListViewItem*, const QString &, int))); 00077 00078 00079 m_bDrag = false; 00080 m_branches.setAutoDelete( true ); 00081 00082 m_openFolderPixmap = DesktopIcon( "folder_open",KIcon::SizeSmall,KIcon::ActiveState ); 00083 } 00084 00085 KFileTreeView::~KFileTreeView() 00086 { 00087 // we must make sure that the KFileTreeViewItems are deleted _before_ the 00088 // branches are deleted. Otherwise, the KFileItems would be destroyed 00089 // and the KFileTreeViewItems had dangling pointers to them. 00090 hide(); 00091 clear(); 00092 m_branches.clear(); // finally delete the branches and KFileItems 00093 } 00094 00095 00096 bool KFileTreeView::isValidItem( QListViewItem *item) 00097 { 00098 if (!item) 00099 return false; 00100 QPtrList<QListViewItem> lst; 00101 QListViewItemIterator it( this ); 00102 while ( it.current() ) 00103 { 00104 if ( it.current() == item ) 00105 return true; 00106 ++it; 00107 } 00108 return false; 00109 } 00110 00111 void KFileTreeView::contentsDragEnterEvent( QDragEnterEvent *ev ) 00112 { 00113 if ( ! acceptDrag( ev ) ) 00114 { 00115 ev->ignore(); 00116 return; 00117 } 00118 ev->acceptAction(); 00119 m_currentBeforeDropItem = selectedItem(); 00120 00121 QListViewItem *item = itemAt( contentsToViewport( ev->pos() ) ); 00122 if( item ) 00123 { 00124 m_dropItem = item; 00125 m_autoOpenTimer->start( KFileView::autoOpenDelay() ); 00126 } 00127 else 00128 { 00129 m_dropItem = 0; 00130 } 00131 } 00132 00133 void KFileTreeView::contentsDragMoveEvent( QDragMoveEvent *e ) 00134 { 00135 if( ! acceptDrag( e ) ) 00136 { 00137 e->ignore(); 00138 return; 00139 } 00140 e->acceptAction(); 00141 00142 00143 QListViewItem *afterme; 00144 QListViewItem *parent; 00145 00146 findDrop( e->pos(), parent, afterme ); 00147 00148 // "afterme" is 0 when aiming at a directory itself 00149 QListViewItem *item = afterme ? afterme : parent; 00150 00151 if( item && item->isSelectable() ) 00152 { 00153 setSelected( item, true ); 00154 if( item != m_dropItem ) { 00155 m_autoOpenTimer->stop(); 00156 m_dropItem = item; 00157 m_autoOpenTimer->start( KFileView::autoOpenDelay() ); 00158 } 00159 } 00160 else 00161 { 00162 m_autoOpenTimer->stop(); 00163 m_dropItem = 0; 00164 } 00165 } 00166 00167 void KFileTreeView::contentsDragLeaveEvent( QDragLeaveEvent * ) 00168 { 00169 // Restore the current item to what it was before the dragging (#17070) 00170 if ( isValidItem(m_currentBeforeDropItem) ) 00171 { 00172 setSelected( m_currentBeforeDropItem, true ); 00173 ensureItemVisible( m_currentBeforeDropItem ); 00174 } 00175 else if ( isValidItem(m_dropItem) ) 00176 setSelected( m_dropItem, false ); // no item selected 00177 m_currentBeforeDropItem = 0; 00178 m_dropItem = 0; 00179 00180 } 00181 00182 void KFileTreeView::contentsDropEvent( QDropEvent *e ) 00183 { 00184 00185 m_autoOpenTimer->stop(); 00186 m_dropItem = 0; 00187 00188 kdDebug(250) << "contentsDropEvent !" << endl; 00189 if( ! acceptDrag( e ) ) { 00190 e->ignore(); 00191 return; 00192 } 00193 00194 e->acceptAction(); 00195 QListViewItem *afterme; 00196 QListViewItem *parent; 00197 findDrop(e->pos(), parent, afterme); 00198 00199 //kdDebug(250) << " parent=" << (parent?parent->text(0):QString::null) 00200 // << " afterme=" << (afterme?afterme->text(0):QString::null) << endl; 00201 00202 if (e->source() == viewport() && itemsMovable()) 00203 movableDropEvent(parent, afterme); 00204 else 00205 { 00206 emit dropped(e, afterme); 00207 emit dropped(this, e, afterme); 00208 emit dropped(e, parent, afterme); 00209 emit dropped(this, e, parent, afterme); 00210 00211 KURL::List urls; 00212 KURLDrag::decode( e, urls ); 00213 emit dropped( this, e, urls ); 00214 00215 KURL parentURL; 00216 if( parent ) 00217 parentURL = static_cast<KFileTreeViewItem*>(parent)->url(); 00218 else 00219 // can happen when dropping above the root item 00220 // Should we choose the first branch in such a case ?? 00221 return; 00222 00223 emit dropped( urls, parentURL ); 00224 emit dropped( this , e, urls, parentURL ); 00225 } 00226 } 00227 00228 bool KFileTreeView::acceptDrag(QDropEvent* e ) const 00229 { 00230 00231 bool ancestOK= acceptDrops(); 00232 // kdDebug(250) << "Do accept drops: " << ancestOK << endl; 00233 ancestOK = ancestOK && itemsMovable(); 00234 // kdDebug(250) << "acceptDrag: " << ancestOK << endl; 00235 // kdDebug(250) << "canDecode: " << KURLDrag::canDecode(e) << endl; 00236 // kdDebug(250) << "action: " << e->action() << endl; 00237 00238 /* KListView::acceptDrag(e); */ 00239 /* this is what KListView does: 00240 * acceptDrops() && itemsMovable() && (e->source()==viewport()); 00241 * ask acceptDrops and itemsMovable, but not the third 00242 */ 00243 return ancestOK && KURLDrag::canDecode( e ) && 00244 // Why this test? All DnDs are one of those AFAIK (DF) 00245 ( e->action() == QDropEvent::Copy 00246 || e->action() == QDropEvent::Move 00247 || e->action() == QDropEvent::Link ); 00248 } 00249 00250 00251 00252 QDragObject * KFileTreeView::dragObject() 00253 { 00254 00255 KURL::List urls; 00256 const QPtrList<QListViewItem> fileList = selectedItems(); 00257 QPtrListIterator<QListViewItem> it( fileList ); 00258 for ( ; it.current(); ++it ) 00259 { 00260 urls.append( static_cast<KFileTreeViewItem*>(it.current())->url() ); 00261 } 00262 QPoint hotspot; 00263 QPixmap pixmap; 00264 if( urls.count() > 1 ){ 00265 pixmap = DesktopIcon( "kmultiple", 16 ); 00266 } 00267 if( pixmap.isNull() ) 00268 pixmap = currentKFileTreeViewItem()->fileItem()->pixmap( 16 ); 00269 hotspot.setX( pixmap.width() / 2 ); 00270 hotspot.setY( pixmap.height() / 2 ); 00271 QDragObject* dragObject = new KURLDrag( urls, this ); 00272 if( dragObject ) 00273 dragObject->setPixmap( pixmap, hotspot ); 00274 return dragObject; 00275 } 00276 00277 00278 00279 void KFileTreeView::slotCollapsed( QListViewItem *item ) 00280 { 00281 KFileTreeViewItem *kftvi = static_cast<KFileTreeViewItem*>(item); 00282 kdDebug(250) << "hit slotCollapsed" << endl; 00283 if( kftvi && kftvi->isDir()) 00284 { 00285 item->setPixmap( 0, itemIcon(kftvi)); 00286 } 00287 } 00288 00289 void KFileTreeView::slotExpanded( QListViewItem *item ) 00290 { 00291 kdDebug(250) << "slotExpanded here !" << endl; 00292 00293 if( ! item ) return; 00294 00295 KFileTreeViewItem *it = static_cast<KFileTreeViewItem*>(item); 00296 KFileTreeBranch *branch = it->branch(); 00297 00298 /* Start the animation for the branch object */ 00299 if( it->isDir() && branch && item->childCount() == 0 ) 00300 { 00301 /* check here if the branch really needs to be populated again */ 00302 kdDebug(250 ) << "starting to open " << it->url().prettyURL() << endl; 00303 startAnimation( it ); 00304 bool branchAnswer = branch->populate( it->url(), it ); 00305 kdDebug(250) << "Branches answer: " << branchAnswer << endl; 00306 if( ! branchAnswer ) 00307 { 00308 kdDebug(250) << "ERR: Could not populate!" << endl; 00309 stopAnimation( it ); 00310 } 00311 } 00312 00313 /* set a pixmap 'open folder' */ 00314 if( it->isDir() && isOpen( item ) ) 00315 { 00316 kdDebug(250)<< "Setting open Pixmap" << endl; 00317 item->setPixmap( 0, itemIcon( it )); // 0, m_openFolderPixmap ); 00318 } 00319 } 00320 00321 00322 00323 void KFileTreeView::slotExecuted( QListViewItem *item ) 00324 { 00325 if ( !item ) 00326 return; 00327 /* This opens the dir and causes the Expanded-slot to be called, 00328 * which strolls through the children. 00329 */ 00330 if( static_cast<KFileTreeViewItem*>(item)->isDir()) 00331 { 00332 item->setOpen( !item->isOpen() ); 00333 } 00334 } 00335 00336 00337 void KFileTreeView::slotAutoOpenFolder() 00338 { 00339 m_autoOpenTimer->stop(); 00340 00341 if ( !isValidItem(m_dropItem) || m_dropItem->isOpen() ) 00342 return; 00343 00344 m_dropItem->setOpen( true ); 00345 m_dropItem->repaint(); 00346 } 00347 00348 00349 void KFileTreeView::slotSelectionChanged() 00350 { 00351 if ( !m_dropItem ) // don't do this while the dragmove thing 00352 { 00353 } 00354 } 00355 00356 00357 KFileTreeBranch* KFileTreeView::addBranch( const KURL &path, const QString& name, 00358 bool showHidden ) 00359 { 00360 const QPixmap& folderPix = KMimeType::mimeType("inode/directory")->pixmap( KIcon::Desktop,KIcon::SizeSmall ); 00361 00362 return addBranch( path, name, folderPix, showHidden); 00363 } 00364 00365 KFileTreeBranch* KFileTreeView::addBranch( const KURL &path, const QString& name, 00366 const QPixmap& pix, bool showHidden ) 00367 { 00368 kdDebug(250) << "adding another root " << path.prettyURL() << endl; 00369 00370 /* Open a new branch */ 00371 KFileTreeBranch *newBranch = new KFileTreeBranch( this, path, name, pix, 00372 showHidden ); 00373 return addBranch(newBranch); 00374 } 00375 00376 KFileTreeBranch *KFileTreeView::addBranch(KFileTreeBranch *newBranch) 00377 { 00378 connect( newBranch, SIGNAL(populateFinished( KFileTreeViewItem* )), 00379 this, SLOT( slotPopulateFinished( KFileTreeViewItem* ))); 00380 00381 connect( newBranch, SIGNAL( newTreeViewItems( KFileTreeBranch*, 00382 const KFileTreeViewItemList& )), 00383 this, SLOT( slotNewTreeViewItems( KFileTreeBranch*, 00384 const KFileTreeViewItemList& ))); 00385 00386 m_branches.append( newBranch ); 00387 return( newBranch ); 00388 } 00389 00390 KFileTreeBranch *KFileTreeView::branch( const QString& searchName ) 00391 { 00392 KFileTreeBranch *branch = 0; 00393 QPtrListIterator<KFileTreeBranch> it( m_branches ); 00394 00395 while ( (branch = it.current()) != 0 ) { 00396 ++it; 00397 QString bname = branch->name(); 00398 kdDebug(250) << "This is the branches name: " << bname << endl; 00399 if( bname == searchName ) 00400 { 00401 kdDebug(250) << "Found branch " << bname << " and return ptr" << endl; 00402 return( branch ); 00403 } 00404 } 00405 return ( 0L ); 00406 } 00407 00408 KFileTreeBranchList& KFileTreeView::branches() 00409 { 00410 return( m_branches ); 00411 } 00412 00413 00414 bool KFileTreeView::removeBranch( KFileTreeBranch *branch ) 00415 { 00416 if(m_branches.contains(branch)) 00417 { 00418 delete (branch->root()); 00419 m_branches.remove( branch ); 00420 return true; 00421 } 00422 else 00423 { 00424 return false; 00425 } 00426 } 00427 00428 void KFileTreeView::setDirOnlyMode( KFileTreeBranch* branch, bool bom ) 00429 { 00430 if( branch ) 00431 { 00432 branch->setDirOnlyMode( bom ); 00433 } 00434 } 00435 00436 00437 void KFileTreeView::slotPopulateFinished( KFileTreeViewItem *it ) 00438 { 00439 if( it && it->isDir()) 00440 stopAnimation( it ); 00441 } 00442 00443 void KFileTreeView::slotNewTreeViewItems( KFileTreeBranch* branch, const KFileTreeViewItemList& itemList ) 00444 { 00445 if( ! branch ) return; 00446 kdDebug(250) << "hitting slotNewTreeViewItems" << endl; 00447 00448 /* Sometimes it happens that new items should become selected, i.e. if the user 00449 * creates a new dir, he probably wants it to be selected. This can not be done 00450 * right after creating the directory or file, because it takes some time until 00451 * the item appears here in the treeview. Thus, the creation code sets the member 00452 * m_neUrlToSelect to the required url. If this url appears here, the item becomes 00453 * selected and the member nextUrlToSelect will be cleared. 00454 */ 00455 if( ! m_nextUrlToSelect.isEmpty() ) 00456 { 00457 KFileTreeViewItemListIterator it( itemList ); 00458 00459 bool end = false; 00460 for( ; !end && it.current(); ++it ) 00461 { 00462 KURL url = (*it)->url(); 00463 00464 if( m_nextUrlToSelect.equals(url, true )) // ignore trailing / on dirs 00465 { 00466 setCurrentItem( static_cast<QListViewItem*>(*it) ); 00467 m_nextUrlToSelect = KURL(); 00468 end = true; 00469 } 00470 } 00471 } 00472 } 00473 00474 QPixmap KFileTreeView::itemIcon( KFileTreeViewItem *item, int gap ) const 00475 { 00476 QPixmap pix; 00477 kdDebug(250) << "Setting icon for column " << gap << endl; 00478 00479 if( item ) 00480 { 00481 /* Check if it is a branch root */ 00482 KFileTreeBranch *brnch = item->branch(); 00483 if( item == brnch->root() ) 00484 { 00485 pix = brnch->pixmap(); 00486 if( m_wantOpenFolderPixmaps && brnch->root()->isOpen() ) 00487 { 00488 pix = brnch->openPixmap(); 00489 } 00490 } 00491 else 00492 { 00493 // TODO: different modes, user Pixmaps ? 00494 pix = item->fileItem()->pixmap( KIcon::SizeSmall ); // , KIcon::DefaultState); 00495 00496 /* Only if it is a dir and the user wants open dir pixmap and it is open, 00497 * change the fileitem's pixmap to the open folder pixmap. */ 00498 if( item->isDir() && m_wantOpenFolderPixmaps ) 00499 { 00500 if( isOpen( static_cast<QListViewItem*>(item))) 00501 pix = m_openFolderPixmap; 00502 } 00503 } 00504 } 00505 00506 return pix; 00507 } 00508 00509 00510 void KFileTreeView::slotAnimation() 00511 { 00512 MapCurrentOpeningFolders::Iterator it = m_mapCurrentOpeningFolders.begin(); 00513 MapCurrentOpeningFolders::Iterator end = m_mapCurrentOpeningFolders.end(); 00514 for (; it != end;) 00515 { 00516 KFileTreeViewItem *item = it.key(); 00517 if (!isValidItem(item)) 00518 { 00519 ++it; 00520 m_mapCurrentOpeningFolders.remove(item); 00521 continue; 00522 } 00523 00524 uint & iconNumber = it.data().iconNumber; 00525 QString icon = QString::fromLatin1( it.data().iconBaseName ).append( QString::number( iconNumber ) ); 00526 // kdDebug(250) << "Loading icon " << icon << endl; 00527 item->setPixmap( 0, DesktopIcon( icon,KIcon::SizeSmall,KIcon::ActiveState )); // KFileTreeViewFactory::instance() ) ); 00528 00529 iconNumber++; 00530 if ( iconNumber > it.data().iconCount ) 00531 iconNumber = 1; 00532 00533 ++it; 00534 } 00535 } 00536 00537 00538 void KFileTreeView::startAnimation( KFileTreeViewItem * item, const char * iconBaseName, uint iconCount ) 00539 { 00540 /* TODO: allow specific icons */ 00541 if( ! item ) 00542 { 00543 kdDebug(250) << " startAnimation Got called without valid item !" << endl; 00544 return; 00545 } 00546 00547 m_mapCurrentOpeningFolders.insert( item, 00548 AnimationInfo( iconBaseName, 00549 iconCount, 00550 itemIcon(item, 0) ) ); 00551 if ( !m_animationTimer->isActive() ) 00552 m_animationTimer->start( 50 ); 00553 } 00554 00555 void KFileTreeView::stopAnimation( KFileTreeViewItem * item ) 00556 { 00557 if( ! item ) return; 00558 00559 kdDebug(250) << "Stoping Animation !" << endl; 00560 00561 MapCurrentOpeningFolders::Iterator it = m_mapCurrentOpeningFolders.find(item); 00562 if ( it != m_mapCurrentOpeningFolders.end() ) 00563 { 00564 if( item->isDir() && isOpen( item) ) 00565 { 00566 kdDebug(250) << "Setting folder open pixmap !" << endl; 00567 item->setPixmap( 0, itemIcon( item )); 00568 } 00569 else 00570 { 00571 item->setPixmap( 0, it.data().originalPixmap ); 00572 } 00573 m_mapCurrentOpeningFolders.remove( item ); 00574 } 00575 else 00576 { 00577 if( item ) 00578 kdDebug(250)<< "StopAnimation - could not find item " << item->url().prettyURL()<< endl; 00579 else 00580 kdDebug(250)<< "StopAnimation - item is zero !" << endl; 00581 } 00582 if (m_mapCurrentOpeningFolders.isEmpty()) 00583 m_animationTimer->stop(); 00584 } 00585 00586 KFileTreeViewItem * KFileTreeView::currentKFileTreeViewItem() const 00587 { 00588 return static_cast<KFileTreeViewItem *>( selectedItem() ); 00589 } 00590 00591 KURL KFileTreeView::currentURL() const 00592 { 00593 KFileTreeViewItem *item = currentKFileTreeViewItem(); 00594 if ( item ) 00595 return currentKFileTreeViewItem()->url(); 00596 else 00597 return KURL(); 00598 } 00599 00600 void KFileTreeView::slotOnItem( QListViewItem *item ) 00601 { 00602 KFileTreeViewItem *i = static_cast<KFileTreeViewItem *>( item ); 00603 if( i ) 00604 { 00605 const KURL url = i->url(); 00606 if ( url.isLocalFile() ) 00607 emit onItem( url.path() ); 00608 else 00609 emit onItem( url.prettyURL() ); 00610 } 00611 } 00612 00613 void KFileTreeView::slotItemRenamed(QListViewItem* item, const QString &name, int col) 00614 { 00615 (void) item; 00616 kdDebug(250) << "Do not bother: " << name << col << endl; 00617 } 00618 00619 KFileTreeViewItem *KFileTreeView::findItem( const QString& branchName, const QString& relUrl ) 00620 { 00621 KFileTreeBranch *br = branch( branchName ); 00622 return( findItem( br, relUrl )); 00623 } 00624 00625 KFileTreeViewItem *KFileTreeView::findItem( KFileTreeBranch* brnch, const QString& relUrl ) 00626 { 00627 KFileTreeViewItem *ret = 0; 00628 if( brnch ) 00629 { 00630 KURL url = brnch->rootUrl(); 00631 00632 if( ! relUrl.isEmpty() && QDir::isRelativePath(relUrl) ) 00633 { 00634 QString partUrl( relUrl ); 00635 00636 if( partUrl.endsWith("/")) 00637 partUrl.truncate( relUrl.length()-1 ); 00638 00639 url.addPath( partUrl ); 00640 00641 kdDebug(250) << "assembled complete dir string " << url.prettyURL() << endl; 00642 00643 KFileItem *fi = brnch->findByURL( url ); 00644 if( fi ) 00645 { 00646 ret = static_cast<KFileTreeViewItem*>( fi->extraData( brnch )); 00647 kdDebug(250) << "Found item !" <<ret << endl; 00648 } 00649 } 00650 else 00651 { 00652 ret = brnch->root(); 00653 } 00654 } 00655 return( ret ); 00656 } 00657 00660 00661 00662 void KFileTreeViewToolTip::maybeTip( const QPoint & ) 00663 { 00664 #if 0 00665 QListViewItem *item = m_view->itemAt( point ); 00666 if ( item ) { 00667 QString text = static_cast<KFileViewItem*>( item )->toolTipText(); 00668 if ( !text.isEmpty() ) 00669 tip ( m_view->itemRect( item ), text ); 00670 } 00671 #endif 00672 } 00673 00674 void KFileTreeView::virtual_hook( int id, void* data ) 00675 { KListView::virtual_hook( id, data ); } 00676 00677 #include "kfiletreeview.moc"
KDE Logo
This file is part of the documentation for kio Library Version 3.4.0.
Documentation copyright © 1996-2004 the KDE developers.
Generated on Thu Apr 14 00:20:25 2005 by doxygen 1.3.7 written by Dimitri van Heesch, © 1997-2003