Logo Search packages:      
Sourcecode: kdeaddons version File versions

plugin_katexmltools.cpp

/***************************************************************************
      plugin_katexmltools.cpp

      List elements, attributes, attribute values and entities allowed by DTD.
      Needs a DTD in XML format (as produced by dtdparse) for most features.

      copyright               : (C) 2001-2002 by Daniel Naber
      email                   : daniel.naber@t-online.de
 ***************************************************************************/

/***************************************************************************
 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.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 ***************************************************************************/

/*
README:
The basic idea is this: certain keyEvents(), namely [<&" ], trigger a completion box.
This is intended as a help for editing. There are some cases where the XML
spec is not followed, e.g. one can add the same attribute twice to an element.
Also see the user documentation. If backspace is pressed after a completion popup
was closed, the popup will re-open. This way typos can be corrected and the popup
will reappear, which is quite comfortable.

FIXME for jowenn if he has time:
-Ctrl-Z doesn't work if completion is visible
-Typing with popup works, but right/left cursor keys and start/end don't, i.e.
 they should be ignored by the completion (?)
-popup not completely visible if it's long and appears at the bottom of the screen

FIXME:
-(docbook) <author lang="">: insert space between the quotes, press "de" and return -> only "d" inserted
-Correctly support more than one view:
 charactersInteractivelyInserted(..) is tied to kv->document()
 but filterInsertString(..) is tied to kv
-The "Insert Element" dialog isn't case insensitive, but it should be
-fix upper/lower case problems (start typing lowercase if the tag etc. is upper case)
-See the "fixme"'s in the code

TODO:
-check for mem leaks
-add "Go to opening/parent tag"?
-check doctype to get top-level element
-can undo behaviour be improved?, e.g. the plugins internal deletions of text
 don't have to be an extra step
-don't offer entities if inside tag but outside attribute value

-Support for more than one namespace at the same time (e.g. XSLT + XSL-FO)?
=>This could also be handled in the XSLT DTD fragment, as described in the XSLT 1.0 spec,
 but then at <xsl:template match="/"><html> it will only show you HTML elements!
=>So better "Assign meta DTD" and "Add meta DTD", the latter will expand the current meta DTD
-Option to insert empty element in <empty/> form
-Show expanded entities with QChar::QChar(int rc) + unicode font
-Don't ignore entities defined in the document's prologue
-Only offer 'valid' elements, i.e. don't take the elements as a set but check
 if the DTD is matched (order, number of occurences, ...)

-Maybe only read the meta DTD file once, then store the resulting QMap on disk (using QDataStream)?
 We'll then have to compare time_of_cache_file <-> time_of_meta_dtd.
-Try to use libxml
*/

#include "plugin_katexmltools.h"
#include "plugin_katexmltools.moc"

#include <assert.h>

#include <qdatetime.h>
#include <qdom.h>
#include <qfile.h>
#include <qlayout.h>
#include <qlistbox.h>
#include <qprogressdialog.h>
#include <qpushbutton.h>
#include <qregexp.h>
#include <qstring.h>
#include <qtimer.h>

#include <kaction.h>
#include <kbuttonbox.h>
#include <klineedit.h>
#include <kcursor.h>
#include <kdebug.h>
#include <kfiledialog.h>
#include <kglobal.h>
#include <kinstance.h>
#include <kio/job.h>
#include <klocale.h>
#include <kmessagebox.h>
#include <kstandarddirs.h>
#include <kgenericfactory.h>

K_EXPORT_COMPONENT_FACTORY( katexmltoolsplugin, KGenericFactory<PluginKateXMLTools>( "katexmltools" ) )

class PluginView : public KXMLGUIClient
{
  friend class PluginKateXMLTools;

  public:
      Kate::MainWindow *win;
};

PluginKateXMLTools::PluginKateXMLTools(QObject* parent, const char* name, const QStringList&)
      : Kate::Plugin ((Kate::Application*)parent, name)
{
      //kdDebug() << "PluginKateXMLTools constructor called" << endl;

      m_dtd_string = QString();
      m_url_string = QString();

      m_mode = none;
      m_correct_pos = 0;

      m_last_line = 0;
      m_last_col = 0;
      m_last_allowed = QStringList();
      m_popup_open_col = -1;

      m_dtds.setAutoDelete(true);
}

PluginKateXMLTools::~PluginKateXMLTools()
{
      //kdDebug() << "xml tools descructor 1..." << endl;
}

void PluginKateXMLTools::addView(Kate::MainWindow *win)
{
      //application()->installEventFilter(this);
      // TODO: doesn't this have to be deleted?
      PluginView *view = new PluginView ();
      (void) new KAction ( i18n("&Insert Element..."), CTRL+Key_Return, this,
            SLOT(slotInsertElement()), view->actionCollection(), "xml_tool_insert_element" );
      (void) new KAction ( i18n("&Close Element"), CTRL+Key_Less, this,
            SLOT(slotCloseElement()), view->actionCollection(), "xml_tool_close_element" );
      (void) new KAction ( i18n("Assign Meta &DTD..."), 0, this,
            SLOT(getDTD()), view->actionCollection(), "xml_tool_assign" );
      view->setInstance( new KInstance("kate") );
      view->setXMLFile("plugins/katexmltools/ui.rc");
      win->guiFactory()->addClient( view );
      view->win = win;
      m_views.append( view );
}

void PluginKateXMLTools::removeView(Kate::MainWindow *win)
{
      for (uint z=0; z < m_views.count(); z++) {
            if (m_views.at(z)->win == win)
            {
                  PluginView *view = m_views.at(z);
                  m_views.remove (view);
                  win->guiFactory()->removeClient (view);
                  delete view;
            }
      }
}

void PluginKateXMLTools::backspacePressed()
{
      kdDebug() << "xml tools backspacePressed" << endl;

        if (!application()->activeMainWindow())
          return;

      Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView();
      if( ! kv ) {
            kdDebug() << "Warning: no Kate::View" << endl;
            return;
      }
      uint line, col;
      kv->cursorPositionReal(&line, &col);
      //kdDebug() << "++ redisplay popup? line:" << line << ", col: " << col << endl;
      if( m_last_line == line && col == m_last_col ) {
            int len = col - m_popup_open_col;
            if( len < 0 ) {
                  kdDebug() << "**Warning: len < 0" << endl;
                  return;
            }
            //kdDebug() << "++ redisplay popup, " << m_last_allowed.count() << ", len:" << len <<endl;
            connectSlots(kv);
            kv->showCompletionBox(stringListToCompletionEntryList(m_last_allowed), len, false);
      }
      return;
}

void PluginKateXMLTools::emptyKeyEvent()
{
      keyEvent(0, 0, QString::null);
}

void PluginKateXMLTools::keyEvent(int, int, const QString &/*s*/)
{
      //kdDebug() << "xml tools keyEvent: '" << s << endl;

        if (!application()->activeMainWindow())
          return;

      Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView();
      if( ! kv ) {
            kdDebug() << "Warning: no Kate::View" << endl;
            return;
      }

      if( ! m_dtds[kv->document()] ) {
            // no meta DTD assigned yet
            //kdDebug() << "not ready, no meta DTD assigned yet for this view/document" << endl;
            return;
      }

      // debug to test speed:
      //QTime t; t.start();

      QStringList allowed = QStringList();

      // get char on the left of the cursor:
      uint line, col;
      kv->cursorPositionReal(&line, &col);
      QString line_str = kv->getDoc()->textLine(line);
      QString left_ch = line_str.mid(col-1, 1);
      QString second_left_ch = line_str.mid(col-2, 1);
      // if eventFilter is used:
      //QString left_ch = s;
      //QString second_left_ch = line_str.mid(col-1, 1);

      if( left_ch == "&" ) {
            kdDebug() << "Getting entities" << endl;
            allowed = m_dtds[kv->document()]->getEntitiesFast("");
            m_mode = entities;
      } else if( left_ch == "<" ) {
            kdDebug() << "*outside tag -> get elements" << endl;
            QString parent_element = getParentElement(*kv, true);
            kdDebug() << "parent: " << parent_element << endl;
            allowed = m_dtds[kv->document()]->getAllowedElementsFast(parent_element);
            m_mode = elements;
      //} else if( left_ch == ">" ) {
      // TODO: optionally close parent tag if not left=="/>"
      } else if( left_ch == " " || (isQuote(left_ch) && second_left_ch == "=") ) {
      // TODO: check second_left_char, too?! then you don't need to trigger
      // with space and we yet save CPU power
            QString current_element = insideTag(*kv);
            QString current_attribute;
            if( ! current_element.isEmpty() ) {
                  current_attribute = insideAttribute(*kv);
            }
            kdDebug() << "Tag: " << current_element << endl;
            kdDebug() << "Attr: " << current_attribute << endl;
            if( ! current_element.isEmpty() && ! current_attribute.isEmpty() ) {
                  kdDebug() << "*inside attribute -> get attribute values" << endl;
                  allowed = m_dtds[kv->document()]->getAllowedAttributeValuesFast(current_element, current_attribute);
                  if( allowed.count() == 1 &&
                        (allowed[0] == "CDATA" || allowed[0] == "ID" || allowed[0] == "IDREF" ||
                        allowed[0] == "IDREFS" || allowed[0] == "ENTITY" || allowed[0] == "ENTITIES" ||
                        allowed[0] == "NMTOKEN" || allowed[0] == "NMTOKENS" || allowed[0] == "NAME") ) {
                        // these must not be taken literally, e.g. don't insert the string "CDATA"
                        allowed.clear();
                  } else {
                        m_mode = attributevalues;
                  }
            } else if( ! current_element.isEmpty() ){
                  kdDebug() << "*inside tag -> get attributes" << endl;
                  allowed = m_dtds[kv->document()]->getAllowedAttributesFast(current_element);
                  m_mode = attributes;
            }
      }

      //kdDebug() << "time elapsed (ms): " << t.elapsed() << endl;
      //kdDebug() << "Allowed strings: " << allowed.count() << endl;

      if( allowed.count() >= 1 && allowed[0] != "__EMPTY" ) {
            allowed = sortQStringList(allowed);
            connectSlots(kv);
            kv->showCompletionBox(stringListToCompletionEntryList(allowed), 0, false);
            m_popup_open_col = col;
            m_last_allowed = allowed;
      }
      //else {
      //    m_last_allowed.clear();
      //}
}

QValueList<KTextEditor::CompletionEntry>
PluginKateXMLTools::stringListToCompletionEntryList(QStringList list)
{
      QValueList<KTextEditor::CompletionEntry> comp_list;
      KTextEditor::CompletionEntry entry;
      for( QStringList::Iterator it = list.begin(); it != list.end(); ++it ) {
            entry.text = (*it);
            comp_list << entry;
      }
      return comp_list;
}


/** disconnect all signals of a specified kateview from the local slots
  *
  */
void  PluginKateXMLTools::disconnectSlots(Kate::View *kv)
{
      disconnect(kv, SIGNAL(filterInsertString(KTextEditor::CompletionEntry*,QString*)), 0, 0);
      disconnect(kv, SIGNAL(completionDone(KTextEditor::CompletionEntry)), 0, 0);
      disconnect(kv, SIGNAL(completionAborted()), 0, 0);
}

/** connect all signals of a specified kateview to the local slots
  *
  */
void PluginKateXMLTools::connectSlots(Kate::View *kv)
{
      // disconnect everything so signals don't arrive twice if the user selects a new meta dtd:

      connect(kv, SIGNAL(filterInsertString(KTextEditor::CompletionEntry*,QString*)),
            this, SLOT(filterInsertString(KTextEditor::CompletionEntry*,QString*)));

      connect(kv, SIGNAL(completionDone(KTextEditor::CompletionEntry)),
            this, SLOT(completionDone(KTextEditor::CompletionEntry)));

      connect(kv, SIGNAL(completionAborted()), this, SLOT(completionAborted()));
}

/** Load the meta DTD. In case of success set the 'ready'
  * flag to true, to show that we're is ready to give hints about the DTD.
  */
void PluginKateXMLTools::getDTD()
{
        if (!application()->activeMainWindow())
          return;

      Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView();
      if( ! kv ) {
            kdDebug() << "Warning: no Kate::View" << endl;
            return;
      }

      disconnect(kv->document(), SIGNAL(charactersInteractivelyInserted(int,int,const QString&)), 0, 0);
      connect(kv->document(), SIGNAL(charactersInteractivelyInserted(int,int,const QString&)),
                        this, SLOT(keyEvent(int,int,const QString&)));

      disconnect(kv->document(), SIGNAL(backspacePressed()), 0, 0);
      connect(kv->document(), SIGNAL(backspacePressed()),
                        this, SLOT(backspacePressed()));

//    disconnectSlots(kv);
//    connectSlots(kv);


      /* Start where the supplied XML-DTDs are installed by default unless
         user changed directory last time:
       */
      QString default_dir = KGlobal::dirs()->findResourceDir("data", "katexmltools/") + "katexmltools/";
      if( m_url_string.isNull() ) {
            m_url_string = default_dir;
      }
      KURL url;

      /* Guess the meta DTD by looking at the doctype's public identifier.
         XML allows comments etc. before the doctype, so look further than
         just the first line.
         Example syntax:
         <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
      */
      uint check_max_lines = 200;
      QString document_start = kv->getDoc()->text(0, 0, check_max_lines+1, 0);
      QRegExp re("<!DOCTYPE\\s+(.*)\\s+PUBLIC\\s+[\"'](.*)[\"']", false);
      re.setMinimal(true);
      int match_pos = re.search(document_start);
      QString filename;
      QString doctype;
      QString top_element;
      if( match_pos != -1 ) {
            top_element = re.cap(1);
            doctype = re.cap(2);
            kdDebug() << "Top element: " << top_element << endl;
            kdDebug() << "Doctype match: " << doctype << endl;
            // XHTML:
            if( doctype == "-//W3C//DTD XHTML 1.0 Transitional//EN" ) {
                  filename = "xhtml1-transitional.dtd.xml";
            } else if( doctype == "-//W3C//DTD XHTML 1.0 Strict//EN" )  {
                  filename = "xhtml1-strict.dtd.xml";
            } else if( doctype == "-//W3C//DTD XHTML 1.0 Frameset//EN" )  {
                  filename = "xhtml1-frameset.dtd.xml";
            // HTML 4.0:
            } else if ( doctype == "-//W3C//DTD HTML 4.01 Transitional//EN" ) {
                  filename = "html4-loose.dtd.xml";
            } else if ( doctype == "-//W3C//DTD HTML 4.01//EN" ) {
                  filename = "html4-strict.dtd.xml";
            // KDE Docbook:
            } else if ( doctype == "-//KDE//DTD DocBook XML V4.1.2-Based Variant V1.1//EN" ) {
                  filename = "kde-docbook.dtd.xml";
            }
      } else if( document_start.find("<xsl:stylesheet") != -1 &&
                        document_start.find("xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"") != -1 ) {
            /* XSLT doesn't have a doctype/DTD. We look for an xsl:stylesheet tag instead.
               Example:
                  <xsl:stylesheet version="1.0"
                        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                        xmlns="http://www.w3.org/TR/xhtml1/strict">
            */
            filename = "xslt-1.0.dtd.xml";
            doctype = "XSLT 1.0";
      } else {
            kdDebug() << "No doctype found" << endl;
      }
      if( filename.isEmpty() ) {
            // no meta dtd found for this file
            url = KFileDialog::getOpenURL(m_url_string, "*.xml",
                  0, i18n("Assign Meta DTD in XML Format"));
      } else {
            url.setFileName(default_dir + filename);
            KMessageBox::information(0, i18n("The current file has been identified "
                  "as a document of type \"%1\". The meta DTD for this document type "
                  "will now be loaded.").arg(doctype),
                  i18n("Loading XML Meta DTD"),
                  QString::fromLatin1("DTDAssigned") );
      }

      // Load the Meta DTD
      if( url.isEmpty() ) {
            return;
      }
      m_url_string = url.url();     // remember directory for next time
      m_dtd_string = "";

      QApplication::setOverrideCursor(KCursor::waitCursor());
      KIO::Job *job = KIO::get(url);
      connect(job, SIGNAL(result(KIO::Job *)), this, SLOT(slotFinished(KIO::Job *)));
      connect(job, SIGNAL(data(KIO::Job *, const QByteArray &)), this, SLOT(slotData(KIO::Job *, const QByteArray &)));
}

void PluginKateXMLTools::slotFinished(KIO::Job *job)
{
      if( job->error() ) {
            //kdDebug() << "XML Plugin error: DTD in XML format (" << filename << ") could not be loaded" << endl;
            job->showErrorDialog(0);
      } else if ( static_cast<KIO::TransferJob *>(job)->isErrorPage() ) {
            // catch failed loading loading via http:
            KMessageBox::error(0, i18n("The file '%1' could not be opened. "
                  "The server returned an error.").arg(m_url_string),
                  i18n("XML Plugin Error"));
      } else {
            PseudoDTD *dtd = new PseudoDTD();
            dtd->analyzeDTD(m_url_string, m_dtd_string);

                if (!application()->activeMainWindow())
                  return;

            Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView();
            if( ! kv ) {
                  kdDebug() << "Warning: no Kate::View" << endl;
                  return;
            }
            m_dtds.replace(kv->document(), dtd);
      }
      QApplication::restoreOverrideCursor();
}

void PluginKateXMLTools::slotData(KIO::Job *, const QByteArray &data)
{
      m_dtd_string += QString(data);
}

/** Offer a line edit with completion for possible elements at cursor position and insert the
  * tag one chosen/entered by the user, plus its closing tag. If there's a text selection,
  * add the markup around it.
  */
void PluginKateXMLTools::slotInsertElement()
{
        if (!application()->activeMainWindow())
          return;

      Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView();
      if( ! kv ) {
            kdDebug() << "Warning: no Kate::View" << endl;
            return;
      }

      bool hasDTD = m_dtds[kv->document()];
      QString parent_element = getParentElement(*kv, false);
      //kdDebug() << "parent: " << parent_element << endl;
      QStringList allowed;
      if( /*m_dtds[kv->document()]*/hasDTD ) {
            allowed = m_dtds[kv->document()]->getAllowedElementsFast(parent_element);
      }

      InsertElement *dialog = new InsertElement(
            (QWidget *)application()->activeMainWindow()->viewManager()->activeView(), "insert_xml");
      QString text = dialog->showDialog(allowed);
      delete dialog;

      if( !text.isEmpty() ) {
            QStringList list = QStringList::split(' ', text);
            QString pre;
            QString post;
            // anders: use <tagname/> if the tag is required to be empty.
            // In that case maybe we should not remove the selection? or overwrite it?
            int adjust = 0; // how much to move cursor.
            // if we know that we have attributes, it goes
            // just after the tag name, otherwise between tags.
            if ( hasDTD && m_dtds[kv->document()]->getAllowedAttributesFast(list[0]).count() ) {
                        adjust++;   // the ">"
            }
            if ( hasDTD && m_dtds[kv->document()]->getAllowedElementsFast(list[0]).contains("__EMPTY") ) {
                  pre = "<" + text + "/>";
                  if (adjust) {
                        adjust++; // for the "/"
                  }
            } else {
                  pre = "<" + text + ">";
                  post ="</" + list[0] + ">";
            }
            QString marked;
            if ( ! post.isEmpty() ) {
                        marked = kv->getDoc()->selection();
            }
            if( marked.length() > 0 ) {
                  kv->getDoc()->removeSelectedText();
            }
            kv->insertText(pre + marked + post);

      }
}

/** Insert a closing tag for the nearest not-closed parent element.
  */
void PluginKateXMLTools::slotCloseElement()
{
        if (!application()->activeMainWindow())
          return;

      Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView();
      if( ! kv ) {
            kdDebug() << "Warning: no Kate::View" << endl;
            return;
      }
      QString parent_element = getParentElement(*kv, false);
      //kdDebug() << "parentElement: '" << parent_element << "'" << endl;
      QString close_tag = "</" + parent_element + ">";
      if( ! parent_element.isEmpty() ) {
            kv->insertText(close_tag);
      }
}

// modify the completion string before it gets inserted
void PluginKateXMLTools::filterInsertString(KTextEditor::CompletionEntry *ce, QString *text)
{
      kdDebug() << "filterInsertString str: " << *text << endl;
      kdDebug() << "filterInsertString text: " << ce->text << endl;

        if (!application()->activeMainWindow())
          return;

      Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView();
      if( ! kv ) {
            kdDebug() << "Warning (filterInsertString()): no Kate::View" << endl;
            return;
      }

      uint line, col;
      kv->cursorPositionReal(&line, &col);
      QString line_str = kv->getDoc()->textLine(line);
      QString left_ch = line_str.mid(col-1, 1);
      QString right_ch = line_str.mid(col, 1);

      m_correct_pos = 0;      // where to move the cursor after completion (>0 = move right)
      if( m_mode == entities ) {
            // This is a bit ugly, but entities are case-sensitive
            // and we want the correct completion even if the user started typing
            // e.g. in lower case but the entity is in upper case
            kv->getDoc()->removeText(line, col - (ce->text.length() - text->length()), line, col);
            *text = ce->text + ";";
      } else if( m_mode == attributes ) {
            *text = *text + "=\"\"";
            m_correct_pos = -1;
            if( !right_ch.isEmpty() && right_ch != ">" && right_ch != "/" && right_ch != " " ) {      // TODO: other whitespaces
                  // add space in front of the next attribute
                  *text = *text + " ";
                  m_correct_pos--;
            }
      } else if( m_mode == attributevalues ) {
            // TODO: support more than one line
            uint start_att_value = 0;
            uint end_att_value = 0;
            // find left quote:
            for( start_att_value = col; start_att_value > 0; start_att_value-- ) {
                  QString ch = line_str.mid(start_att_value-1, 1);
                  if( isQuote(ch) ) {
                        break;
                  }
            }
            // find right quote:
            for( end_att_value = col; end_att_value <= line_str.length(); end_att_value++ ) {
                  QString ch = line_str.mid(end_att_value-1, 1);
                  if( isQuote(ch) ) {
                        break;
                  }
            }
            // maybe the user has already typed something to trigger completion,
            // don't overwrite that:
            start_att_value += ce->text.length() - text->length();
            // delete the current contents of the attribute:
            if( start_att_value < end_att_value ) {
                  kv->getDoc()->removeText(line, start_att_value, line, end_att_value-1);
                  // FIXME: this makes the scrollbar jump
                  // but without it, inserting sometimes goes crazy :-(
                  kv->setCursorPositionReal(line, start_att_value);
            }
      } else if( m_mode == elements ) {
            // anders: if the tag is marked EMPTY, insert in form <tagname/>
            QString str;
            if ( m_dtds[kv->document()]->getAllowedElementsFast(ce->text).contains("__EMPTY") ) {
                  str = "/>";
            } else {
                  str = "></" + ce->text + ">";
            }
            *text = *text + str;
            m_correct_pos = - str.length();           // place cursor after tag name (not between the tags!)
      }
}

static void correct_pos(Kate::View *kv, int count)
{
      if( count > 0 ) {
            for( int i = 0; i < count; i++ ) {
                  kv->cursorRight();
            }
      } else if( count < 0 ) {
            for( int i = 0; i < -count; i++ ) {
                  kv->cursorLeft();
            }
      }
}

void PluginKateXMLTools::completionAborted()
{
        if (!application()->activeMainWindow())
          return;

      Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView();
      if( ! kv ) {
            kdDebug() << "Warning (completionAborted()): no Kate::View" << endl;
            return;
      }
      disconnectSlots(kv);
      kv->cursorPositionReal(&m_last_line, &m_last_col);
      m_last_col--;

      correct_pos(kv,m_correct_pos);
      m_correct_pos = 0;

      kdDebug() << "completionAborted() at line:" << m_last_line << ", col:" << m_last_col << endl;
}

void PluginKateXMLTools::completionDone(KTextEditor::CompletionEntry)
{
      kdDebug() << "completionDone()" << endl;

        if (!application()->activeMainWindow())
          return;

      Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView();
      if( ! kv ) {
            kdDebug() << "Warning (completionDone()): no Kate::View" << endl;
            return;
      }
      disconnectSlots(kv);

      correct_pos(kv,m_correct_pos);
      m_correct_pos = 0;

      if( m_mode == attributes ) {
            // immediately show attribute values:
            QTimer::singleShot(10, this, SLOT(emptyKeyEvent()));
      }

}

// ========================================================================
// Pseudo-XML stuff:

/** Check if cursor is inside a tag, that is
  * if "<" occurs before ">" occurs (on the left side of the cursor).
  * Return the tag name, return "" if we cursor is outside a tag.
  */
QString PluginKateXMLTools::insideTag(Kate::View &kv)
{
      uint line = 0, col = 0;
      kv.cursorPositionReal(&line, &col);
      int y = line;     // another variable because uint <-> int
      do {
            QString line_str = kv.getDoc()->textLine(y);
            for( uint x = col; x > 0; x-- ) {
                  QString ch = line_str.mid(x-1, 1);
                  if( ch == ">" ) {   // cursor is outside tag
                        return "";
                  } else if( ch == "<" ) {
                        QString tag;
                        // look for white space on the right to get the tag name
                        for( uint z = x; z <= line_str.length() ; z++ ) {
                              ch = line_str.mid(z-1, 1);
                              if( ch.at(0).isSpace() || ch == "/" || ch == ">" ) {
                                    return tag.right(tag.length()-1);
                              } else if( z == line_str.length() ) {   // end of line
                                    tag += ch;
                                    return tag.right(tag.length()-1);
                              } else {
                                    tag += ch;
                              }
                        }
                  }
            }
            y--;
            col = kv.getDoc()->textLine(y).length();
      } while( y >= 0 );

      return "";
}

/** Check if cursor is inside an attribute value, that is
  * if '="' is on the left, and if it's nearer than "<" or ">".
  * Return the attribute name or "" if we're outside an attribute
  * value.
  * Note: only call when insideTag() == true.
  * TODO: allow whitespace around "="
  */
QString PluginKateXMLTools::insideAttribute(Kate::View &kv)
{
      uint line = 0, col = 0;
      kv.cursorPositionReal(&line, &col);
      int y = line;     // another variable because uint <-> int
      uint x = 0;
      QString line_str = "";
      QString ch = "";
      do {
            line_str = kv.getDoc()->textLine(y);
            for( x = col; x > 0; x-- ) {
                  ch = line_str.mid(x-1, 1);
                  QString ch_left = line_str.mid(x-2, 1);
                  // TODO: allow whitespace
                  if( isQuote(ch) && ch_left == "=" ) {
                        break;
                  } else if( isQuote(ch) && ch_left != "=" ) {
                        return "";
                  } else if( ch == "<" || ch == ">" ) {
                        return "";
                  }
            }
            y--;
            col = kv.getDoc()->textLine(y).length();
      } while( !isQuote(ch) );

      // look for next white space on the left to get the tag name
      QString attr = "";
      for( int z = x; z >= 0; z-- ) {
            ch = line_str.mid(z-1, 1);
            if( ch.at(0).isSpace() ) {
                  break;
            } else if( z == 0 ) {   // start of line == whitespace
                  attr += ch;
                  break;
            } else {
                  attr = ch + attr;
            }
      }

      return attr.left(attr.length()-2);
}

/** Find the parent element for the current cursor position. That is,
  * go left and find the first opening element that's not closed yet,
  * ignoring empty elements.
  * Examples: If cursor is at "X", the correct parent element is "p":
  * <p> <a x="xyz"> foo <i> test </i> bar </a> X
  * <p> <a x="xyz"> foo bar </a> X
  * <p> foo <img/> bar X
  * <p> foo bar X
  * WARNING (TODO): behaviour undefined if called when cursor is
  * /inside/ an element!!
  */
QString PluginKateXMLTools::getParentElement(Kate::View &kv, bool ignore_single_bracket)
{

      int nesting_level = 1;

      bool in_tag = false;
      QString tag = "";

      uint line = 0, col = 0;
      kv.cursorPositionReal(&line, &col);
      col = col-1;
      int y = line;     // another variable because uint <-> int
      int y_prev = y;
      do {
            QString line_str = kv.getDoc()->textLine(y);
            int stop = ignore_single_bracket ? 1 : 0;
            for( int x = col; x >= stop; x-- ) {
                  QString ch = line_str.mid(x-stop, 1);
                  if( ch == ">" ) {
                        in_tag = true;
                        tag = "";
                  } else if( ch == "<" ) {
                        in_tag = false;
                        if( isEmptyTag("<"+tag+">") ) {
                              // do nothing
                        } else if( isOpeningTag("<"+tag+">") ) {
                              nesting_level--;
                        } else if( isClosingTag("<"+tag+">") ) {
                              nesting_level++;
                        }
                        if( nesting_level <= 0 ) {
                              // cut of everything after the element name:
                              uint elem_count = 0;
                              while( ! tag.at(elem_count).isSpace() && elem_count < tag.length() ) {
                                    elem_count++;
                              }
                              QString element = tag.left(elem_count);
                              return element;
                        }
                        // "else": empty tags can be ignored
                  } else if( in_tag == true && y != y_prev ) {
                        tag = ch + tag;         // we're moving left and changing rows
                        y_prev = y;
                  } else if( in_tag == true ) {
                        tag = ch + tag;         // we're moving left!
                  }
            }
            y--;
            col = kv.getDoc()->textLine(y).length();
      } while( y >= 0 );
      // TODO: limit line_count?

      return QString();
}

/** Return true if the tag is neither a closing tag
  * nor an empty tag, nor a comment, nor processing instruction.
  */
bool PluginKateXMLTools::isOpeningTag(QString tag)
{
      if( !isClosingTag(tag) && !isEmptyTag(tag) &&
            !tag.startsWith("<?") && !tag.startsWith("<!") ) {
            return true;
      } else {
            return false;
      }
}

/** Return true if the tag is a closing tag. Return false
  * if the tag is an opening tag or an empty tag (!)
  */
bool PluginKateXMLTools::isClosingTag(QString tag)
{
      if( tag.startsWith("</") ) {
            return true;
      } else {
            return false;
      }
}

bool PluginKateXMLTools::isEmptyTag(QString tag)
{
      if( tag.right(2) == "/>" ) {
            return true;
      } else {
            return false;
      }
}

/** Return true if ch is a single or double quote. Expects ch to be of length 1.
  */
bool PluginKateXMLTools::isQuote(QString ch)
{
      if( ! ch ) {
            return false;
      }
      assert(ch.length()==1);
      if( ch == "\"" || ch == "'" ) {
            return true;
      } else {
            return false;
      }
}


// ========================================================================
// Tools:

/** Sort a QStringList case-insensitively. Static. TODO: make it more simple. */
QStringList PluginKateXMLTools::sortQStringList(QStringList list) {
      // Sort list case-insensitive. This looks complicated but using a QMap
      // is even suggested by the Qt documentation.
      QMap<QString,QString> map_list;
      for ( QStringList::Iterator it = list.begin(); it != list.end(); ++it ) {
            QString str = *it;
            if( map_list.contains(str.lower()) ) {
                  // do not override a previous value, e.g. "Auml" and "auml" are two different
                  // entities, but they should be sorted next to each other.
                  // TODO: currently it's undefined if e.g. "A" or "a" comes first, it depends on
                  // the meta DTD (really? it seems to work okay?!?)
                  map_list[str.lower()+"_"] = str;
            } else {
                  map_list[str.lower()] = str;
            }
      }
      list.clear();
      QMap<QString,QString>::Iterator it;
      // Qt doc: "the items are alphabetically sorted [by key] when iterating over the map":
      for( it = map_list.begin(); it != map_list.end(); ++it ) {
            list.append(it.data());
      }
      return list;
}

// ========================================================================
// "Insert Element" dialog:

InsertElement::InsertElement(QWidget *parent, const char *name)
      :KDialogBase(parent, name, true, i18n("Insert XML Element"),
            KDialogBase::Ok|KDialogBase::Cancel)
{
}

InsertElement::~InsertElement()
{
}

void InsertElement::slotHistoryTextChanged(const QString& text)
{
      enableButtonOK(!text.isEmpty());
}

QString InsertElement::showDialog(QStringList &completions)
{
      QWidget *page = new QWidget(this);
      setMainWidget(page);
      QVBoxLayout *topLayout = new QVBoxLayout(page, 0, spacingHint());

      KHistoryCombo *combo = new KHistoryCombo(page, "value");
      combo->setHistoryItems(completions, true);
            connect(combo->lineEdit(), SIGNAL(textChanged ( const QString & )),this,SLOT( slotHistoryTextChanged(const QString &)));
      QString text = i18n("Enter XML tag name and attributes (\"<\", \">\" and closing tag will be supplied):");
      QLabel *label = new QLabel(text, page, "insert");

      topLayout->addWidget(label);
      topLayout->addWidget(combo);

      combo->setFocus();
      slotHistoryTextChanged(combo->lineEdit()->text());
      if( exec() ) {
            return combo->currentText();
      }
      return QString::null;
}

Generated by  Doxygen 1.6.0   Back to index