[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-pim
Subject:    Re: [Kde-pim] Towards better nested list handling in KMail
From:       Stephen Kelly <steveire () gmail ! com>
Date:       2008-03-26 2:26:14
Message-ID: fscc88$7tm$1 () ger ! gmane ! org
[Download RAW message or body]

Hi,

Thomas McGuire wrote:

> Hi Stephen,
> 
> On Sunday 16 March 2008, Stephen Kelly wrote:
>> This is another feature I've been working on for kjots.
>>
>> KMEditor supports lists currently, but not nested lists. I've been
>> experiementing with adding a DoTheRightThing nested list feature in kjots
>> and kmail.
> 
> I won't have time to look at this (and the link editor) until after the
> Akonadi meeting this weekend. But then, I'll have a torough look.
> 

I've made some progress on this and created a helper class to format the
list using an event filter. I still have a lot of optimization to do and
more events and cases to cover, but I think I can do everything I want now
with it.

Just thought I'd let you know, so need to spend time on a thorough look into
the last patch. My work-in-progress is attached. It applies against kjots,
but I can apply something similat to KMEditor when it's done.

I had a deeper look into the qt rich text classes too, but I haven't yet
tracked down how the link formatting/unformatting could work.

> It's great that you work on this and that we will be able to share code
> between KMail and KJots.
> 

Sure. I'd actually like to see a KRichTextEdit and KPart in kdelibs that all
could use. It might just mean moving most of KMEditor into it. As far as I
can see most of it is not specific to PIM/email and could be shared by many
applications. KMEditor could then just be a subclass of it?

Best regards,


Steve.



["listhelper.patch" (text/x-diff)]

Index: KJotsMain.cpp
===================================================================
--- KJotsMain.cpp	(revision 786203)
+++ KJotsMain.cpp	(working copy)
@@ -294,6 +294,28 @@
     action->setText(i18n("Font Size"));
     action->setIcon(KIcon("preferences-desktop-font"));
 
+    KSelectAction* selaction = \
actionCollection()->add<KSelectAction>("bullet_type"); +    \
selaction->setText(i18n("Bullet Type")); +    QStringList lst;
+    lst     << i18n("None")
+            << i18n("Disc")
+            << i18n("Circle")
+            << i18n("Square")
+            << i18n("123")
+            << i18n("abc")
+            << i18n("ABC");
+    selaction->setItems(lst);
+    selaction->setCurrentItem(0);
+    selaction->setIcon(KIcon("format-list-unordered"));
+
+    action = actionCollection()->addAction("indent_more");
+    action->setText(i18n("Increase Indent"));
+    action->setIcon(KIcon("format-indent-more"));
+
+    action = actionCollection()->addAction("indent_less");
+    action->setText(i18n("Decrease Indent"));
+    action->setIcon(KIcon("format-indent-less"));
+
     action = actionCollection()->addAction("auto_bullet");
     action->setText(i18n("Auto Bullets"));
     action->setIcon(KIcon("format-list-unordered"));
Index: kjotsedit.cpp
===================================================================
--- kjotsedit.cpp	(revision 785973)
+++ kjotsedit.cpp	(working copy)
@@ -27,6 +27,7 @@
 
 #include <QMimeData>
 #include <QTextCursor>
+#include <QTextList>
 #include <QStackedWidget>
 #include <QUrl>
 #include <QMenu>
@@ -35,11 +36,15 @@
 #include <kaction.h>
 #include <kfontaction.h>
 #include <kfontsizeaction.h>
+#include <kselectaction.h>
 #include <kactioncollection.h>
+#include <kfontdialog.h>
 #include <kcolordialog.h>
+#include <kdebug.h>
 
 #include "kjotsentry.h"
 #include "bookshelf.h"
+#include "nestedlisthelper.h"
 
 KJotsEdit::KJotsEdit ( QWidget *parent ) : KTextEdit(parent)
 {
@@ -82,6 +87,9 @@
     connect(actionCollection->action("linkify"), SIGNAL(triggered()), \
                SLOT(onLinkify()));
     connect(actionCollection->action("insert_checkmark"), SIGNAL(triggered()), \
SLOT(addCheckmark()));  
+    connect((KSelectAction*)actionCollection->action("bullet_type"), \
SIGNAL(triggered(int)), SLOT(onBulletType(int))); +    \
connect(actionCollection->action("indent_more"), SIGNAL(triggered()), \
SLOT(onIndentMore())); +    connect(actionCollection->action("indent_less"), \
                SIGNAL(triggered()), SLOT(onIndentLess()));
     connect(actionCollection->action("auto_bullet"), SIGNAL(triggered()), \
                SLOT(onAutoBullet()));
     connect(actionCollection->action("horizontal_rule"), SIGNAL(triggered()), \
SLOT(onRule()));  
@@ -92,6 +100,8 @@
     connect(this, SIGNAL(currentCharFormatChanged(const QTextCharFormat &)),
             this, SLOT(updateCurrentCharFormat(const QTextCharFormat &)));
 
+    installEventFilter(this);
+    nestedListHelper = new NestedListHelper(this);
 }
 
 void KJotsEdit::disableEditing ( void )
@@ -211,6 +221,61 @@
     mergeFormatOnWordOrSelection(fmt);
 }
 
+bool KJotsEdit::eventFilter(QObject *obj, QEvent *event)
+{
+    bool filter = false;
+    if ( (event->type() == QEvent::KeyPress)
+          || (event->type() == QEvent::Drop) ){
+        
+        if ( (event->type() == QEvent::KeyPress) && textCursor().currentList() )
+        {
+            QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
+            
+            bool handled = false;
+            
+            // False if the key press event was not handled or not completely
+            // handled by the Helper class.
+            handled = nestedListHelper->handleBeforeKeyPressEvent(keyEvent);
+
+            if (!handled){
+                keyPressEvent(keyEvent);
+            }
+            filter = true;
+        }
+        
+        if ( (event->type() == QEvent::KeyPress) && textCursor().currentList() )
+        {
+            QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
+            nestedListHelper->handleAfterKeyPressEvent(keyEvent);
+        }
+
+        
+//         if ( (event->type() == QEvent::Drop) )
+//         {
+//             QDropEvent *_dropEvent = static_cast<QDropEvent*>(event);
+//             dropEvent( _dropEvent );
+//             nestedListHelper->handleAfterDropEvent(_dropEvent);
+//         }
+    }
+    return filter;
+}
+
+void KJotsEdit::onBulletType(int styleIndex)
+{
+    nestedListHelper->handleOnBulletType(-styleIndex);
+    setFocus();
+}
+
+void KJotsEdit::onIndentMore()
+{
+    nestedListHelper->handleOnIndentMore();
+}
+
+void KJotsEdit::onIndentLess()
+{
+    nestedListHelper->handleOnIndentLess();
+}
+
 void KJotsEdit::updateCurrentCharFormat(const QTextCharFormat &format)
 {
     fontChanged(format.font());
Index: kjotsedit.h
===================================================================
--- kjotsedit.h	(revision 785973)
+++ kjotsedit.h	(working copy)
@@ -35,6 +35,8 @@
 class Bookshelf;
 class KJotsPage;
 
+class NestedListHelper;
+
 class KJotsEdit : public KTextEdit
 {
 Q_OBJECT
@@ -48,6 +50,7 @@
 
 protected:
     virtual void contextMenuEvent( QContextMenuEvent* );
+    bool eventFilter(QObject *obj, QEvent *event);
 
 protected slots:
     void onBookshelfSelection ( void );
@@ -61,6 +64,9 @@
     void onFontBackgroundColor ( void );
     void onFontFamilySelection( const QString& f);
     void onFontSizeSelection( int s);
+    void onBulletType(int styleIndex);
+    void onIndentMore();
+    void onIndentLess();
     void updateCurrentCharFormat(const QTextCharFormat &format);
     void fontChanged(const QFont &f);
     void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
@@ -75,6 +81,7 @@
     QPointer<Bookshelf> bookshelf;
     QPointer<KJotsPage> currentPage;
     KActionCollection *actionCollection;
+    NestedListHelper *nestedListHelper;
 };
 
 #endif // __KJOTSEDIT_H
Index: kjotsui.rc
===================================================================
--- kjotsui.rc	(revision 781504)
+++ kjotsui.rc	(working copy)
@@ -1,5 +1,5 @@
 <!DOCTYPE kpartgui>
-<kpartgui version="4" name="kjots" >
+<kpartgui version="5" name="kjots" >
  <MenuBar>
 
   <Menu name="file">
@@ -74,5 +74,8 @@
   <Separator/>
   <Action name="font_color"/>
   <Action name="font_background_color"/>
+  <Action name="bullet_type"/>
+  <Action name="indent_more"/>
+  <Action name="indent_less"/>
  </ToolBar>
 </kpartgui>
Index: nestedlisthelper.cpp
===================================================================
--- nestedlisthelper.cpp	(revision 0)
+++ nestedlisthelper.cpp	(revision 0)
@@ -0,0 +1,280 @@
+/**
+ * Nested list helper
+ *
+ * Copyright 2008  Stephen Kelly <steveire@gmailcom>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301  USA
+ */
+
+#include "nestedlisthelper.h"
+
+
+#include <QKeyEvent>
+#include <QDropEvent>
+#include <QTextCursor>
+#include <QTextList>
+#include <QTextDocumentFragment>
+
+#include <KTextEdit>
+#include <KDebug>
+
+
+NestedListHelper::NestedListHelper(QTextEdit *te)
+{
+    textEdit = te;
+    
+}
+
+NestedListHelper::~NestedListHelper()
+{
+}
+
+bool NestedListHelper::handleBeforeKeyPressEvent(QKeyEvent *event)
+{
+
+    // Only attempt to handle Backspace
+    if (event->key() != Qt::Key_Backspace)
+        return false;
+    
+    QTextCursor cursor = textEdit->textCursor();
+    bool handled = false;
+
+    if( !cursor.hasSelection()
+         && cursor.block().textList()
+         && event->key() == Qt::Key_Backspace
+         && cursor.atBlockStart() )
+    {
+         handleOnIndentLess();
+         handled = true;
+        
+    }
+
+    if ( cursor.hasSelection()
+          && cursor.block().textList()
+          && event->key() == Qt::Key_Backspace
+          && cursor.selection().toPlainText().contains("\n") )
+    {
+         handleOnIndentLess();
+         handled = true;
+        
+    }
+    return handled;
+}
+
+bool NestedListHelper::handleAfterKeyPressEvent(QKeyEvent *event)
+{
+    // Only attempt to handle Backspace and Return
+    if ((event->key() != Qt::Key_Backspace)
+         &&(event->key() != Qt::Key_Return))
+        return false;
+    
+    QTextCursor cursor = textEdit->textCursor();
+    bool handled = false;
+
+    if( !cursor.hasSelection() && cursor.block().textList() ) {
+        // Check if we're on the last list item.
+        // itemNumber is zero indexed
+        if (cursor.currentList()->count() == cursor.currentList()->itemNumber( \
cursor.block() ) + 1 ) +        {
+            if ( ( event->key() == Qt::Key_Return) || ( event->key() == \
Qt::Key_Backspace) ) +            {
+                reformatList();
+                handled = true;
+            }
+
+        }
+    }
+    return handled;
+}
+
+
+// bool NestedListHelper::handleAfterDropEvent(QDropEvent *dropEvent)
+// {
+//     // Do something smart here.
+// 
+//     kDebug() << "handle drop event";
+//     reformatList();
+//     return true;
+// }
+
+
+void NestedListHelper::reformatList()
+{
+    QTextCursor cursor = textEdit->textCursor();
+    if ( !cursor.currentList() ){
+        int initialPosition = cursor.position();
+        int initialAnchor = cursor.anchor();
+        int higherPositionIndex;
+        int lowerPositionIndex;
+        
+        if (initialPosition != initialAnchor){
+            if (initialPosition > initialAnchor){
+                lowerPositionIndex = initialAnchor;
+                higherPositionIndex = initialPosition;
+            }else{
+                lowerPositionIndex = initialPosition;
+                higherPositionIndex = initialAnchor;
+            }
+        }
+        else {
+            lowerPositionIndex = higherPositionIndex = initialPosition;
+        }
+
+        cursor.setPosition(lowerPositionIndex);
+        cursor.movePosition(QTextCursor::PreviousBlock);
+        if (cursor.currentList()){
+            textEdit->setTextCursor(cursor);
+            reformatList();
+        }
+
+        cursor.setPosition(higherPositionIndex);
+        cursor.movePosition(QTextCursor::NextBlock);
+        if (cursor.currentList()){
+            textEdit->setTextCursor(cursor);
+            reformatList();
+        }
+        cursor.setPosition(initialAnchor, QTextCursor::MoveAnchor);
+        cursor.setPosition(initialPosition, QTextCursor::KeepAnchor);
+        textEdit->setTextCursor(cursor);
+        return;
+    }
+
+    QTextBlock block = cursor.block();
+
+    // Start at the top of the list
+    while ( block.previous().textList() != 0 ){
+        block = block.previous();
+    }
+
+    QTextBlockFormat blockFmt = block.blockFormat();
+    blockFmt.setTopMargin(12);
+    blockFmt.setBottomMargin(0);
+    cursor.setPosition(block.position());
+    cursor.setBlockFormat(blockFmt);
+    block = cursor.block();
+
+    cursor.createList(cursor.currentList()->format());
+    QTextList* list = block.textList();
+    list->add(block);
+
+    QMap<int, QTextList *> nestedLists;
+    nestedLists.insert(list->format().indent(), list);
+
+    int indentLevel;
+    while ( block.next().textList() != 0 ) {
+        block = block.next();
+        list = block.textList();
+        indentLevel = list->format().indent();
+        
+        foreach (int key, nestedLists.keys() ){
+            if (indentLevel < key){
+                nestedLists.remove(key);
+            }
+        }
+        
+
+        if ( !nestedLists.contains(indentLevel) ) {
+            nestedLists.insert(indentLevel, list);
+        } else {
+            
+            list = nestedLists.take(indentLevel);
+        }
+
+        blockFmt = block.blockFormat();
+        blockFmt.setTopMargin(0);
+        blockFmt.setBottomMargin(0);
+        cursor.setPosition(block.position());
+        cursor.setBlockFormat(blockFmt);
+
+        list->add(block);
+        nestedLists.insert(indentLevel, list);
+
+    }
+    blockFmt = block.blockFormat();
+    blockFmt.setBottomMargin(12);
+    cursor.setPosition(block.position());
+    cursor.setBlockFormat(blockFmt);
+}
+
+
+void NestedListHelper::handleOnIndentMore()
+{
+    QTextCursor cursor = textEdit->textCursor();
+
+    QTextListFormat listFmt;
+    if (!cursor.currentList()) {
+        QTextListFormat::Style style = QTextListFormat::ListDisc;
+        handleOnBulletType(style);
+    } else {
+        listFmt = cursor.currentList()->format();
+        listFmt.setIndent(listFmt.indent() + 1);
+
+        cursor.createList(listFmt);
+        reformatList();
+    }
+}
+
+void NestedListHelper::handleOnIndentLess()
+{
+    QTextCursor cursor = textEdit->textCursor();
+    QTextList* currentList = cursor.currentList();
+    if ( !currentList )
+        return;
+    QTextListFormat listFmt;
+    listFmt = currentList->format();
+    if ( listFmt.indent() > 1 )
+    {
+        listFmt.setIndent(listFmt.indent() - 1);
+        cursor.createList(listFmt);
+    }else{
+        QTextBlockFormat bfmt;
+        bfmt.setObjectIndex(-1);
+        cursor.setBlockFormat(bfmt);
+    }
+    reformatList();
+}
+
+
+void NestedListHelper::handleOnBulletType(int styleIndex)
+{
+    QTextCursor cursor = textEdit->textCursor();
+    if (styleIndex != 0) {
+        QTextListFormat::Style style = (QTextListFormat::Style)styleIndex;
+        QTextList *currentList = cursor.currentList();
+        QTextListFormat listFmt;
+
+        cursor.beginEditBlock();
+
+        if (currentList){
+            listFmt = currentList->format();
+            listFmt.setStyle(style);
+            currentList->setFormat(listFmt);
+        }
+        else {
+            listFmt.setStyle(style);
+            cursor.createList(listFmt);
+        }
+
+        cursor.endEditBlock();
+    } else {
+        QTextBlockFormat bfmt;
+        bfmt.setObjectIndex(-1);
+        cursor.setBlockFormat(bfmt);
+    }
+    reformatList();
+}
+
+
Index: nestedlisthelper.h
===================================================================
--- nestedlisthelper.h	(revision 0)
+++ nestedlisthelper.h	(revision 0)
@@ -0,0 +1,113 @@
+/**
+ * Nested list helper
+ *
+ * Copyright 2008  Stephen Kelly <steveire@gmailcom>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301  USA
+ */
+
+#ifndef NESTEDLISTHELPER_H
+#define NESTEDLISTHELPER_H
+
+class QTextEdit;
+
+class QKeyEvent;
+class QDropEvent;
+
+/**
+ *
+ * @short Helper class for automatic handling of nested lists in a text edit
+ *
+ *
+ * @author Stephen Kelly
+ * @since 4.1
+ */
+class NestedListHelper
+{
+public:
+    
+    /**
+     * Create a helper
+     *
+     * @param te The text edit object to handle lists in.
+     */
+    NestedListHelper(QTextEdit *te);
+
+    /**
+     * Destructor
+     */
+    ~NestedListHelper();
+
+    /**
+     *
+     * Handles a key press before it is processed by the text edit widget.
+     *
+     * Currently this causes a backspace at the beginning of a line or with a
+     * multi-line selection to decrease the nesting level of the list.
+     *
+     * @param event The event to be handled
+     * @return Whether the event was handled by this method.
+     */
+    bool handleBeforeKeyPressEvent(QKeyEvent *event);
+
+    /**
+     *
+     * Handles a key press after it is processed by the text edit widget.
+     *
+     * Currently this causes a Return at the end of the last list item, or
+     * a Backspace after the last list item to recalculate the spacing
+     * between the list items.
+     *
+     * @param event The event to be handled
+     * @return Whether the event was handled by this method.
+     */
+    bool handleAfterKeyPressEvent(QKeyEvent *event);
+
+    
+//     bool handleAfterDropEvent(QDropEvent *event);
+
+    
+    /**
+     * Increases the indent (nesting level) on the current list item or selection.
+     */
+    void handleOnIndentMore();
+    
+
+    
+    /**
+     * Decreases the indent (nesting level) on the current list item or selection.
+     */
+    void handleOnIndentLess();
+
+    
+    
+    /**
+     * Changes the style of the current list or creates a new list with 
+     * the specified style.
+     *
+     * @param styleIndex The QTextListStyle of the list.
+     */
+    void handleOnBulletType(int styleIndex);
+
+    
+private:
+    void reformatList();
+    
+    QTextEdit *textEdit;
+
+};
+
+#endif
Index: CMakeLists.txt
===================================================================
--- CMakeLists.txt	(revision 781504)
+++ CMakeLists.txt	(working copy)
@@ -14,6 +14,7 @@
    kjotsedit.cpp
    kjotsbookmarks.cpp
    bookshelf.cpp
+   nestedlisthelper.cpp
    kjotsbrowser.cpp )
 
 kde4_add_ui_files(kjots_SRCS confpagemisc.ui )



_______________________________________________
KDE PIM mailing list kde-pim@kde.org
https://mail.kde.org/mailman/listinfo/kde-pim
KDE PIM home page at http://pim.kde.org/

[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic