/*************************************************************************** * snippet feature from kdevelop/plugins/snippet/ * * * * Copyright (C) 2007 by Robert Gruber * * rgruber@users.sourceforge.net * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "snippetwidget.h" #include "snippetitem.h" #include "snippetdlg.h" #include "messageviewer/autoqpointer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include SnippetWidget::SnippetWidget(KMeditor* editor, KActionCollection* actionCollection, QWidget* parent) : QTreeWidget( parent ), mEditor( editor ), mActionCollection( actionCollection ) { setObjectName("snippet widget"); // init the QTreeWidget //setSorting( -1 ); //setFullWidth(true); header()->hide(); setAcceptDrops(true); setDragEnabled(true); //setDropVisualizer(false); setRootIsDecorated(true); setAlternatingRowColors( true ); connect( this, SIGNAL( itemActivated (QTreeWidgetItem *, int) ), this, SLOT( slotEdit( QTreeWidgetItem *) ) ); //### in KDE 3 the enter key used to insert the snippet in the editor // (slotExecute()) but the enterPressed signal is gone. It is also a problem // that pressing enter on an item and double-clicking is supposed have the // exact same effect. connect( editor, SIGNAL( insertSnippet() ), this, SLOT( slotExecute() )); QTimer::singleShot(0, this, SLOT(readConfig())); } SnippetWidget::~SnippetWidget() { writeConfig(); /* We need to delete the child items before the parent items otherwise KMail would crash on exiting */ int itemsLeft = _list.count(); while (itemsLeft) { for (int i = 0; i < _list.count(); i++) { if (_list[i] && _list[i]->childCount() == 0) { delete _list[i]; _list[i] = 0; itemsLeft--; } } } } /*! Opens the dialog to add a snippet */ void SnippetWidget::slotAdd() { //kDebug() << "Ender slotAdd()"; AutoQPointer dlg( new SnippetDlg( mActionCollection, this ) ); dlg->setObjectName( "SnippetDlg" ); /*check if the user clicked a SnippetGroup If not, we set the group variable to the SnippetGroup which the selected item is a child of*/ SnippetGroup * group = dynamic_cast(selectedItem()); if ( !group && selectedItem() ) group = dynamic_cast(selectedItem()->parent()); /* still no group, let's make a default one */ if (!group ) { if ( _list.isEmpty() ) { group = new SnippetGroup(this , i18nc( "@label:listbox General group for often used snippets in mail.", "General") , SnippetGroup::getMaxId() ); _list.append( group ); } else { group = dynamic_cast( _list.first() ); } } assert( group ); /*fill the combobox with the names of all SnippetGroup entries*/ foreach (SnippetItem *item, _list) { if (dynamic_cast(item)) { dlg->cbGroup->addItem(item->getName()); } } dlg->cbGroup->setCurrentIndex( dlg->cbGroup->findText( group->getName() ) ); if ( dlg->exec() == KDialog::Accepted && dlg ) { group = dynamic_cast(SnippetItem::findItemByName(dlg->cbGroup->currentText(), _list)); _list.append( makeItem( group, dlg->snippetName->text(), dlg->snippetText->toPlainText(), dlg->keyWidget->keySequence() ) ); } } SnippetItem* SnippetWidget::makeItem( SnippetItem *parent, const QString &name, const QString &text, const QKeySequence &keySeq ) { SnippetItem *item = new SnippetItem( parent, name, text ); const QString actionName = i18n( "Snippet %1", name ); const QString normalizedName = QString( actionName ).replace( ' ', '_' ); item->setToolTip( 0, text ); if ( !mActionCollection->action( normalizedName ) ) { KAction *action = mActionCollection->addAction( normalizedName, item, SLOT( slotExecute() ) ); action->setText( actionName ); action->setShortcut( keySeq ); item->setAction( action ); connect( item, SIGNAL( execute( QTreeWidgetItem* ) ), this, SLOT( slotExecuted( QTreeWidgetItem* ) ) ); } return item; } /*! Opens the dialog to add a snippet */ void SnippetWidget::slotAddGroup() { //kDebug() << "Ender slotAddGroup()"; AutoQPointer dlg( new SnippetDlg( mActionCollection, this ) ); dlg->setObjectName( "SnippetDlg" ); dlg->setGroupMode( true ); dlg->setWindowTitle( i18n("Add Group") ); if ( dlg->exec() == KDialog::Accepted && dlg ) { _list.append( new SnippetGroup(this, dlg->snippetName->text(), SnippetGroup::getMaxId() ) ); } } /*! Removes the selected snippet */ void SnippetWidget::slotRemove() { //get current data QTreeWidgetItem * item = currentItem(); SnippetItem *snip = dynamic_cast( item ); if (!snip) return; SnippetGroup *group = dynamic_cast( item ); if (group) { if (group->childCount() > 0 && KMessageBox::warningContinueCancel(this, i18n("Do you really want to remove this group and all its snippets?"),QString(), KStandardGuiItem::del()) == KMessageBox::Cancel) return; for ( int i = 0; i < _list.size(); i++ ) { if (_list[i]->getParent() == group->getId()) { delete _list.takeAt( i-- ); } } } //kDebug() << "remove " << snip->getName(); int snipIndex = _list.indexOf(snip); if (snipIndex > -1) delete _list.takeAt(snipIndex); } /*! Opens the dialog of editing the selected snippet */ void SnippetWidget::slotEdit( QTreeWidgetItem* item ) { if( item == 0 ) { item = currentItem(); } SnippetGroup *pGroup = dynamic_cast(item); SnippetItem *pSnippet = dynamic_cast( item ); if (!pSnippet || pGroup) /*selected item must be a SnippetItem but MUST not be a SnippetGroup*/ return; //init the dialog AutoQPointer dlg( new SnippetDlg( mActionCollection, this ) ); dlg->setObjectName( "SnippetDlg" ); dlg->snippetName->setText(pSnippet->getName()); dlg->snippetText->setText(pSnippet->getText()); dlg->btnAdd->setText(i18n("&Apply")); if ( pSnippet->getAction() ) dlg->keyWidget->setKeySequence( pSnippet->getAction()->shortcut().primary() ); dlg->setWindowTitle(i18n("Edit Snippet")); /*fill the combobox with the names of all SnippetGroup entries*/ foreach (SnippetItem *item, _list) { if (dynamic_cast(item)) { dlg->cbGroup->addItem( item->getName() ); } } QString parentGroupText = SnippetItem::findGroupById( pSnippet->getParent(), _list )->getName(); dlg->cbGroup->setCurrentIndex( dlg->cbGroup->findText( parentGroupText ) ); if ( dlg->exec() == KDialog::Accepted && dlg ) { //update the QListView and the SnippetItem item->setText( 0, dlg->snippetName->text() ); pSnippet->setName( dlg->snippetName->text() ); pSnippet->setText( dlg->snippetText->toPlainText() ); pSnippet->setToolTip( 0, pSnippet->getText() ); if ( pSnippet->getAction() ) pSnippet->getAction()->setShortcut( dlg->keyWidget->keySequence()); /* if the user changed the parent we need to move the snippet */ if ( SnippetItem::findGroupById(pSnippet->getParent(), _list)->getName() != dlg->cbGroup->currentText() ) { SnippetGroup * newGroup = dynamic_cast(SnippetItem::findItemByName(dlg->cbGroup->currentText(), _list)); pSnippet->parent()->removeChild(pSnippet); newGroup->addChild(pSnippet); pSnippet->resetParent(); } item->setSelected( true ); } } /*! Opens the dialog of editing the selected snippet-group */ void SnippetWidget::slotEditGroup() { //get current data QTreeWidgetItem * item = currentItem(); SnippetGroup *pGroup = dynamic_cast( item ); if (!pGroup) /*selected item MUST be a SnippetGroup*/ return; //init the dialog AutoQPointer dlg( new SnippetDlg( mActionCollection, this ) ); dlg->setObjectName( "SnippetDlg" ); dlg->setGroupMode( true ); dlg->snippetName->setText(pGroup->getName()); dlg->btnAdd->setText(i18n("&Apply")); dlg->setWindowTitle(i18n("Edit Group")); if ( dlg->exec() == KDialog::Accepted && dlg ) { //update the QListView and the SnippetGroup item->setText( 0, dlg->snippetName->text() ); pGroup->setName( dlg->snippetName->text() ); item->setSelected( true ); } } void SnippetWidget::slotExecuted(QTreeWidgetItem * item) { if( item == 0 ) { item = currentItem(); } SnippetItem *pSnippet = dynamic_cast( item ); if (!pSnippet || dynamic_cast(item)) return; //process variables if any, then insert into the active view insertIntoActiveView( parseText( pSnippet->getText() ) ); } /*! Inserts the parameter text into the active view */ void SnippetWidget::insertIntoActiveView( const QString &text ) { mEditor->insertPlainText( text ); } /*! Write the config file */ void SnippetWidget::writeConfig() { if( !_cfg ) return; _cfg->deleteGroup("SnippetPart"); //this is necessary otherwise delete entries will stay in list until //they get overwritten by a more recent entry KConfigGroup kcg = _cfg->group("SnippetPart"); QString strKeyName=""; QString strKeyText=""; QString strKeyId=""; int iSnipCount = 0; int iGroupCount = 0; foreach ( SnippetItem *const item, _list ) { //write the snippet-list SnippetGroup * group = dynamic_cast(item); if (group) { strKeyName=QString("snippetGroupName_%1").arg(iGroupCount); strKeyId=QString("snippetGroupId_%1").arg(iGroupCount); kcg.writeEntry(strKeyName, group->getName()); kcg.writeEntry(strKeyId, group->getId()); iGroupCount++; } else if (dynamic_cast(item)) { strKeyName=QString("snippetName_%1").arg(iSnipCount); strKeyText=QString("snippetText_%1").arg(iSnipCount); strKeyId=QString("snippetParent_%1").arg(iSnipCount); kcg.writeEntry(strKeyName, item->getName()); kcg.writeEntry(strKeyText, item->getText()); kcg.writeEntry(strKeyId, item->getParent()); KAction * action = item->getAction(); if ( action ) { const KShortcut& sc = action->shortcut(); if (!sc.isEmpty() ) { kcg.writeEntry( QString("snippetShortcut_%1").arg(iSnipCount), sc.toString() ); } } iSnipCount++; } else { } } kcg.writeEntry("snippetCount", iSnipCount); kcg.writeEntry("snippetGroupCount", iGroupCount); int iCount = 1; QMap::Iterator it; for ( it = _mapSaved.begin(); it != _mapSaved.end(); ++it ) { //write the saved variable values if (it.value().length()<=0) continue; //is the saved value has no length -> no need to save it strKeyName=QString("snippetSavedName_%1").arg(iCount); strKeyText=QString("snippetSavedVal_%1").arg(iCount); kcg.writeEntry(strKeyName, it.key()); kcg.writeEntry(strKeyText, it.value()); iCount++; } kcg.writeEntry("snippetSavedCount", iCount-1); kcg.sync(); } /*! Initial read the config file */ void SnippetWidget::readConfig() { if ( !_cfg ) _cfg = KSharedConfig::openConfig( "kmailsnippetrc", KConfig::NoGlobals ); KConfigGroup kcg = _cfg->group("SnippetPart"); QString strKeyName; QString strKeyText; QString strKeyId; SnippetItem *item; SnippetGroup *group; //kDebug(); //if entry doesn't get found, this will return -1 which we will need a bit later int iCount = kcg.readEntry("snippetGroupCount", -1); for ( int i=0; igetName() << " " << group->getId(); _list.append(group); // the assumption is that there are usually only a few groups/snippets, so better // show them directly which makes accessing them simpler group->setExpanded(true); } } /* Check if the snippetGroupCount property has been found if iCount is -1 this means, that the user has his snippets stored without groups. Should only happen with an empty config file. */ if (iCount != -1) { iCount = kcg.readEntry("snippetCount", 0); for ( int i=0; igetName() << " " << item->getParent(); _list.append(item); } } } else { } iCount = kcg.readEntry("snippetSavedCount", 0); for ( int i=1; i<=iCount; i++) { //read the saved-values and store in QMap strKeyName=QString("snippetSavedName_%1").arg(i); strKeyText=QString("snippetSavedVal_%1").arg(i); QString strNameVal=""; QString strTextVal=""; strNameVal = kcg.readEntry(strKeyName, ""); strTextVal = kcg.readEntry(strKeyText, ""); if (!strNameVal.isEmpty() && !strTextVal.isEmpty()) { _mapSaved[strNameVal] = strTextVal; } } } /*! * Shows the Popup-Menu if there is an item where the event occurred */ void SnippetWidget::contextMenuEvent( QContextMenuEvent *e ) { KMenu popup; SnippetItem *item = static_cast(itemAt(e->pos())); if ( item ) { popup.addTitle( item->getName() ); if (dynamic_cast(item)) { popup.addAction( i18n("Edit &group..."), this, SLOT( slotEditGroup() ) ); } else { popup.addAction( KIcon("edit-paste"), i18n("&Paste"), this, SLOT( slotExecuted() ) ); popup.addAction( KIcon("document-properties"), i18n("&Edit..."), this, SLOT( slotEdit() ) ); } popup.addAction( KIcon("edit-delete"), i18n("&Remove"), this, SLOT( slotRemove() ) ); popup.addSeparator(); } else { popup.addTitle(i18n("Text Snippets")); } popup.addAction( i18n("&Add Snippet..."), this, SLOT( slotAdd() ) ); popup.addAction( i18n("Add G&roup..."), this, SLOT( slotAddGroup() ) ); popup.exec(e->globalPos()); } /*! This function is used to parse the given QString for variables. If found the user will be prompted for a replacement value. It returns the string text with all replacements made */ QString SnippetWidget::parseText( const QString &text ) { QString str = text; QString strName; QString strNew; QString strMsg; int iFound = -1; int iEnd = -1; QMap mapVar; do { iFound = text.indexOf(QRegExp("\\$[A-Za-z-_0-9\\s]*\\$"), iEnd+1); //find the next variable by this QRegExp if (iFound >= 0) { iEnd = text.indexOf('$', iFound+1)+1; strName = text.mid(iFound, iEnd-iFound); if ( strName != QString('$') + QString('$') ) { //if not doubel-delimiter if ( mapVar[strName].length() <= 0 ) { // and not already in map strMsg = i18n( "Please enter the value for %1:", strName ); strNew = showSingleVarDialog( strName, &_mapSaved ); if ( strNew.isEmpty() ) return QString(); //user clicked Cancel } else { continue; //we have already handled this variable } } else { strNew = '$'; //if double-delimiter -> replace by single character } str.replace(strName, strNew); mapVar[strName] = strNew; } } while ( iFound != -1 ); return str; } /*! This function constructs a dialog which contains a label and a linedit for the given variable It return either the entered value or an empty string if the user hit cancel */ QString SnippetWidget::showSingleVarDialog( const QString &var, QMap *mapSave ) { // --BEGIN-- building a dynamic dialog AutoQPointer dlg( new QDialog( this ) ); // don't fix this krazy issues without actually trying the code! dlg->setWindowTitle(i18n("Enter Values for Variables")); QGridLayout * layout = new QGridLayout( dlg ); QGridLayout * layoutTop = new QGridLayout(); QGridLayout * layoutVar = new QGridLayout(); QGridLayout * layoutBtn = new QGridLayout(); layout->setMargin( 11 ); layout->setSpacing( 6 ); layoutTop->setMargin( 0 ); layoutTop->setSpacing( 6 ); layoutVar->setMargin( 0 ); layoutVar->setSpacing( 6 ); layoutBtn->setMargin( 0 ); layoutBtn->setSpacing( 6 ); layout->setObjectName( "layout" ); layoutTop->setObjectName( "layoutTop" ); layoutVar->setObjectName( "layoutVar" ); layoutBtn->setObjectName( "layoutBtn" ); KTextEdit * te = NULL; QLabel * labTop = NULL; QCheckBox * cb = NULL; labTop = new QLabel( dlg ); labTop->setObjectName( "label" ); layoutTop->addWidget(labTop, 0, 0); labTop->setText( i18n("Enter the replacement values for %1:", var) ); layout->addLayout( layoutTop, 0, 0, 1, 2 ); cb = new QCheckBox( dlg ); cb->setObjectName( "cbVar" ); cb->setChecked( false ); cb->setText(i18n( "Make value &default" )); te = new KTextEdit( dlg ); te->setObjectName( "teVar" ); layoutVar->addWidget( te, 0, 1, Qt::AlignTop); layoutVar->addWidget( cb, 1, 1, Qt::AlignTop); if ((*mapSave)[var].length() > 0) { cb->setChecked( true ); te->setText((*mapSave)[var]); } cb->setToolTip( i18n("Enable this to save the value entered to the right as the default value for this variable") ); cb->setWhatsThis( i18n("If you enable this option, the value entered to the right will be saved. " "If you use the same variable later, even in another snippet, the value entered to the right " "will be the default value for that variable.") ); layout->addLayout( layoutVar, 1, 0, 1, 2 ); KPushButton * btn1 = new KPushButton( KStandardGuiItem::cancel(), dlg ); btn1->setObjectName( "pushButton1") ; layoutBtn->addWidget( btn1, 0, 0 ); KPushButton * btn2 = new KPushButton( KStandardGuiItem::apply(), dlg ); btn2->setObjectName( "pushButton2") ; btn2->setDefault( true ); layoutBtn->addWidget( btn2, 0, 1 ); layout->addLayout( layoutBtn, 2, 0, 1, 2 ); te->setFocus(); // --END-- building a dynamic dialog //connect the buttons to the KDialog default slots connect(btn1, SIGNAL(clicked()), dlg, SLOT(reject()) ); connect(btn2, SIGNAL(clicked()), dlg, SLOT(accept()) ); //execute the dialog QString strReturn = ""; if ( dlg->exec() == KDialog::Accepted && dlg ) { if (cb->isChecked()) //if the checkbox is on; save the values for later (*mapSave)[var] = te->toPlainText(); else (*mapSave).remove(var); strReturn = te->toPlainText(); //copy the entered values back to the given map } // AutoQPointer will delete dlg when it goes out of scope. // Previously all widgets were deleted here but this should be enough. // - all widgets as well as the main layout are child to the dialog and // get deleted automatically // - all sub-layouts are child to the main layout and get deleted // automatically. return strReturn; } /*! Emulate Qt3's selectedItem() because it does exactly what we need here. */ QTreeWidgetItem * SnippetWidget::selectedItem() const { if ( selectedItems().isEmpty() ) return 0; return currentItem(); } /*! Check here if the data the user is about to drop fits our restrictions. We only accept dropps of plaintext, because from the dropped text we will create a snippet. */ void SnippetWidget::dragMoveEvent( QDragMoveEvent * event ) { QTreeWidgetItem * item = itemAt(event->pos()); if (item && event->mimeData()->hasText() && static_cast(event->source()) != this) { event->setDropAction( Qt::CopyAction ); event->accept(); } else { event->ignore(); } } void SnippetWidget::dragEnterEvent( QDragEnterEvent * event ) { if ( event->mimeData()->hasFormat("text/x-kmail-textsnippet") || event->mimeData()->hasText() ) { event->setDropAction( Qt::CopyAction ); event->accept(); } } void SnippetWidget::dropEvent( QDropEvent * event ) { QTreeWidgetItem * item = itemAt( event->pos() ); if ( !item || !event->mimeData()->hasText() ) { event->ignore(); return; } else { event->setDropAction( Qt::CopyAction ); event->accept(); } SnippetItem *snippet = dynamic_cast( item ); SnippetGroup *group = dynamic_cast( item ); SnippetGroup *parent = 0; if ( group ) parent = group; else { assert( dynamic_cast( snippet->parent() ) ); parent = static_cast( snippet->parent() ); } // fill the dialog with the given data AutoQPointer dlg( new SnippetDlg( mActionCollection, this ) ); dlg->setObjectName( "SnippetDlg" ); dlg->snippetName->clear(); dlg->snippetText->setText( event->mimeData()->text() ); /*fill the combobox with the names of all SnippetGroup entries*/ foreach ( SnippetItem *const si, _list ) { if ( dynamic_cast( si ) ) { dlg->cbGroup->addItem( si->getName() ); } } dlg->cbGroup->setCurrentIndex( dlg->cbGroup->findText( parent->getName() ) ); if ( dlg->exec() == KDialog::Accepted && dlg ) { /* get the group that the user selected with the combobox */ group = dynamic_cast( SnippetItem::findItemByName( dlg->cbGroup->currentText(), _list ) ); _list.append( makeItem( group, dlg->snippetName->text(), dlg->snippetText->toPlainText(), dlg->keyWidget->keySequence() ) ); } } void SnippetWidget::startDrag( Qt::DropActions supportedActions ) { Q_UNUSED( supportedActions ); // Don't allow to drag groups around if ( dynamic_cast( currentItem() ) ) return; QString text = static_cast( currentItem() )->getText(); if( text.isEmpty() ) return; QDrag *drag = new QDrag( this ); QMimeData *mimeData = new QMimeData(); mimeData->setData( "text/x-kmail-textsnippet", text.toUtf8() ); drag->setMimeData( mimeData ); drag->exec(); } void SnippetWidget::slotExecute() { slotExecuted(currentItem()); } #include "snippetwidget.moc"