jkj yuio
jkj yuio

Reputation: 2613

Qt QML How to manually select word in TextArea by double click

double click selects words, but i'd like to customise it to, for example, select the $1000 term.

Example:

import QtQuick 2.7
import QtQuick.Controls 1.4

ApplicationWindow
{
    visible: true
    width: 640
    height: 480

    TextArea
    {
        anchors.fill: parent
        text: "hello, try to select the $1000 prize!"
        font.pointSize: 14
    } 
}

For example, by somehow overriding selectWord or by overriding onDoubleClicked or by somehow adding my own MouseArea that doesn't break the existing TextArea functionality.

Not sure how to do this.

Thanks for any help.

update

I tried adding a MouseArea but it didnt work. Example;

ApplicationWindow
{
    visible: true
    width: 640
    height: 480

    TextArea
    {
        anchors.fill: parent
        text: "hello, try to select the $1000 prize!"
        font.pointSize: 14

        MouseArea
        {
            anchors.fill: parent
            propagateComposedEvents: true
            onClicked: 
            {
                // attempt to get an event on click without
                // affecting the TextArea. But it breaks selection.
                console.log("clicked some text")
                mouse.accepted = false
            }
        }
    }
}    

Update 2

I think this problem is a version of the long-running Qt problem that you can't have some kind of "event observer" whose job is to check events but not stop them from continuing their normal operation.

If i had an event observer, i could make it "observe" the TextArea and do something on click or double click.

So, here goes trying to make one....

toucharea.h

#pragma once

#include <QGuiApplication>
#include <QQuickItem>
#include <QTime>

class TouchArea : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged)

public:

    QTime _lastMousePress;
    int   _clickThresholdMS = 300;

    bool eventFilter(QObject*, QEvent *event) override
    {
        // if false this will allow the event to continue as normal
        // if true it will stop the event propagating
        bool handled = false;

        // https://doc.qt.io/qt-5/qevent.html#Type-enum
        QEvent::Type t = event->type();
        switch (t)
        {
        case QEvent::TouchUpdate:
            break;
        case QEvent::KeyPress:
        case QEvent::KeyRelease:
            {
                QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
                qDebug("key press %d", keyEvent->key());
            }
            break;
        case QEvent::MouseButtonPress:
            {
                qDebug() << "mouse press";
                _lastMousePress.start();
                break;
            }
        case QEvent::MouseButtonRelease:
            {
                qDebug() << "mouse release";
                int dt = _lastMousePress.elapsed();

                if (dt < _clickThresholdMS)
                {
                    qDebug() << "mouse click";
                    emit clicked();
                }
                break;
            }
        }

        return handled;
    }

    QQuickItem *target() const { return _target; }

    void setTarget(QQuickItem *target) 
    {
        qDebug() << "set target";
        if (_target == target) return;

        if (_target)
            _target->removeEventFilter(this);

        _target = target;

        if (_target)
            _target->installEventFilter(this);

        emit targetChanged();
    }

signals:

    void targetChanged();
    void clicked();

private:

    QQuickItem* _target = 0;
};

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <qqmlcontext.h>

#include "toucharea.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    qmlRegisterType<TouchArea>("App", 1, 0, "TouchArea");

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    return app.exec();
}

main.qml

import QtQuick 2.12
import QtQuick.Controls 1.4

import App 1.0

ApplicationWindow
{
    visible: true
    width: 640
    height: 480


    TextArea
    {
        id: tarea
        anchors.fill: parent
        text: "hello, try to select the $1000 prize!"
        font.pointSize: 14

        MouseArea
        {
            enabled: false
            anchors.fill: parent

            TouchArea
            {
                target: parent
                onClicked: console.log("captured a click")
            }
        }
    }
}    

Well it nearly worked. I can only capture my synthetic click if we handled the event in eventFilter. When not handled the filter does not see the MouseButtonRelease.

Why is this. I was expecting this to be the first handler encountered before the other QtQuick items get to see it.

Any help?

Upvotes: 1

Views: 2447

Answers (3)

Adversus
Adversus

Reputation: 2226

Looks like you can do this with TapHandler:

    import QtQuick 2.15
    import QtQuick.Controls 2.15

    TextField {
        width: parent.width

        selectByMouse: true

        TapHandler {
            grabPermissions: PointerHandler.TakeOverForbidden
            onDoubleTapped: (eventPoint) => {
                print("taptap");
                eventPoint.accepted = true;
            }
        }
    }

Note that I experienced both that the double-click = select string on TextField did and did not work after the above. Click+drag to select always worked though.

Upvotes: 1

jkj yuio
jkj yuio

Reputation: 2613

Figured out a way;

Step 1, define an Observer class for events

observer.h


#pragma once

#include <QGuiApplication>
#include <QQuickItem>
#include <QTime>
#include <QMouseEvent>

class Observer : public QQuickItem
{
    Q_OBJECT

public:

    QTime _lastMousePress;
    int   _clickThresholdMS = 300;

    Observer()
    {
        setFiltersChildMouseEvents(true);
    }

    bool childMouseEventFilter(QQuickItem*, QEvent *event) override
    {
        // if false this will allow the event to continue as normal
        // if true it will stop the event propagating
        bool handled = false;

        // https://doc.qt.io/qt-5/qevent.html#Type-enum
        QEvent::Type t = event->type();
        switch (t)
        {
        case QEvent::TouchUpdate:
            break;
        case QEvent::KeyPress:
        case QEvent::KeyRelease:
            {
                QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
                qDebug("key press %d", keyEvent->key());
            }
            break;
        case QEvent::MouseButtonPress:
            {
                //qDebug() << "mouse press";
                _lastMousePress.start();
            }
            break;
        case QEvent::MouseButtonRelease:
            {
                //qDebug() << "mouse release";
                int dt = _lastMousePress.elapsed();

                if (dt < _clickThresholdMS)
                {
                    //qDebug() << "mouse click";
                    emit clicked();
                }
            }
            break;
        case QEvent::MouseButtonDblClick:
            {
                //QMouseEvent* mevent = static_cast<QMouseEvent*>(event);
                //qDebug() << "mouse double click";
                emit doubleClicked();
                handled = true;
            }
            break;
        }

        return handled;
    }

signals:

    void clicked();
    void doubleClicked();

};

Step 2, put this in main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <qqmlcontext.h>

#include "observer.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    qmlRegisterType<Observer>("App", 1, 0, "Observer");

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    return app.exec();
}

Step 3, use Observer to detect event

Detect whichever event you want, then make it do what you need, for example, for double click to select a wider class of characters in TextArea;

import QtQuick 2.12
import QtQuick.Controls 1.4

import App 1.0

ApplicationWindow
{
    visible: true
    width: 640
    height: 480

    Observer
    {
        anchors.fill: parent

        onDoubleClicked:
        {
            tarea.selectWord();

            var s = tarea.selectionStart
            var e = tarea.selectionEnd

            function allowed(c)
            {
                if (c == "$" || c == "#") return true;
                if (c >= "0" && c <= "9") return true;
                if (c.toUpperCase() != c.toLowerCase()) return true;
                return false;
            }

            while (allowed(tarea.getText(s-1, s))) tarea.select(--s, e);
            while (allowed(tarea.getText(e, e+1))) tarea.select(s, ++e);
        }

        TextArea
        {
            id: tarea
            anchors.fill: parent
            text: "hello, try to select the #$$$1000###$foo prize!"
            font.pointSize: 14
        }
    }
}    

Upvotes: 1

Adrien Leravat
Adrien Leravat

Reputation: 2789

I cannot think of any solution that would preserve the TextArea behavior, just on the QML side. I would instead try to wrap, inherit or reimplement the component on the C++ side, to add what you want.

  • Inheriting may be tricky, as few Qt classes are meant to be inherited directly, looking at QQuickTextArea and QQuickTextEdit source, it might be challenging
  • Reimplementing can be some work, but that would probably be the option I would go for, while keeping the logic and code there to a minimum. QQuickTextArea seems a good base/reference as smaller/simpler than QQuickTextArea (which in turn uses TextArea internally)

Two key elements to that:

  • Events management (mouse, selection, ...)
  • Rendering of the component, based on Qt OpenGL wrapper classes (QSG* Scene graph classes). It is a bit obscure, but there are some tutorials and presentations out there about it. And you may be able to just copy-paste the original code.

For both, I would recommend digging into the source code:

Upvotes: 0

Related Questions