From 5e7eaa6187938853662e99bb86973c2493a7e716 Mon Sep 17 00:00:00 2001 From: Aurelien Regat-Barrel Date: Fri, 14 Jan 2022 13:45:52 +0100 Subject: [PATCH] Add simple Qt example --- CMakeLists.txt | 58 ++++--- CMakePresets.json | 42 +++++ build.sh | 11 +- launch-msvc.bat | 41 +++++ scripts/install_recent_cmake.sh | 16 ++ tools/CMakeLists.txt | 1 + tools/qt.demoapp/AsyncFileScanner.cpp | 104 +++++++++++++ tools/qt.demoapp/AsyncFileScanner.h | 34 +++++ tools/qt.demoapp/CMakeLists.txt | 23 +++ tools/qt.demoapp/MainWindow.cpp | 115 ++++++++++++++ tools/qt.demoapp/MainWindow.h | 39 +++++ tools/qt.demoapp/WidgetFileTextViewer.cpp | 31 ++++ tools/qt.demoapp/WidgetFileTextViewer.h | 12 ++ tools/qt.demoapp/WidgetFileTreeView.cpp | 144 ++++++++++++++++++ tools/qt.demoapp/WidgetFileTreeView.h | 28 ++++ .../images/icons/folder-closed-16.png | Bin 0 -> 206 bytes tools/qt.demoapp/images/images.qrc | 5 + tools/qt.demoapp/main.cpp | 17 +++ tools/qt.demoapp/pch.h | 48 ++++++ 19 files changed, 744 insertions(+), 25 deletions(-) create mode 100644 CMakePresets.json create mode 100644 launch-msvc.bat create mode 100755 scripts/install_recent_cmake.sh create mode 100644 tools/qt.demoapp/AsyncFileScanner.cpp create mode 100644 tools/qt.demoapp/AsyncFileScanner.h create mode 100644 tools/qt.demoapp/CMakeLists.txt create mode 100644 tools/qt.demoapp/MainWindow.cpp create mode 100644 tools/qt.demoapp/MainWindow.h create mode 100644 tools/qt.demoapp/WidgetFileTextViewer.cpp create mode 100644 tools/qt.demoapp/WidgetFileTextViewer.h create mode 100644 tools/qt.demoapp/WidgetFileTreeView.cpp create mode 100644 tools/qt.demoapp/WidgetFileTreeView.h create mode 100644 tools/qt.demoapp/images/icons/folder-closed-16.png create mode 100644 tools/qt.demoapp/images/images.qrc create mode 100644 tools/qt.demoapp/main.cpp create mode 100644 tools/qt.demoapp/pch.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b67b28b..5faccbd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,9 @@ -cmake_minimum_required(VERSION 3.12) +cmake_minimum_required(VERSION 3.20) +project(DebugVision) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) # may be enable later once we have a way to install them. #set(Boost_USE_STATIC_LIBS ON) @@ -10,26 +12,42 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) #find_package(GMock REQUIRED) #find_package(fmt REQUIRED) -option(BUILD_WITH_ASAN "Build with address sanitizer" ON) -option(BUILD_WITH_UBSAN "Build with undefined behaviour sanitizer" ON) +option(BUILD_WITH_ASAN "Build with address sanitizer" OFF) +option(BUILD_WITH_UBSAN "Build with undefined behaviour sanitizer" OFF) option(BUILD_WITH_TSAN "Build with address sanitizer" OFF) -add_compile_options(-Wall -Wextra -pedantic -Wmissing-include-dirs -Wformat=2 -Wunused -Wno-error=unused-variable -Wcast-align -Wno-vla -Wnull-dereference -Wmaybe-uninitialized) -add_compile_options(-Werror -fdiagnostics-color=auto) - -if(BUILD_WITH_ASAN) - add_compile_options(-fsanitize=address) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address") -endif(BUILD_WITH_ASAN) - -if(BUILD_WITH_UBSAN) - add_compile_options(-fsanitize=undefined) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined") -endif(BUILD_WITH_UBSAN) - -if(BUILD_WITH_TSAN) - add_compile_options(-fsanitize=thread) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=thread") -endif(BUILD_WITH_TSAN) +# Qt +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Concurrent REQUIRED) +message("QT_VERSION=${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}") + +if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + add_compile_options(/W4 /external:W3 /WX /permissive-) + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT /GL /Ox") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG") + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /LTCG") + if (QT_VERSION_MAJOR EQUAL 5) + # with Qt5, setting /external:W3 is not enough to get rid of all warnings + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd5054") + endif() +else() + add_compile_options(-Wall -Wextra -pedantic -Wmissing-include-dirs -Wformat=2 -Wunused -Wno-error=unused-variable -Wcast-align -Wno-vla -Wnull-dereference -Wmaybe-uninitialized) + add_compile_options(-Werror -fdiagnostics-color=auto) + + if(BUILD_WITH_ASAN) + add_compile_options(-fsanitize=address) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address") + endif(BUILD_WITH_ASAN) + + if(BUILD_WITH_UBSAN) + add_compile_options(-fsanitize=undefined) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined") + endif(BUILD_WITH_UBSAN) + + if(BUILD_WITH_TSAN) + add_compile_options(-fsanitize=thread) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=thread") + endif(BUILD_WITH_TSAN) +endif() add_subdirectory(tools) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..adf1f04 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,42 @@ +{ + "version": 2, + "configurePresets": [ + { + "name": "base-preset", + "hidden": true, + "binaryDir": "${sourceDir}/build/${presetName}", + "generator": "Ninja", + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/install" + } + }, + { + "name": "debug", + "inherits": "base-preset", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "release", + "inherits": "base-preset", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "visual-studio", + "inherits": "base-preset", + "generator": "Visual Studio 16 2019", + "cacheVariables": { + "CMAKE_CONFIGURATION_TYPES": "Debug;RelWithDebInfo" + } + } + ], + "buildPresets": [ + { + "name": "release", + "configurePreset": "release" + } + ] +} \ No newline at end of file diff --git a/build.sh b/build.sh index a34a4fc..65ed8dd 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,7 @@ -rm -rf build/release -mkdir -p build/release -cd build/release -cmake -G Ninja ../.. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=install -ninja +#/bin/bash +set -eu +rm -rf build/release +cmake --preset="release" +cmake --build --preset="release" +cmake --build --preset="release" --target install diff --git a/launch-msvc.bat b/launch-msvc.bat new file mode 100644 index 0000000..35864dc --- /dev/null +++ b/launch-msvc.bat @@ -0,0 +1,41 @@ +@echo off +REM script to be used on Windows to generate and open a VC++ solution + +REM where to put the build files +set BUILDDIR=%~dp0build + +REM the VC++ solution file +set SLNFILE=%BUILDDIR%\visual-studio\DebugVision.sln + +REM make Qt visible +set QTDIR=C:\Qt\6.2.2\msvc2019_64 +if not exist %QTDIR% (echo invalid Qt bin path: "%QTDIR%" && GOTO:FAILURE) +set PATH=%QTDIR%\bin;%PATH% + +REM find VC++ +set "VCPATH=C:\Program Files (x86)\Microsoft Visual Studio\2019\Community" +if not exist "%VCPATH%" ( + set "VCPATH=C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional" +) + +REM generate build files if not already done +if not exist %SLNFILE% ( + REM make VC++ & cmake visible + REM make sure to install "C++ CMake tools for Windows" in VS installer + call "%VCPATH%\VC\Auxiliary\Build\vcvars64.bat" || GOTO:FAILURE + + if exist %BUILDDIR% rmdir /s/q %BUILDDIR% + mkdir %BUILDDIR% || GOTO:FAILURE + compact /c /q %BUILDDIR% > nul + cmake --preset="visual-studio" || GOTO:FAILURE +) + +REM open the VC++ solution +start %SLNFILE% + +REM success! +GOTO:EOF + +:FAILURE +echo Failure! +pause \ No newline at end of file diff --git a/scripts/install_recent_cmake.sh b/scripts/install_recent_cmake.sh new file mode 100755 index 0000000..32c3d67 --- /dev/null +++ b/scripts/install_recent_cmake.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# script to install a recent cmake version in CI build env + +set -eu + +CMAKEVER=3.22.1 +curl -sL https://github.com/Kitware/CMake/releases/download/v${CMAKEVER}/cmake-${CMAKEVER}-linux-x86_64.tar.gz -o cmake.tar.gz +tar xzf cmake.tar.gz +ls -l +mv cmake-${CMAKEVER}-linux-x86_64 /opt/cmake +ln -s /opt/cmake/bin/cmake /usr/bin/cmake +ln -s /opt/cmake/bin/ctest /usr/bin/ctest +ln -s /opt/cmake/bin/cpack /usr/bin/cpack +rm cmake.tar.gz + +cmake -version diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index d03dc69..d1afe00 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(debug.vision.application) +add_subdirectory(qt.demoapp) diff --git a/tools/qt.demoapp/AsyncFileScanner.cpp b/tools/qt.demoapp/AsyncFileScanner.cpp new file mode 100644 index 0000000..b579b47 --- /dev/null +++ b/tools/qt.demoapp/AsyncFileScanner.cpp @@ -0,0 +1,104 @@ +#include "AsyncFileScanner.h" + +#include +#include + +AsyncFileScanner::AsyncFileScanner() +{ + qDebug() << __func__; + connect(&m_futureWatcher, &QFutureWatcher::started, this, &AsyncFileScanner::scanStarted); + connect(&m_futureWatcher, &QFutureWatcher::finished, this, &AsyncFileScanner::scanFinished); +} + +AsyncFileScanner::~AsyncFileScanner() +{ + qDebug() << __func__; + stop(); +} + +void AsyncFileScanner::startFolderScan(const QString& rootDir) +{ + qDebug() << __func__; + if (isScanInProgress()) + { + qWarning() << "can't start a new file scanning: another scan is already in progress"; + return; + } + + m_future = QtConcurrent::run([this, rootDir] { + qDebug() << "async file scanner started"; + const QStringList files = performFileSearchStage(rootDir, {"*.log", "log*.txt"}); + if (!m_future.isCanceled()) + { + performFileReadStage(files); + } + qDebug() << "async file scanner ended"; + }); + m_futureWatcher.setFuture(m_future); +} + +void AsyncFileScanner::stop() +{ + qDebug() << __func__; + if (m_future.isRunning()) + { + qDebug() << "stopping async file scanner..."; + m_future.cancel(); + m_future.waitForFinished(); + } +} + +QStringList AsyncFileScanner::performFileSearchStage(const QString& rootDir, const QStringList& patterns) +{ + Q_ASSERT(m_future.isRunning()); // we expect to run as an async task + + qDebug() << "searching for files of kind" << patterns; + QStringList result; + result.reserve(1'000); + + QDirIterator it(rootDir, patterns, QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) + { + if (m_future.isCanceled()) + { + qDebug() << "file search was canceled"; + break; + } + + QString filePath = it.next(); + result += filePath; + emit foundNewFile(rootDir, filePath); + } + + qDebug() << "end of file search, number of file found:" << result.size(); + return result; +} + +static int countTextLinesInFile(const QString& filePath) +{ + QFile file{filePath}; + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "failed to read file" << filePath; + return 0; + } + + QTextStream stream(&file); + const QString text = stream.readAll(); + return text.isEmpty() ? 0 : 1 + text.count(QLatin1Char('\n')); +} + +void AsyncFileScanner::performFileReadStage(const QStringList& files) +{ + qDebug() << "starting to read file content"; + for (const QString& filePath : files) + { + if (m_future.isCanceled()) + { + qDebug() << "reading file content was canceled"; + break; + } + + emit countedFileLineNumbers(filePath, countTextLinesInFile(filePath)); + } +} diff --git a/tools/qt.demoapp/AsyncFileScanner.h b/tools/qt.demoapp/AsyncFileScanner.h new file mode 100644 index 0000000..609a516 --- /dev/null +++ b/tools/qt.demoapp/AsyncFileScanner.h @@ -0,0 +1,34 @@ +#pragma once + +// Type used to scan folders for log files via a background task +class AsyncFileScanner : public QObject +{ + Q_OBJECT +signals: + void scanStarted(); + void scanFinished(); + void foundNewFile(QString rootDir, QString filePath); + void countedFileLineNumbers(QString filePath, int lineCount); + +public: + AsyncFileScanner(); + ~AsyncFileScanner(); + + [[nodiscard]] bool isScanInProgress() const + { + return m_future.isRunning(); + } + + void startFolderScan(const QString& rootDir); + + void stop(); + +private: + QStringList performFileSearchStage(const QString& rootDir, const QStringList& patterns); + void performFileReadStage(const QStringList& files); + +private: + bool m_isScanning = false; + QFuture m_future; + QFutureWatcher m_futureWatcher; +}; diff --git a/tools/qt.demoapp/CMakeLists.txt b/tools/qt.demoapp/CMakeLists.txt new file mode 100644 index 0000000..b10066d --- /dev/null +++ b/tools/qt.demoapp/CMakeLists.txt @@ -0,0 +1,23 @@ + +add_executable(qt.demoapp + AsyncFileScanner.cpp + AsyncFileScanner.h + main.cpp + MainWindow.cpp + MainWindow.h + WidgetFileTextViewer.cpp + WidgetFileTextViewer.h + WidgetFileTreeView.cpp + WidgetFileTreeView.h + # special files + pch.h + images/images.qrc +) +target_precompile_headers(qt.demoapp PRIVATE pch.h) +target_link_libraries(qt.demoapp PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Concurrent) +set_target_properties(qt.demoapp PROPERTIES AUTOMOC ON AUTORCC ON) + +if (WIN32) + # display the Win32 console only for debug builds + set_target_properties(qt.demoapp PROPERTIES WIN32_EXECUTABLE $,FALSE,TRUE>) +endif() diff --git a/tools/qt.demoapp/MainWindow.cpp b/tools/qt.demoapp/MainWindow.cpp new file mode 100644 index 0000000..5adf19e --- /dev/null +++ b/tools/qt.demoapp/MainWindow.cpp @@ -0,0 +1,115 @@ +#include "MainWindow.h" + +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow() +{ + qDebug() << __func__; + createMenus(); + createStatusBar(); + + setWindowTitle(QString("%1 (%2)").arg(qApp->applicationName(), qApp->applicationVersion())); + + auto splitter = new QSplitter(this); + splitter->addWidget(&m_logFileList); + splitter->addWidget(&m_logTextViewer); + splitter->setSizes(QList() << 300 << 300); + + setCentralWidget(splitter); + + connectSignals(); +} + +void MainWindow::clear() +{ + m_logFileList.clear(); + m_logTextViewer.clear(); +} + +void MainWindow::createMenus() +{ + auto fileMenu = menuBar()->addMenu(tr("&File")); + { + m_actionOpenFolder = new QAction(tr("Open folder..."), this); + m_actionOpenFolder->setStatusTip(tr("Search for log files in a selected folder.")); + m_actionOpenFolder->setShortcuts(QKeySequence::Open); + connect(m_actionOpenFolder, &QAction::triggered, this, &MainWindow::onOpenFolder); + fileMenu->addAction(m_actionOpenFolder); + } + fileMenu->addSeparator(); + { + m_actionCloseAll = new QAction(tr("Clear all"), this); + m_actionCloseAll->setStatusTip(tr("Clear all the elements.")); + connect(m_actionCloseAll, &QAction::triggered, this, &MainWindow::clear); + fileMenu->addAction(m_actionCloseAll); + } + fileMenu->addSeparator(); + { + auto action = new QAction(tr("Exit"), this); + action->setStatusTip(tr("Exit the application")); + action->setShortcuts(QKeySequence::Quit); + connect(action, &QAction::triggered, this, &MainWindow::close); + fileMenu->addAction(action); + } +} + +void MainWindow::createStatusBar() +{ + m_progressBar = new QProgressBar(statusBar()); + m_progressBar->setAlignment(Qt::AlignRight); + m_progressBar->setTextVisible(false); + m_progressBar->setMaximumWidth(200); + statusBar()->addPermanentWidget(m_progressBar); +} + +void MainWindow::connectSignals() +{ + // sync UI components with the status of the async file scanner + connect(&m_fileScanner, &AsyncFileScanner::scanStarted, this, [this] { + m_actionOpenFolder->setEnabled(false); + m_actionCloseAll->setEnabled(false); + m_progressBar->setRange(0, 0); + }); + connect(&m_fileScanner, &AsyncFileScanner::scanFinished, this, [this] { + m_actionOpenFolder->setEnabled(true); + m_actionCloseAll->setEnabled(true); + m_progressBar->setRange(0, 1); + }); + // handle async file scanner results + connect(&m_fileScanner, &AsyncFileScanner::foundNewFile, &m_logFileList, &WidgetFileTreeView::addFile); + connect(&m_fileScanner, &AsyncFileScanner::countedFileLineNumbers, &m_logFileList, &WidgetFileTreeView::setFileLineCount); + // sync the tree view with the text editor + connect(&m_logFileList, &WidgetFileTreeView::highlightedFileChanged, &m_logTextViewer, &WidgetFileTextViewer::displayFileContent); +} + +void MainWindow::onOpenFolder() +{ + if (m_fileScanner.isScanInProgress()) + { + qWarning() << "can't open a new folder while file scanning is in progress"; + return; + } + + const QString path = QFileDialog::getExistingDirectory( + this, + tr("Open folder"), + "", + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (!path.isEmpty()) + { + m_logFileList.addRootFolder(path); + m_fileScanner.startFolderScan(path); + } +} + +void MainWindow::closeEvent(QCloseEvent* event) +{ + m_fileScanner.stop(); + event->accept(); +} diff --git a/tools/qt.demoapp/MainWindow.h b/tools/qt.demoapp/MainWindow.h new file mode 100644 index 0000000..34689a0 --- /dev/null +++ b/tools/qt.demoapp/MainWindow.h @@ -0,0 +1,39 @@ +#pragma once + +#include "AsyncFileScanner.h" +#include "WidgetFileTextViewer.h" +#include "WidgetFileTreeView.h" + +#include + +class QAction; +class QProgressBar; + +// Main window of the application +class MainWindow : public QMainWindow +{ + Q_OBJECT +public: + MainWindow(); + + void clear(); + +private: + void createMenus(); + void createStatusBar(); + void connectSignals(); + void onOpenFolder(); + +private: + void closeEvent(QCloseEvent* event) final; + +private: + // UI elements + QAction* m_actionOpenFolder = nullptr; + QAction* m_actionCloseAll = nullptr; + QProgressBar* m_progressBar = nullptr; + WidgetFileTreeView m_logFileList; + WidgetFileTextViewer m_logTextViewer; + // non UI elements + AsyncFileScanner m_fileScanner; +}; diff --git a/tools/qt.demoapp/WidgetFileTextViewer.cpp b/tools/qt.demoapp/WidgetFileTextViewer.cpp new file mode 100644 index 0000000..4a4461b --- /dev/null +++ b/tools/qt.demoapp/WidgetFileTextViewer.cpp @@ -0,0 +1,31 @@ +#include "WidgetFileTextViewer.h" + +static QString readFileContent(const QString& filePath) +{ + QFile file{filePath}; + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "failed to read file" << filePath; + return ""; + } + + QTextStream stream(&file); + return stream.readAll(); +} + +WidgetFileTextViewer::WidgetFileTextViewer(QWidget* parent) : + QTextEdit(parent) +{ + setReadOnly(true); + setAcceptRichText(false); + setLineWrapMode(QTextEdit::NoWrap); + setFontFamily("Courier New"); + setFontPointSize(10); +} + +void WidgetFileTextViewer::displayFileContent(const QString& filePath) +{ + qDebug() << "displaying content of file" << filePath; + clear(); + setPlainText(readFileContent(filePath)); +} diff --git a/tools/qt.demoapp/WidgetFileTextViewer.h b/tools/qt.demoapp/WidgetFileTextViewer.h new file mode 100644 index 0000000..946dbe7 --- /dev/null +++ b/tools/qt.demoapp/WidgetFileTextViewer.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +// Widget responsible of displaying the content of a log file +class WidgetFileTextViewer : public QTextEdit +{ +public: + WidgetFileTextViewer(QWidget* parent = nullptr); + + void displayFileContent(const QString& filePath); +}; diff --git a/tools/qt.demoapp/WidgetFileTreeView.cpp b/tools/qt.demoapp/WidgetFileTreeView.cpp new file mode 100644 index 0000000..e2b9d16 --- /dev/null +++ b/tools/qt.demoapp/WidgetFileTreeView.cpp @@ -0,0 +1,144 @@ +#include "WidgetFileTreeView.h" + +#include + +// role used to store the absolute file path of the file associated to an item (if any) +constexpr int CustomRoleAbsFilePath = Qt::UserRole; + +WidgetFileTreeView::WidgetFileTreeView(QWidget* parent) : + QTreeWidget(parent) +{ + setRootIsDecorated(false); + setHeaderLabels({tr("File path"), tr("Lines")}); + connect(this, &QTreeWidget::itemDoubleClicked, this, &WidgetFileTreeView::onItemDoubleClicked); +} + +void WidgetFileTreeView::clear() +{ + QTreeWidget::clear(); + m_rootFolderItems.clear(); + m_filePathItems.clear(); + m_highlightedFilePath.reset(); +} + +void WidgetFileTreeView::addRootFolder(const QString& rootPath) +{ + ensureRootFolderItemExists(rootPath); +} + +std::optional itemColorForFileName(const QString& fileName) +{ + if (fileName.contains("error", Qt::CaseInsensitive)) + { + return QColor(255, 219, 231); // very ligh red + } + return {}; +} + +void WidgetFileTreeView::addFile(const QString& rootPath, const QString& filePath) +{ + const QFileInfo finfo{filePath}; + Q_ASSERT(finfo.isFile()); + const QString absPath = finfo.absoluteFilePath(); + + if (auto it = m_filePathItems.find(absPath); it == m_filePathItems.end()) + { + const QDir rootDir(rootPath); + const QString relPath = rootDir.relativeFilePath(absPath); + + QTreeWidgetItem* rootItem = ensureRootFolderItemExists(rootPath); + auto item = new QTreeWidgetItem(rootItem, QStringList() << relPath); + m_filePathItems[absPath] = item; + item->setData(0, CustomRoleAbsFilePath, absPath); + item->setToolTip(0, absPath); + + // adjust color if needed + if (auto color = itemColorForFileName(finfo.fileName())) + { + item->setBackground(0, *color); + item->setBackground(1, *color); + } + } + else + { + qDebug() << "not adding file to the tree because it already exists"; + } +} + +// simple formating helper for (not so) big numbers +static QString formatLineCount(int count) +{ + if (count > 999 && count < 1'000'000) + { + return QString("%1,%2").arg(count / 1'000).arg(count % 1'000, 3, 10, QLatin1Char('0')); + } + return QString::number(count); +} + +void WidgetFileTreeView::setFileLineCount(const QString& filePath, int lineCount) +{ + const QFileInfo finfo{filePath}; + Q_ASSERT(finfo.isFile()); + const QString absPath = finfo.absoluteFilePath(); + + if (auto it = m_filePathItems.find(absPath); it != m_filePathItems.end()) + { + QTreeWidgetItem* item = it->second; + item->setText(1, formatLineCount(lineCount)); + } + else + { + qCritical() << "can't update file line count because it does not exist in the tree:" << filePath; + } +} + +QTreeWidgetItem* WidgetFileTreeView::ensureRootFolderItemExists(const QString& rootPath) +{ + const QFileInfo finfo{rootPath}; + Q_ASSERT(finfo.isDir()); + + const QString absPath = finfo.absoluteFilePath(); + if (auto it = m_rootFolderItems.find(absPath); it != m_rootFolderItems.end()) + { + // already created + return it->second; + } + + // not created yet: do it + auto item = new QTreeWidgetItem(this, QStringList() << finfo.fileName()); + m_rootFolderItems[absPath] = item; + item->setToolTip(0, absPath); + item->setIcon(0, QPixmap(":/images/icons/folder-closed-16.png")); + item->setExpanded(true); + return item; +} + +void WidgetFileTreeView::onItemDoubleClicked(QTreeWidgetItem* item, int column) +{ + Q_UNUSED(column); + + // get the file path associated to this item, if applicable + const QString filePath = item->data(0, CustomRoleAbsFilePath).toString(); + if (filePath.isEmpty()) + { + return; + } + + QFont font = item->font(0); + + // clear the currently highlighted item, if any + if (m_highlightedFilePath) + { + QTreeWidgetItem* highlightItem = m_filePathItems[*m_highlightedFilePath]; + Q_ASSERT(highlightItem != nullptr); + font.setBold(false); + highlightItem->setFont(0, font); + } + + // highlight the new item + m_highlightedFilePath = filePath; + font.setBold(true); + item->setFont(0, font); + + emit highlightedFileChanged(filePath); +} diff --git a/tools/qt.demoapp/WidgetFileTreeView.h b/tools/qt.demoapp/WidgetFileTreeView.h new file mode 100644 index 0000000..a74381b --- /dev/null +++ b/tools/qt.demoapp/WidgetFileTreeView.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +// Widget responsible of displaying the list of available log files +class WidgetFileTreeView : public QTreeWidget +{ + Q_OBJECT +signals: + void highlightedFileChanged(QString filePath); + +public: + WidgetFileTreeView(QWidget* parent = nullptr); + + void clear(); + void addRootFolder(const QString& rootPath); + void addFile(const QString& rootPath, const QString& filePath); + void setFileLineCount(const QString& filePath, int lineCount); + +private: + QTreeWidgetItem* ensureRootFolderItemExists(const QString& rootPath); + void onItemDoubleClicked(QTreeWidgetItem* item, int column); + +private: + std::unordered_map m_rootFolderItems; + std::unordered_map m_filePathItems; + std::optional m_highlightedFilePath; +}; diff --git a/tools/qt.demoapp/images/icons/folder-closed-16.png b/tools/qt.demoapp/images/icons/folder-closed-16.png new file mode 100644 index 0000000000000000000000000000000000000000..9decc00ba7973b4482bf2d4f32feee2a647f9e79 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9F5M?jcysy3fAP_V(% z#WBR9_w6}FzC!^5t{0hqaHH>k$#@Gxwf}%P2j%sP2)m;m?NI$<{6#PP_>dEPR!apCNo~W;56Ec}%%{ za4Pf3hhgd|L|w3`i%y`ce{-4EdaWO!PC{xWt~$(69A^W BOYQ&w literal 0 HcmV?d00001 diff --git a/tools/qt.demoapp/images/images.qrc b/tools/qt.demoapp/images/images.qrc new file mode 100644 index 0000000..c618aea --- /dev/null +++ b/tools/qt.demoapp/images/images.qrc @@ -0,0 +1,5 @@ + + + icons/folder-closed-16.png + + diff --git a/tools/qt.demoapp/main.cpp b/tools/qt.demoapp/main.cpp new file mode 100644 index 0000000..7f53388 --- /dev/null +++ b/tools/qt.demoapp/main.cpp @@ -0,0 +1,17 @@ +#include "MainWindow.h" + +#include + +int main(int argc, char* argv[]) +{ + QApplication a(argc, argv); + + QCoreApplication::setApplicationName("DebugVision"); + QCoreApplication::setApplicationVersion("0.1-qtdemo"); + + MainWindow w; + w.resize(800, 600); + w.show(); + + return a.exec(); +} diff --git a/tools/qt.demoapp/pch.h b/tools/qt.demoapp/pch.h new file mode 100644 index 0000000..16f7e3e --- /dev/null +++ b/tools/qt.demoapp/pch.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +#include + +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) +namespace std { +template <> +struct hash +{ + std::size_t operator()(const QString& str) const noexcept + { + return qHash(str); + } +}; +} // namespace std +#endif