├── .gitignore ├── chat.log ├── main.cpp ├── main.qml ├── messagesmodel.cpp ├── messagesmodel.h ├── qml.qrc ├── textarea1.pro └── uiwatchdog.h /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | *.rc 35 | /.qmake.cache 36 | /.qmake.stash 37 | 38 | # qtcreator generated files 39 | *.pro.user* 40 | 41 | # xemacs temporary files 42 | *.flc 43 | 44 | # Vim temporary files 45 | .*.swp 46 | 47 | # Visual Studio generated files 48 | *.ib_pdb_index 49 | *.idb 50 | *.ilk 51 | *.pdb 52 | *.sln 53 | *.suo 54 | *.vcproj 55 | *vcproj.*.*.user 56 | *.ncb 57 | *.sdf 58 | *.opensdf 59 | *.vcxproj 60 | *vcxproj.* 61 | 62 | # MinGW generated files 63 | *.Debug 64 | *.Release 65 | 66 | # Python byte code 67 | *.pyc 68 | 69 | # Binaries 70 | # -------- 71 | *.dll 72 | *.exe 73 | 74 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "messagesmodel.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 11 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 12 | #endif 13 | QGuiApplication app(argc, argv); 14 | 15 | QQmlApplicationEngine engine; 16 | const QUrl url(QStringLiteral("qrc:/main.qml")); 17 | QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, 18 | &app, [url](QObject *obj, const QUrl &objUrl) { 19 | if (!obj && url == objUrl) 20 | QCoreApplication::exit(-1); 21 | }, Qt::QueuedConnection); 22 | 23 | QFile chatLogfile(":/chat.log"); 24 | if (!chatLogfile.open(QIODevice::ReadOnly)) { 25 | qFatal("%s", chatLogfile.errorString().toUtf8().data()); 26 | return 1; 27 | } 28 | 29 | // 1. Raw chat log 30 | const auto chatLog = chatLogfile.readAll(); 31 | engine.rootContext()->setContextProperty("chatLog", chatLog); 32 | 33 | // 2. Model with chat log lines 34 | const auto chatLinesArray = chatLog.split('\n'); 35 | QStringList chatLines; 36 | chatLines.reserve(chatLinesArray.size()); 37 | for (const auto &l : chatLinesArray) 38 | chatLines.append(l); 39 | 40 | MessagesModel chatLogModel; 41 | chatLogModel.setStringList(chatLines); 42 | engine.rootContext()->setContextProperty("chatLogModel", &chatLogModel); 43 | 44 | engine.load(url); 45 | 46 | return app.exec(); 47 | } 48 | -------------------------------------------------------------------------------- /main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Window 2.15 3 | import QtQuick.Controls 2.15 4 | 5 | ApplicationWindow { 6 | width: 200 7 | height: 400 8 | visible: true 9 | title: qsTr("Text is ez") 10 | 11 | menuBar: MenuBar { 12 | Menu { 13 | title: qsTr("Load") 14 | 15 | MenuItem { 16 | text: qsTr("Chat History") 17 | onTriggered: textArea.text = chatLog 18 | } 19 | } 20 | } 21 | 22 | // 1. TextArea 23 | // ScrollView { 24 | // id: view 25 | // anchors.fill: parent 26 | 27 | // TextArea { 28 | // id: textArea 29 | // textFormat: TextEdit.RichText 30 | // wrapMode: Text.Wrap 31 | // selectByKeyboard: true 32 | // selectByMouse: true 33 | // } 34 | // } 35 | 36 | // 2. ListView 37 | ListView { 38 | id: view 39 | anchors.fill: parent 40 | model: chatLogModel 41 | delegate: TextArea { 42 | id: delegateRoot 43 | padding: 0 44 | width: ListView.view.width 45 | textFormat: TextEdit.RichText 46 | wrapMode: Text.Wrap 47 | 48 | text: display 49 | 50 | Connections { 51 | target: selectionArea 52 | function onSelectionChanged() { 53 | updateSelection(); 54 | } 55 | } 56 | 57 | Component.onCompleted: updateSelection() 58 | 59 | function updateSelection() { 60 | if (index < selectionArea.selStartIndex || index > selectionArea.selEndIndex) 61 | delegateRoot.select(0, 0); 62 | else if (index > selectionArea.selStartIndex && index < selectionArea.selEndIndex) 63 | delegateRoot.selectAll(); 64 | else if (index === selectionArea.selStartIndex && index === selectionArea.selEndIndex) 65 | delegateRoot.select(selectionArea.selStartPos, selectionArea.selEndPos); 66 | else if (index === selectionArea.selStartIndex) 67 | delegateRoot.select(selectionArea.selStartPos, delegateRoot.length); 68 | else if (index === selectionArea.selEndIndex) 69 | delegateRoot.select(0, selectionArea.selEndPos); 70 | } 71 | } 72 | 73 | ScrollBar.vertical: ScrollBar { 74 | id: scrollBar 75 | policy: ScrollBar.AlwaysOn 76 | minimumSize: 0.1 77 | } 78 | 79 | function indexAtRelative(x, y) { 80 | return indexAt(x + contentX, y + contentY) 81 | } 82 | } 83 | 84 | MouseArea { 85 | id: selectionArea 86 | property int selStartIndex 87 | property int selEndIndex 88 | property int selStartPos 89 | property int selEndPos 90 | 91 | signal selectionChanged 92 | 93 | anchors.fill: parent 94 | enabled: !scrollBar.hovered 95 | cursorShape: enabled ? Qt.IBeamCursor : Qt.ArrowCursor 96 | 97 | function indexAndPos(x, y) { 98 | const index = view.indexAtRelative(x, y); 99 | if (index === -1) 100 | return; 101 | const item = view.itemAtIndex(index); 102 | const relItemY = item.y - view.contentY; 103 | const pos = item.positionAt(x, y - relItemY); 104 | 105 | return [index, pos]; 106 | } 107 | 108 | onPressed: { 109 | [selStartIndex, selStartPos] = indexAndPos(mouse.x, mouse.y); 110 | selectionChanged(); 111 | } 112 | 113 | onPositionChanged: { 114 | [selEndIndex, selEndPos] = indexAndPos(mouse.x, mouse.y); 115 | selectionChanged(); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /messagesmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "messagesmodel.h" 2 | -------------------------------------------------------------------------------- /messagesmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class MessagesModel : public QStringListModel 6 | { 7 | Q_OBJECT 8 | 9 | public: 10 | using QStringListModel::QStringListModel; 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | chat.log 5 | 6 | 7 | -------------------------------------------------------------------------------- /textarea1.pro: -------------------------------------------------------------------------------- 1 | QT += quick 2 | 3 | # You can make your code fail to compile if it uses deprecated APIs. 4 | # In order to do so, uncomment the following line. 5 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 6 | 7 | SOURCES += \ 8 | main.cpp \ 9 | messagesmodel.cpp 10 | 11 | RESOURCES += qml.qrc 12 | 13 | # Additional import path used to resolve QML modules in Qt Creator's code model 14 | QML_IMPORT_PATH = 15 | 16 | # Additional import path used to resolve QML modules just for Qt Quick Designer 17 | QML_DESIGNER_IMPORT_PATH = 18 | 19 | # Default rules for deployment. 20 | qnx: target.path = /tmp/$${TARGET}/bin 21 | else: unix:!android: target.path = /opt/$${TARGET}/bin 22 | !isEmpty(target.path): INSTALLS += target 23 | 24 | HEADERS += \ 25 | messagesmodel.h \ 26 | uiwatchdog.h 27 | -------------------------------------------------------------------------------- /uiwatchdog.h: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** MIT License 3 | ** 4 | ** Copyright (C) 2018-2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com 5 | ** Author: Sérgio Martins 6 | ** 7 | ** This file is part of KDToolBox (https://github.com/KDAB/KDToolBox). 8 | ** 9 | ** Permission is hereby granted, free of charge, to any person obtaining a copy 10 | ** of this software and associated documentation files (the "Software"), to deal 11 | ** in the Software without restriction, including without limitation the rights 12 | ** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | ** copies of the Software, ** and to permit persons to whom the Software is 14 | ** furnished to do so, subject to the following conditions: 15 | ** 16 | ** The above copyright notice and this permission notice (including the next paragraph) 17 | ** shall be included in all copies or substantial portions of the Software. 18 | ** 19 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | ** LIABILITY, WHETHER IN AN ACTION OF ** CONTRACT, TORT OR OTHERWISE, 24 | ** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | ** DEALINGS IN THE SOFTWARE. 26 | ****************************************************************************/ 27 | 28 | #ifndef UIWATCHDOG_H 29 | #define UIWATCHDOG_H 30 | 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | #ifdef Q_OS_WIN 39 | # include 40 | #endif 41 | 42 | #define MAX_TIME_BLOCKED 300 // ms 43 | 44 | Q_DECLARE_LOGGING_CATEGORY(uidelays) 45 | Q_LOGGING_CATEGORY(uidelays, "uidelays") 46 | 47 | class UiWatchdog; 48 | class UiWatchdogWorker : public QObject 49 | { 50 | public: 51 | enum Option { 52 | OptionNone = 0, 53 | OptionDebugBreak = 1 54 | }; 55 | typedef int Options; 56 | 57 | private: 58 | UiWatchdogWorker(Options options) 59 | : QObject() 60 | , m_watchTimer(new QTimer(this)) 61 | , m_options(options) 62 | { 63 | qCDebug(uidelays) << "UiWatchdogWorker created"; 64 | connect(m_watchTimer, &QTimer::timeout, this, &UiWatchdogWorker::checkUI); 65 | } 66 | 67 | ~UiWatchdogWorker() 68 | { 69 | qCDebug(uidelays) << "UiWatchdogWorker destroyed"; 70 | stop(); 71 | } 72 | 73 | void start(int frequency_msecs = 200) 74 | { 75 | m_watchTimer->start(frequency_msecs); 76 | m_elapsedTimeSinceLastBeat.start(); 77 | } 78 | 79 | void stop() 80 | { 81 | m_watchTimer->stop(); 82 | } 83 | 84 | void checkUI() 85 | { 86 | qint64 elapsed; 87 | 88 | { 89 | QMutexLocker l(&m_mutex); 90 | elapsed = m_elapsedTimeSinceLastBeat.elapsed(); 91 | } 92 | 93 | if (elapsed > MAX_TIME_BLOCKED) { 94 | qDebug() << "UI is blocked !" << elapsed; // Add custom action here! 95 | if ((m_options & OptionDebugBreak)) 96 | debugBreak(); 97 | } 98 | } 99 | 100 | void debugBreak() 101 | { 102 | #ifdef Q_OS_WIN 103 | DebugBreak(); 104 | #endif 105 | } 106 | 107 | void reset() 108 | { 109 | QMutexLocker l(&m_mutex); 110 | m_elapsedTimeSinceLastBeat.restart(); 111 | } 112 | 113 | QTimer *const m_watchTimer; 114 | QElapsedTimer m_elapsedTimeSinceLastBeat; 115 | QMutex m_mutex; 116 | const Options m_options; 117 | friend class UiWatchdog; 118 | }; 119 | 120 | class UiWatchdog : public QObject 121 | { 122 | public: 123 | 124 | explicit UiWatchdog(UiWatchdogWorker::Options options = UiWatchdogWorker::OptionNone, QObject *parent = nullptr) 125 | : QObject(parent) 126 | , m_uiTimer(new QTimer(this)) 127 | , m_options(options) 128 | { 129 | QLoggingCategory::setFilterRules("uidelays.debug=false"); 130 | qCDebug(uidelays) << "UiWatchdog created"; 131 | connect(m_uiTimer, &QTimer::timeout, this, &UiWatchdog::onUiBeat); 132 | } 133 | 134 | ~UiWatchdog() 135 | { 136 | stop(); 137 | qCDebug(uidelays) << "UiWatchdog destroyed"; 138 | } 139 | 140 | void start(int frequency_msecs = 100) 141 | { 142 | if (m_worker) 143 | return; 144 | 145 | m_uiTimer->start(frequency_msecs); 146 | 147 | m_worker = new UiWatchdogWorker(m_options); 148 | m_watchDogThread = new QThread(this); 149 | m_worker->moveToThread(m_watchDogThread); 150 | m_watchDogThread->start(); 151 | connect(m_watchDogThread, &QThread::started, m_worker, [this, frequency_msecs] { 152 | m_worker->start(frequency_msecs); 153 | }); 154 | } 155 | 156 | void stop() 157 | { 158 | if (!m_worker) 159 | return; 160 | 161 | m_uiTimer->stop(); 162 | m_worker->deleteLater(); 163 | m_watchDogThread->quit(); 164 | const bool didquit = m_watchDogThread->wait(2000); 165 | qCDebug(uidelays) << "watch thread quit?" << didquit; 166 | delete m_watchDogThread; 167 | 168 | m_watchDogThread = nullptr; 169 | m_worker = nullptr; 170 | } 171 | 172 | void onUiBeat() 173 | { 174 | m_worker->reset(); 175 | } 176 | 177 | private: 178 | QTimer *const m_uiTimer; 179 | QThread *m_watchDogThread = nullptr; 180 | UiWatchdogWorker *m_worker = nullptr; 181 | const UiWatchdogWorker::Options m_options; 182 | }; 183 | 184 | #endif 185 | --------------------------------------------------------------------------------