How to reposition cursor to the end of the document without autoscroll
-
Hello. I am creating my serial terminal application and I have noticed an issue with my cursor. My serial terminal application have 2 modes (autoscroll enabled or disabled). The code where I send data to console :
void Widget::readData() { while(serial_local->is_data_available()){ QByteArray line = serial_local->read_data(); QString DataAsString = QString(line); logging_local->Write_to_file(DataAsString); //CAPTURE INFO LOGS if(DataAsString.contains(info_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;32m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(0,128,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //CAPTURE WARNING LOGS else if(DataAsString.contains(warning_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;33m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(255,165,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //CAPTURE ERROR LOGS else if(DataAsString.contains(error_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;31m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(255,0,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //All other data with ANSI code is printed in black color else{ DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(Qt::black); //by default the text will be black ui->Console_read->insertPlainText(DataAsString); } } // if autoscroll checkbox is checked, make sure the cursor is always at the bottom and vertical scrollbar automatically keeps up with the logs. if(ui->autoscroll->isChecked()){ QScrollBar *bar = ui->Console_read->verticalScrollBar(); bar->setValue(bar->maximum()); } // if autoscroll is disabled, no need to keep up with the vertical scrollbar but ensure that when user clicks on serial console, the logs still print at the very bottom. else{ } }
When user clicks somewhere on my console (QTextEdit widget where the data is being written from the serial device), the cursor will change the position (position will be where the user clicked) and the way I print to the console will no longer be correct. See the example:
I want to log messages always at the bottom regardless whether user clicked on the screen or not. I can think of 2 options:
- Disable the mouse clicking for QTextEdit window so the user has no ability to modify cursor position.
- Everytime the data is about the be printed, reposition the cursor to the bottom of the QTextEdit window. That way, even if the user clicked somewhere on the screen, the cursor will be automatically repositioned at the end of the document.
I decided to try 2nd method I found some functions online to do that (https://www.qtcentre.org/threads/6396-Disable-QTextCursor-Mouse-click-repositioning).
QTextCursor cursor = ui->Console_read->textCursor(); cursor.clearSelection(); cursor.movePosition(QTextCursor::End); ui->Console_read->setTextCursor(cursor);
I added this code to the beggining of my readData function:
void Widget::readData() { // Everytime the data is about to be printed to console, ensure that the cursor is at the bottom of the document regardless of autoscroll enabled or disabled. QTextCursor cursor = ui->Console_read->textCursor(); cursor.clearSelection(); cursor.movePosition(QTextCursor::End); ui->Console_read->setTextCursor(cursor); while(serial_local->is_data_available()){ QByteArray line = serial_local->read_data(); QString DataAsString = QString(line); logging_local->Write_to_file(DataAsString); //CAPTURE INFO LOGS if(DataAsString.contains(info_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;32m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(0,128,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //CAPTURE WARNING LOGS else if(DataAsString.contains(warning_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;33m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(255,165,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //CAPTURE ERROR LOGS else if(DataAsString.contains(error_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;31m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(255,0,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //All other data with ANSI code is printed in black color else{ DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(Qt::black); //by default the text will be black ui->Console_read->insertPlainText(DataAsString); } } // if autoscroll checkbox is checked, make sure the cursor is always at the bottom and vertical scrollbar automatically keeps up with the logs. if(ui->autoscroll->isChecked()){ QScrollBar *bar = ui->Console_read->verticalScrollBar(); bar->setValue(bar->maximum()); } // if autoscroll is disabled, no need to keep up with the vertical scrollbar but ensure that when user clicks on serial console, the logs still print at the very bottom. else{ } }
The above method seems to work fine when autoscroll checkbox is enabled ( The vertical scrollbar is always at the bottom so the user can keep up with the latest logs at all times)
However, when autoscroll is disabled, the ui->Console_read->setTextCursor(cursor); cause the cursor to move to the end of the document and have the same effect as autoscroll (automatically goes to the bottom of the vertical scrollbar) which is something I do not want when autoscroll is disabled. I want to be able to print logs at the end of the document but I do not want to jump to the end of console when autoscroll is disabled.
-
Your logical flow:
Read serial data function: set cursor position in QTextEdit while serial data available? read serial data insert read data into QTextEdit at cursor position set above check if autoscroll enabled in QTextEdit? set Scrollbar to show end of QTextEdit else do nothing
you need to do the following:
Read serial data function: while serial data available? read serial data check if autoscroll enabled in QTextEdit? set Scrollbar to show end of QTextEdit else check if current cursor position is in the middle if so, set it to end of textedit else show scrollbar to current position (not end of textedit or maximum) insert read data into QTextEdit at cursor position set above
you have to arrange and include some feature as indicated above.
-
Hello, than you for your help. I rewritten my function a little bit:
void Widget::readData() { while(serial_local->is_data_available()){ if(ui->autoscroll->isChecked()){ QTextCursor cursor = ui->Console_read->textCursor(); cursor.clearSelection(); cursor.movePosition(QTextCursor::End); ui->Console_read->setTextCursor(cursor); QScrollBar *bar = ui->Console_read->verticalScrollBar(); bar->setValue(bar->maximum()); } else{ //check if current cursor position is in the middle if so, set it to end of textedit else show scrollbar to current position (not end of textedit or maximum) int column_number = ui->Console_read->textCursor().columnNumber(); qDebug("column number = %u \n",column_number); if(column_number != 0){ QTextCursor cursor = ui->Console_read->textCursor(); cursor.clearSelection(); cursor.movePosition(QTextCursor::End); ui->Console_read->setTextCursor(cursor); } else{ } } QByteArray line = serial_local->read_data(); QString DataAsString = QString(line); logging_local->Write_to_file(DataAsString); //CAPTURE INFO LOGS if(DataAsString.contains(info_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;32m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(0,128,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //CAPTURE WARNING LOGS else if(DataAsString.contains(warning_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;33m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(255,165,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //CAPTURE ERROR LOGS else if(DataAsString.contains(error_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;31m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(255,0,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //All other data with ANSI code is printed in black color else{ DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(Qt::black); //by default the text will be black ui->Console_read->insertPlainText(DataAsString); } } }
Read serial data function: while serial data available? read serial data check if autoscroll enabled in QTextEdit? set Scrollbar to show end of QTextEdit else check if current cursor position is in the middle if so, set it to end of textedit else show scrollbar to current position (not end of textedit or maximum) insert read data into QTextEdit at cursor position set above
According to your logic flow, if autoscroll is disabled, I should check if cursor position is in the middle (I check if column_number is not 0), the I move the cursor to the end, but that cause the scrollback to move together with the cursor.
As soon as I click somewhere with my mouse, this will be triggered:
if(column_number != 0){ QTextCursor cursor = ui->Console_read->textCursor(); cursor.clearSelection(); cursor.movePosition(QTextCursor::End); ui->Console_read->setTextCursor(cursor); }
and my scrollbar for some reason goes to the bottom which I do not want.. I do not understand what does cursor have to do with scrollbar (I only want the cursor to go to the end so it writes the latest logs at the bottom , but I want the scrollbar to maintain its position in the middle)
-
@lukutis222 said in How to reposition cursor to the end of the document without autoscroll:
I do not understand what does cursor have to do with scrollbar (I only want the cursor to go to the end so it writes the latest logs at the bottom , but I want the scrollbar to maintain its position in the middle)
I don't know, only guessing, but maybe the text edit automatically scrolls to where the cursor is? Hence scrolls to bottom when you append. In which case, save the current cursor or scrollbar position, move cursor to end and append new text, then restore previous cursor or scrollbar position?
-
the QTextEdit's QTextCursor will always move towards the current cursor position when the clicked event is fired.
if you need to have the append activity and also the manual cursor positioning to happen, you need to save the cursor position in the clicked event as JonB suggested and reposition the cursor once append is over. for this should be in Manual scroll mode (Not Autoscroll). Remember any time we can know where is the cursor position in QTextEdit.
-
@JonB @dan1973
Thank you both very much. I am getting very close to where I want with my program.Since autoscroll always automatically scrolls to cursor position, I no longer need
QScrollBar *bar = ui->Console_read->verticalScrollBar(); bar->setValue(bar->maximum());
My current function:
void Widget::readData() { static bool refresh_scrollbar = 0; QTextCursor cursor_backup; // this is for saving cursor backup while(serial_local->is_data_available()){ if(ui->autoscroll->isChecked()){ QTextCursor cursor = ui->Console_read->textCursor(); cursor.clearSelection(); cursor.movePosition(QTextCursor::End); ui->Console_read->setTextCursor(cursor); } else{ //check if current cursor position is in the middle if so, set it to end of textedit else show scrollbar to current position (not end of textedit or maximum) int column_number = ui->Console_read->textCursor().columnNumber(); qDebug("column number = %u \n",column_number); if(column_number != 0){ cursor_backup = ui->Console_read->textCursor(); QTextCursor cursor = ui->Console_read->textCursor(); cursor.clearSelection(); cursor.movePosition(QTextCursor::End); ui->Console_read->setTextCursor(cursor); refresh_scrollbar = 1; } else{ } } QByteArray line = serial_local->read_data(); QString DataAsString = QString(line); logging_local->Write_to_file(DataAsString); //CAPTURE INFO LOGS if(DataAsString.contains(info_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;32m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(0,128,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //CAPTURE WARNING LOGS else if(DataAsString.contains(warning_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;33m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(255,165,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //CAPTURE ERROR LOGS else if(DataAsString.contains(error_match)){ DataAsString.replace(QRegularExpression("\\033\\1330;31m"), ""); DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(QColor(255,0,0)); // 'custom' color ui->Console_read->insertPlainText(DataAsString); } //All other data with ANSI code is printed in black color else{ DataAsString.replace(QRegularExpression("\\033\\1330m"), ""); ui->Console_read->setTextColor(Qt::black); //by default the text will be black ui->Console_read->insertPlainText(DataAsString); } //reposition the cursor once the write is complete if(refresh_scrollbar == 1){ qDebug("refresh cursor to the old position \n"); ui->Console_read->setTextCursor(cursor_backup); refresh_scrollbar = 0; } } }
I have a flag refresh_scrollbar which tells me when I need to restore the cursor position. Whenever I place cursor somewhere else apart from the end, my program gets stuck in this loop:
if(column_number != 0){ cursor_backup = ui->Console_read->textCursor(); QTextCursor cursor = ui->Console_read->textCursor(); cursor.clearSelection(); cursor.movePosition(QTextCursor::End); ui->Console_read->setTextCursor(cursor); refresh_scrollbar = 1; }
It will move the cursor to the end temporarily, write all the logs at the end of the file and then it will reposition the cursor back to where the cursor was moved:
if(refresh_scrollbar == 1){ qDebug("refresh cursor to the old position \n"); ui->Console_read->setTextCursor(cursor_backup); refresh_scrollbar = 0; }
The only issue now is when I click somewhere in the middle,:
1 second later (after the next block of messages is received from serial device) the cursor shifts up :
and it will remain there forever because my cursor position is not at the end hence my program is stuck in the loop setting the cursor at the end, writing logs and then moving the cursor back to the position where the mouse was clicked. I am not sure whether that is supposed to be correct behaviour. Is it possible for the cursor not to shift up and remain in exact same position even though the new messages keep coming?
Due to this issue, it becomes very difficult if I want to lets say copy some logs and paste it in separate notepad. As soon as I position my cursor in the middle, it shifts all the logs up and does not allow me to highlight text that I want to copy
-
@lukutis222 said in How to reposition cursor to the end of the document without autoscroll:
readData()
"The only issue now is when I click somewhere in the middle,:
1 second later (after the next block of messages is received from serial device) the cursor shifts up :"
this happens bcz the appended data is pushing the cursor up and you also need to apply setCursor position again after append."and it will remain there forever because my cursor position is not at the end hence my program is stuck in the loop setting the cursor at the end, writing logs and then moving the cursor back to the position where the mouse was clicked." ........obviously bcz you remembered the cursor pos of the mouse click and set to that pos.
"Is it possible for the cursor not to shift up and remain in exact same position even though the new messages keep coming?"......Yes. set CursorPos after append.
check yourself, are you using readyRead() signal of QT Serial or you are synchronously using timer reading the serial data?
Also, what is the data rate you get serial info?
Write_to_file() might be taking more time....always write to file when not reading from Serial or no data available in serial, you can open file...write to it and close it.. in a span of us or ms. keep writing to file in a background thread having a buffer which keeps appending the received line(QByteArray) data.
dont combine readying and writing operations here, keep them separate.
Also remember cursor postion using clicked event of QTextEdit. -
This post is deleted!
-
I am using ready readRead signal to receive the data. There is no problems with reading the data. I read data every 1 second simply because the device that I am connected to prints every 1 second.
You mentioned:
this happens bcz the appended data is pushing the cursor up and you also need to apply setCursor position again after append.But that is exactly what I am doing here isnt?
if(refresh_scrollbar == 1){ qDebug("refresh cursor to the old position \n"); ui->Console_read->setTextCursor(cursor_backup); refresh_scrollbar = 0; }
After the data is appended, I check if refresh_scrollbar ==1 (it will be 1 if the cursor position is not at the bottom of the document), and then I set the cursor to the last saved position.
How can I then avoid my cursor to shift up when I placed my cursor in the middle and new data is received ?
Also, thank you for additional suggestions. I will try to implement them once I solve this issue.
So at the moment the logic is as following (when autoscroll is disabled):
Check if cursor is in the middle, if YES save the current cursor position using the function:
cursor_backup = ui->Console_read->textCursor();
After that, move the cursor to the end of the file to append the serial data, after appending serial data move back to the saved cursor position using the formula:
ui->Console_read->setTextCursor(cursor_backup); -
debug (i.e., qDebug()) on column_number after you ....
ui->Console_read->setTextCursor(cursor);
and after you append text
you need to know where is the cursor positioned after append. And also when you programmatically set the cursor to cursor_backup it should scroll to that position as well.
I suggest debug your code for two iterations, by keeping the data transfer rate to 10 sec. you will know the control flow here.
-
@dan1973 I have reflashed my remote device to send just 1 line of data every 1 second. It simply sending string: "HELLO FROM EXTERNAL DEVICE"
I simplified my ReadData function a little :
void Widget::readData() { static bool refresh_scrollbar = 0; QTextCursor cursor_backup; // this is for saving cursor backup while(serial_local->is_data_available()){ //if autoscroll is checked, just move the cursor to the end of the file which will automatically cause the autoscroll to happen if(ui->autoscroll->isChecked()){ QTextCursor cursor = ui->Console_read->textCursor(); cursor.clearSelection(); cursor.movePosition(QTextCursor::End); ui->Console_read->setTextCursor(cursor); } else{ int column_number = ui->Console_read->textCursor().columnNumber(); qDebug("column number initial = %u \n",column_number); // if cursor is in the middle, save the cursor backup, move the cursor to the end to append logs to the console and refresh the cursor to the "saved" position if(column_number != 0){ cursor_backup = ui->Console_read->textCursor(); QTextCursor cursor = ui->Console_read->textCursor(); cursor.clearSelection(); cursor.movePosition(QTextCursor::End); ui->Console_read->setTextCursor(cursor); int column_number2 = ui->Console_read->textCursor().columnNumber(); qDebug("column number after setTextCursor = %u \n",column_number2); refresh_scrollbar = 1; } else{ } } QByteArray line = serial_local->read_data(); QString DataAsString = QString(line); ui->Console_read->insertPlainText(DataAsString); //reposition the cursor once the write is complete if(refresh_scrollbar == 1){ qDebug("refresh cursor to the old position \n"); ui->Console_read->setTextCursor(cursor_backup); int column_number3 = ui->Console_read->textCursor().columnNumber(); qDebug("column number after append = %u \n",column_number3); refresh_scrollbar = 0; } } }
If I click somewhere in the middle :
The autoscroll will and cursor will automatically adjust and put this line (where I placed my cursor initially) at the top of the QTextEdit window.
My program is then stuck in the loop adjusting the cursor position:
column number initial = 30 column number after setTextCursor = 0 refresh cursor to the old position column number after append = 30 column number initial = 30 column number after setTextCursor = 0 refresh cursor to the old position column number after append = 30 column number initial = 30 column number after setTextCursor = 0 refresh cursor to the old position column number after append = 30 column number initial = 30 column number after setTextCursor = 0 refresh cursor to the old position column number after append = 30 column number initial = 30 column number after setTextCursor = 0 refresh cursor to the old position column number after append = 30 column number initial = 30 column number after setTextCursor = 0 refresh cursor to the old position column number after append = 30
I am not sure if my usage of function:
int column_number = ui->Console_read->textCursor().columnNumber();
is correct here
-
int column_number = ui->Console_read->textCursor().columnNumber(); // Get Col No (30) qDebug("column number initial = %u \n",column_number); // if cursor is in the middle, save the cursor backup, move the cursor to the end to append logs to the console and refresh the cursor to the "saved" position if(column_number != 0){ cursor_backup = ui->Console_read->textCursor(); // Get Cursor QTextCursor cursor = ui->Console_read->textCursor(); // Get Cursor cursor.clearSelection(); cursor.movePosition(QTextCursor::End); // Move to End ui->Console_read->setTextCursor(cursor); // set Cursor to End int column_number2 = ui->Console_read->textCursor().columnNumber(); // Get Col No (End pos)(0) qDebug("column number after setTextCursor = %u \n",column_number2); refresh_scrollbar = 1; --- ui->Console_read->insertPlainText(DataAsString); // Insert Text at End --- //reposition the cursor once the write is complete if(refresh_scrollbar == 1){ qDebug("refresh cursor to the old position \n"); ui->Console_read->setTextCursor(cursor_backup); // Position the cursor now to saved pos (30) int column_number3 = ui->Console_read->textCursor().columnNumber(); // Get Col no qDebug("column number after append = %u \n",column_number3); refresh_scrollbar = 0; }
your cursor is working properly. it is moved to end (0) .... appends data......and then is moved back to saved pos (30).
Now check with mouse click event inside the QTextEdit and log and see. -
@lukutis222 said in How to reposition cursor to the end of the document without autoscroll:
QTextEdit does not have mouse click event;
There is https://doc.qt.io/qt-6/qwidget.html#mousePressEvent
-
@jsulm I have read about the mouseclickevent and tried the following:
In my widget.h:
class Widget : public QWidget { Q_OBJECT public: Serial* serial_local; // this will be "local" instance of the Serial object that will point to the global Serial object. Logging* logging_local; TestTool* testtool_local; Widget(Serial* serial_ptr,Logging* logging_ptr,TestTool* testtool_ptr, QWidget *parent = nullptr); ~Widget(); void fillPortsParameters(); private slots: void on_Scan_button_clicked(); void on_Serial_connect_button_clicked(); void readData(); void on_write_box_returnPressed(); void on_Scenario_select_currentIndexChanged(int index); private: Ui::Widget *ui; QString error_match; QString info_match; QString warning_match; protected: void mousePressEvent(QMouseEvent * event); };
I have added:
protected: void mousePressEvent(QMouseEvent * event);
And then In my widget.cpp I have added:
void Widget::mousePressEvent(QMouseEvent *event) { qDebug() << "Pressed"; }
Now when I click somewhere on my application I get this message printed "Pressed", however, the mouseclickevent is only registered when I click on an empty space. For example if I click anywhere on the QTextEdit window, it wont register, if I click on button "scan devices" or "connect" it will also not register.
Do I need to register an event for each widget instead of just my main Widget class?
-
@lukutis222
Indeed, because the mouse event goes to the lower-level widget you click on.Look at installing an application event filter, or on your
Widget
, for a convenient way of intercepting all mouse events, regardless of targeted widget: https://doc.qt.io/qt-6/eventsandfilters.html, https://doc.qt.io/qt-6/qobject.html#eventFilter. Otherwsie you will have to subclass and overridemousePressEvent
for every one of your widgets. -
@dan1973
Yes I figured that but its not fully clear how to do it since I do not have a seperate class for QTextEdit. Can I do that in my Widget component?Im not sure how to override QTextEdit mousePressEvent in some component (such as widget.h and widget.cpp) which does not have anything to do with QTextEdit
-
Sample appln
.h file#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QLabel> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACE class Widget : public QWidget { Q_OBJECT public: Widget(QWidget *parent = nullptr); ~Widget(); bool bMPress; protected: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; private: Ui::Widget *ui; QLabel *lbl1; }; #endif // WIDGET_H
.cpp file
#include "widget.h" #include "ui_widget.h" #include <QDebug> #include <QCursor> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); lbl1 = new QLabel("new", this); lbl1->setGeometry(20, 20, 100, 30); bMPress = false; setMouseTracking(true); } Widget::~Widget() { delete ui; } void Widget::mousePressEvent(QMouseEvent *e) { bMPress = true; QString str1 = "\n X: " + QString::number(QCursor::pos().x()) + " Y: " + QString::number(QCursor::pos().x()); qDebug() << str1; } void Widget::mouseMoveEvent(QMouseEvent *e) { if(bMPress) { QString str1 = "\n X: " + QString::number(QCursor::pos().x()) + " Y: " + QString::number(QCursor::pos().x()); lbl1->setText(str1); this->repaint(); } } void Widget::mouseReleaseEvent(QMouseEvent *e) { bMPress = false; }