From a6387729d2d0ac0a0402a786cd2f0684e5c0f419 Mon Sep 17 00:00:00 2001 From: nayk Date: Sun, 20 Jul 2025 11:23:44 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D0=B8=20=D0=B8=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 107 ++++++ CMakeLists.txt | 10 + LICENSE | 9 + LICENSE.ru | 25 ++ README.md | 34 +- _cmake/app_settings.cmake | 28 ++ _cmake/common.cmake | 52 +++ _cmake/config.h.in | 9 + _cmake/copy_depends.cmake | 289 +++++++++++++++ _cmake/developer.cmake | 8 + _cmake/lib_settings.cmake | 29 ++ _cmake/post_build.cmake | 40 ++ _cmake/setup_directories.cmake | 25 ++ _cmake/target_options.cmake | 55 +++ _cmake/update_translations.cmake | 70 ++++ _cmake/version.cmake | 52 +++ _cmake/versioninfo.rc.in | 35 ++ _include/application_config.cpp | 225 ++++++++++++ _include/application_config.h | 51 +++ _resources/images/main_icon.png | Bin 0 -> 410 bytes _resources/images/main_title.png | Bin 0 -> 5469 bytes _resources/images/screenshot.png | Bin 0 -> 26292 bytes _resources/main.ico | Bin 0 -> 52014 bytes _resources/main.qrc | 6 + _resources/main_icon.rc | 1 + application/CMakeLists.txt | 81 +++++ application/main.cpp | 26 ++ application/main_window.cpp | 163 +++++++++ application/main_window.h | 63 ++++ application/main_window.ui | 197 ++++++++++ lib_logger/CMakeLists.txt | 57 +++ lib_logger/logger.h | 107 ++++++ lib_logger/private/log_worker.h | 85 +++++ lib_logger/private/logger.cpp | 605 +++++++++++++++++++++++++++++++ 34 files changed, 2543 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 LICENSE.ru create mode 100644 _cmake/app_settings.cmake create mode 100644 _cmake/common.cmake create mode 100644 _cmake/config.h.in create mode 100644 _cmake/copy_depends.cmake create mode 100644 _cmake/developer.cmake create mode 100644 _cmake/lib_settings.cmake create mode 100644 _cmake/post_build.cmake create mode 100644 _cmake/setup_directories.cmake create mode 100644 _cmake/target_options.cmake create mode 100644 _cmake/update_translations.cmake create mode 100644 _cmake/version.cmake create mode 100644 _cmake/versioninfo.rc.in create mode 100644 _include/application_config.cpp create mode 100644 _include/application_config.h create mode 100644 _resources/images/main_icon.png create mode 100644 _resources/images/main_title.png create mode 100644 _resources/images/screenshot.png create mode 100644 _resources/main.ico create mode 100644 _resources/main.qrc create mode 100644 _resources/main_icon.rc create mode 100644 application/CMakeLists.txt create mode 100644 application/main.cpp create mode 100644 application/main_window.cpp create mode 100644 application/main_window.h create mode 100644 application/main_window.ui create mode 100644 lib_logger/CMakeLists.txt create mode 100644 lib_logger/logger.h create mode 100644 lib_logger/private/log_worker.h create mode 100644 lib_logger/private/logger.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23a669a --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +.build/ +_other/ +_distrib*/ + +# Tmp files +*~ +Thumbs.db* + +# C++ objects and libs +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.so.* +*.dll +*.dylib +*.ko +*.obj +*.elf +*.lib + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +*.qmlc +*.jsc +Makefile* +*build-* +*.qm +*.prl + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database +compile_commands.json + +# QtCreator local machine specific files for imported projects +*creator.user* + +*_qmlcache.qrc + +# ---> C +# Prerequisites +*.d + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# Fortran module files +*.mod +*.smod + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9244ec6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 4.0) + +project (QtLoggerExample) + +# Logger library: +add_subdirectory(lib_logger) + +# Example Application: +add_subdirectory(application) + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9be8c26 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 Evgeny Teterin (nayk) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE.ru b/LICENSE.ru new file mode 100644 index 0000000..6aff4e4 --- /dev/null +++ b/LICENSE.ru @@ -0,0 +1,25 @@ +MIT лицензия + +Copyright (c) 2025 Evgeny Teterin (nayk) + +Данная лицензия разрешает лицам, получившим копию +данного программного обеспечения и сопутствующей документации +(в дальнейшем именуемыми «Программное Обеспечение»), безвозмездно +использовать Программное Обеспечение без ограничений, +включая неограниченное право на использование, копирование, изменение, +слияние, публикацию, распространение, сублицензирование и/или продажу +копий Программного Обеспечения, а также лицам, которым предоставляется +данное Программное Обеспечение, при соблюдении следующих условий: + +Указанное выше уведомление об авторском праве и данные условия +должны быть включены во все копии или значимые части данного Программного Обеспечения. + +ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», +БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, +ВКЛЮЧАЯ ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ +НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ, НО НЕ ОГРАНИЧИВАЯСЬ ИМИ. +НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ +ПО КАКИМ-ЛИБО ИСКАМ, ЗА УЩЕРБ ИЛИ ПО ИНЫМ ТРЕБОВАНИЯМ, В ТОМ ЧИСЛЕ, +ПРИ ДЕЙСТВИИ КОНТРАКТА, ДЕЛИКТЕ ИЛИ ИНОЙ СИТУАЦИИ, +ВОЗНИКШИМ ИЗ-ЗА ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ +ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ. diff --git a/README.md b/README.md index 7bc815c..4f17e18 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ # Qt_Logger_Example -Пример организации логирования в файл для Qt \ No newline at end of file +Пример организации логирования в файл для Qt + +## Библиотека lib_logger + +Логгер представляет собой класс singleton и запускается на старте приложения, например в `main.cpp` через + +``` +Logger::instance().start(); +``` + +После запуска используется стандартный механизм логирования Qt, никаких дополнительных действий не требуется. +Сохранение текста в файл выполняется в отдельном потоке. При этом механизм логирования полностью потокобезопасен. + +Работает под Qt5, Qt6, ОС Windows и GNU/Linux. + +## Приложение + +Приложение использует библиотеку `lib_logger` для сохранения лога в файл. Логирование событий осуществляется через стандартный механизм Qt: + +``` +qInfo() << "Строка информации"; +qDebug() << "Строка отладки"; +qWarning() << "Строка предупреждения"; +qCritical() << "Строка ошибки"; +``` + +Строки отладки (`qDebug`, `qCDebug`) сохраняются только если приложение запущено с параметром `/debug` или в каталоге ПО находится файл `debug` (без расширения с любым содержанием). +Логи сохраняются в каталоге профиля приложения. Если в каталоге ПО находится файл `portable` (без расширения с любым содержанием), то каталог логов будет создан в подкаталоге `profile` каталога ПО. + +## Внешний вид приложения для тестирования (Qt6, ОС Windows 11) + +![picture](/_resources/images/screenshot.png) + diff --git a/_cmake/app_settings.cmake b/_cmake/app_settings.cmake new file mode 100644 index 0000000..3dd65b2 --- /dev/null +++ b/_cmake/app_settings.cmake @@ -0,0 +1,28 @@ + +# Файл для подключения в основной проект через include +# Настройки для приложений + +include(${CMAKE_CURRENT_LIST_DIR}/common.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/developer.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/version.cmake) + +# Имя выходного файла совпадает с названием проекта: +set(RUNTIME_OUTPUT_NAME ${PROJECT_NAME}) + +configure_file( + ${CMAKE_CURRENT_LIST_DIR}/config.h.in + ${CMAKE_CURRENT_BINARY_DIR}/config.h + @ONLY +) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${DISTRIB_DIR}) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${DISTRIB_DIR}) + +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/versioninfo.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc + @ONLY + ) +endif() + diff --git a/_cmake/common.cmake b/_cmake/common.cmake new file mode 100644 index 0000000..4728977 --- /dev/null +++ b/_cmake/common.cmake @@ -0,0 +1,52 @@ + +# Общие настройки для всех типов проектов + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +if (CMAKE_COMPILER_IS_GNUCXX) + if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS "9.0.0") + set(CMAKE_CXX_STANDARD 20) + else() + set(CMAKE_CXX_STANDARD 23) + endif() +else() + set(CMAKE_CXX_STANDARD 23) +endif() + +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Поиск библиотек Qt: +find_package(QT NAMES Qt6 Qt5 REQUIRED) + +# Настройки каталогов: +include(${CMAKE_CURRENT_LIST_DIR}/setup_directories.cmake) + +# Определение разрядности: +if (${CMAKE_SIZEOF_VOID_P} STREQUAL 4) + set(DIR_PREFIX "32") +elseif (${CMAKE_SIZEOF_VOID_P} STREQUAL 8) + set(DIR_PREFIX "64") +endif () + +# Каталог для готовых приложений и библиотек после компиляции: +set(DISTRIB_DIR + ${ROOT_PROJECT_DIR}_distrib/${CMAKE_SYSTEM_NAME}_Qt${QT_VERSION}_${DIR_PREFIX}-bit +) + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(IS_DEBUG TRUE) + set(IS_RELEASE FALSE) +else() + set(IS_DEBUG FALSE) + set(IS_RELEASE TRUE) +endif() + +message(STATUS "Project '${PROJECT_NAME}' compiler ${CMAKE_CXX_COMPILER} version: ${CMAKE_CXX_COMPILER_VERSION}") +message(STATUS "Project '${PROJECT_NAME}' distrib dir: '${DISTRIB_DIR}'") +message(STATUS "Project '${PROJECT_NAME}' IS_RELEASE: ${IS_RELEASE}, IS_DEBUG: ${IS_DEBUG}") +message(STATUS "Project '${PROJECT_NAME}' CMAKE_PREFIX_PATH: '${CMAKE_PREFIX_PATH}'") +message(STATUS "Project '${PROJECT_NAME}' CMAKE_SYSTEM_LIBRARY_PATH: '${CMAKE_SYSTEM_LIBRARY_PATH}'") + diff --git a/_cmake/config.h.in b/_cmake/config.h.in new file mode 100644 index 0000000..41f8ac6 --- /dev/null +++ b/_cmake/config.h.in @@ -0,0 +1,9 @@ +#define PROG_NAME "@PROJECT_NAME@" +#define PROG_VERSION "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.@BUILD_NUM@@PROJECT_VERSION_TWEAK@" +#define PROG_CAPTION "@PROJECT_NAME@ v" +#define PROG_DESCRIPTION "@PROJECT_DESCRIPTION@" +#define SOFT_DEVELOPER "@SOFT_DEVELOPER@" +#define DEVELOPER_DOMAIN "@DEVELOPER_DOMAIN@" +#define BUILD_DATE "@BUILD_DATE@" +#define BUILD_NUM "@BUILD_NUM@" +#define ORIGINAL_FILE_NAME "@ORIGINAL_FILE_NAME@" diff --git a/_cmake/copy_depends.cmake b/_cmake/copy_depends.cmake new file mode 100644 index 0000000..211a6a7 --- /dev/null +++ b/_cmake/copy_depends.cmake @@ -0,0 +1,289 @@ + +# Определение зависимостей после сборки приложения + +cmake_minimum_required(VERSION 4.0) + +set(LIBS_FIND_DIRS "${PREFIX}") +set(QT_SHARE_DIR "${PREFIX}") + +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + + if (EXISTS "${PREFIX}/bin") + set(LIBS_FIND_DIRS "${PREFIX}/bin") + endif() + +else() + + if (EXISTS "${PREFIX}/lib") + set(LIBS_FIND_DIRS "${PREFIX}/lib") + endif() + +endif() + +if (EXISTS "${PREFIX}/share/qt${QT_VERSION_MAJOR}") + set(QT_SHARE_DIR "${PREFIX}/share/qt${QT_VERSION_MAJOR}") +endif() + +if(TARGET_TYPE STREQUAL "EXECUTABLE") + + if (EXISTS "${OUTPUT_DIR}/${TARGET_NAME}") + set(TARGET_FILE "${OUTPUT_DIR}/${TARGET_NAME}") + elseif (EXISTS "${OUTPUT_DIR}/${TARGET_NAME}.exe") + set(TARGET_FILE "${OUTPUT_DIR}/${TARGET_NAME}.exe") + endif() + +else() + + if (EXISTS "${OUTPUT_DIR}/${TARGET_NAME}") + set(TARGET_FILE "${OUTPUT_DIR}/${TARGET_NAME}") + elseif (EXISTS "${OUTPUT_DIR}/${TARGET_NAME}.dll") + set(TARGET_FILE "${OUTPUT_DIR}/${TARGET_NAME}.dll") + elseif (EXISTS "${OUTPUT_DIR}/${TARGET_NAME}.so") + set(TARGET_FILE "${OUTPUT_DIR}/${TARGET_NAME}.so") + endif() + +endif() + +message(STATUS "TARGET_FILE: '${TARGET_FILE}'") +message(STATUS "LIBS_FIND_DIRS: '${LIBS_FIND_DIRS}'") +message(STATUS "OUTPUT_DIR: '${OUTPUT_DIR}'") +message(STATUS "QT_VERSION_MAJOR: '${QT_VERSION_MAJOR}'") +message(STATUS "Find and copy dependencies. Please, wait...") + +# functions -------------------------------------------------------------------- + +# Копирование файла + +function(safe_copy src dest) + + # Пытаемся скопировать через execute_process (кросс-платформенно) + execute_process( + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${src}" "${dest}" + RESULT_VARIABLE copy_result + ERROR_VARIABLE copy_error + OUTPUT_QUIET # Подавляем stdout + ) + + if(NOT copy_result EQUAL 0) + message(WARNING "Не удалось скопировать ${src} -> ${dest}: ${copy_error}") + return() + endif() + +endfunction() + +# Отслеживание цепочки ссылок + +function(get_symlink_chain dep symlink_chain) + unset(chain) + set(current_dep "${dep}") + + # 1. Разрешаем основную цепочку симлинков (исходный файл → конечный файл) + while(IS_SYMLINK "${current_dep}") + execute_process( + COMMAND readlink "${current_dep}" + OUTPUT_VARIABLE link_target + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + list(INSERT chain 0 "${current_dep}") # Добавляем текущий симлинк в начало + get_filename_component(parent_dir "${current_dep}" DIRECTORY) + set(current_dep "${parent_dir}/${link_target}") # Переходим по ссылке + endwhile() + + # 2. Добавляем конечный файл в начало цепочки + list(INSERT chain 0 "${current_dep}") + + # 3. Теперь ищем ВСЕ симлинки в этой директории, которые ссылаются на конечный файл + if(EXISTS "${current_dep}") + get_filename_component(final_file "${current_dep}" REALPATH) # Абсолютный путь конечного файла + get_filename_component(parent_dir "${current_dep}" DIRECTORY) # Директория конечного файла + + # Получаем список всех файлов в этой директории + file(GLOB all_files LIST_DIRECTORIES false "${parent_dir}/*") + foreach(file IN LISTS all_files) + # Если это симлинк, проверяем, ведёт ли он на конечный файл + if(IS_SYMLINK "${file}") + execute_process( + COMMAND readlink -f "${file}" # Абсолютный путь цели симлинка + OUTPUT_VARIABLE symlink_target + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + # Если цель симлинка совпадает с конечным файлом, добавляем в цепочку + if(symlink_target STREQUAL final_file AND NOT file IN_LIST chain) + list(APPEND chain "${file}") + endif() + endif() + endforeach() + endif() + + set(${symlink_chain} "${chain}" PARENT_SCOPE) +endfunction() + +# ------------------------------------------------------------------------------ + +if (EXISTS "${TARGET_FILE}") + + # Получаем runtime-зависимости + file(GET_RUNTIME_DEPENDENCIES + EXECUTABLES ${TARGET_FILE} + RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS + UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS + DIRECTORIES ${LIBS_FIND_DIRS} + ) + + set(USE_SQL OFF) + set(USE_PRINT OFF) + set(USE_NETWORK OFF) + set(USE_SERIALPORT OFF) + set(USE_MULTIMEDIA OFF) + + message(STATUS "\nResolved dependencies copy:\n") + + foreach(DEP ${RESOLVED_DEPS}) + + string(FIND "${DEP}" "${LIBS_FIND_DIRS}" POS) + + if(POS EQUAL 0) + + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + + message(STATUS "${DEP}") + file(COPY "${DEP}" DESTINATION "${OUTPUT_DIR}") + + else () + + #message(STATUS "${DEP}") + #file(COPY "${DEP}" DESTINATION "${OUTPUT_DIR}/system_lib" FOLLOW_SYMLINK_CHAIN) + + get_symlink_chain("${DEP}" dep_chain) + + foreach(file IN LISTS dep_chain) + + message(STATUS "${file}") + safe_copy("${file}" "${OUTPUT_DIR}") + + endforeach() + + endif() + + endif() + + string(FIND "${DEP}" "Qt${QT_VERSION_MAJOR}Sql" POS) + + if(POS GREATER 0) + set(USE_SQL ON) + endif() + + string(FIND "${DEP}" "Qt${QT_VERSION_MAJOR}Print" POS) + + if(POS GREATER 0) + set(USE_PRINT ON) + endif() + + string(FIND "${DEP}" "Qt${QT_VERSION_MAJOR}Network" POS) + + if(POS GREATER 0) + set(USE_NETWORK ON) + endif() + + string(FIND "${DEP}" "Qt${QT_VERSION_MAJOR}Serial" POS) + + if(POS GREATER 0) + set(USE_SERIALPORT ON) + endif() + + string(FIND "${DEP}" "Qt${QT_VERSION_MAJOR}Multimedia" POS) + + if(POS GREATER 0) + set(USE_MULTIMEDIA ON) + endif() + + endforeach() + + if (EXISTS "${LIBS_FIND_DIRS}/libQt${QT_VERSION_MAJOR}XcbQpa.so") + + get_symlink_chain("${LIBS_FIND_DIRS}/libQt${QT_VERSION_MAJOR}XcbQpa.so" xcb_chain) + + foreach(file IN LISTS xcb_chain) + + message(STATUS "${file}") + safe_copy("${file}" "${OUTPUT_DIR}") + + endforeach() + + endif() + + message(STATUS "\nResolved dependencies not copy:\n") + + foreach(DEP ${RESOLVED_DEPS}) + string(FIND "${DEP}" "${LIBS_FIND_DIRS}" POS) + if(NOT POS EQUAL 0) + message(STATUS "${DEP}") + endif() + endforeach() + + message(STATUS "\nUnresolved dependencies:\n") + + foreach(DEP ${UNRESOLVED_DEPS}) + message(STATUS "${DEP}") + endforeach() + + if (EXISTS "${QT_SHARE_DIR}/plugins/platforms") + file(COPY "${QT_SHARE_DIR}/plugins/platforms" DESTINATION "${OUTPUT_DIR}") + endif() + + if (EXISTS "${QT_SHARE_DIR}/plugins/platformthemes") + file(COPY "${QT_SHARE_DIR}/plugins/platformthemes" DESTINATION "${OUTPUT_DIR}") + endif() + + if (EXISTS "${QT_SHARE_DIR}/plugins/styles") + file(COPY "${QT_SHARE_DIR}/plugins/styles" DESTINATION "${OUTPUT_DIR}") + endif() + + if (EXISTS "${QT_SHARE_DIR}/plugins/imageformats") + file(COPY "${QT_SHARE_DIR}/plugins/imageformats" DESTINATION "${OUTPUT_DIR}") + endif() + + if (USE_SQL AND (EXISTS "${QT_SHARE_DIR}/plugins/sqldrivers")) + file(COPY "${QT_SHARE_DIR}/plugins/sqldrivers" DESTINATION "${OUTPUT_DIR}") + endif() + + if (USE_PRINT AND (EXISTS "${QT_SHARE_DIR}/plugins/printsupport")) + file(COPY "${QT_SHARE_DIR}/plugins/printsupport" DESTINATION "${OUTPUT_DIR}") + endif() + + if (USE_NETWORK AND (EXISTS "${QT_SHARE_DIR}/plugins/networkinformation")) + file(COPY "${QT_SHARE_DIR}/plugins/networkinformation" DESTINATION "${OUTPUT_DIR}") + endif() + + if (USE_MULTIMEDIA AND (EXISTS "${QT_SHARE_DIR}/plugins/multimedia")) + file(COPY "${QT_SHARE_DIR}/plugins/multimedia" DESTINATION "${OUTPUT_DIR}") + endif() + + if (EXISTS "${QT_SHARE_DIR}/translations") + + file(MAKE_DIRECTORY "${OUTPUT_DIR}/translations") + + if (EXISTS "${QT_SHARE_DIR}/translations/qtbase_ru.qm") + file(COPY "${QT_SHARE_DIR}/translations/qtbase_ru.qm" DESTINATION "${OUTPUT_DIR}/translations") + endif() + + if (EXISTS "${QT_SHARE_DIR}/translations/qtdeclarative_ru.qm") + file(COPY "${QT_SHARE_DIR}/translations/qtdeclarative_ru.qm" DESTINATION "${OUTPUT_DIR}/translations") + endif() + + if (USE_SERIALPORT AND (EXISTS "${QT_SHARE_DIR}/translations/qtserialport_ru.qm")) + file(COPY "${QT_SHARE_DIR}/translations/qtserialport_ru.qm" DESTINATION "${OUTPUT_DIR}/translations") + endif() + + if (USE_MULTIMEDIA AND (EXISTS "${QT_SHARE_DIR}/translations/qtmultimedia_ru.qm")) + file(COPY "${QT_SHARE_DIR}/translations/qtmultimedia_ru.qm" DESTINATION "${OUTPUT_DIR}/translations") + endif() + + endif() + +else() + + message(STATUS "'${TARGET_FILE}' not found. Exit.") + +endif() + diff --git a/_cmake/developer.cmake b/_cmake/developer.cmake new file mode 100644 index 0000000..530fa4a --- /dev/null +++ b/_cmake/developer.cmake @@ -0,0 +1,8 @@ + +if(NOT DEFINED SOFT_DEVELOPER OR "${SOFT_DEVELOPER}" STREQUAL "") + set(SOFT_DEVELOPER "Evgeny Teterin") +endif() + +if(NOT DEFINED DEVELOPER_DOMAIN OR "${DEVELOPER_DOMAIN}" STREQUAL "") + set(DEVELOPER_DOMAIN "poseon.ru") +endif() diff --git a/_cmake/lib_settings.cmake b/_cmake/lib_settings.cmake new file mode 100644 index 0000000..324e71d --- /dev/null +++ b/_cmake/lib_settings.cmake @@ -0,0 +1,29 @@ + +# Файл для подключения в основной проект через include +# Настройки для библиотек + +include(${CMAKE_CURRENT_LIST_DIR}/common.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/developer.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/version.cmake) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${DISTRIB_DIR}) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${DISTRIB_DIR}) + +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(RUNTIME_OUTPUT_NAME lib${PROJECT_NAME}.dll) + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/versioninfo.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc + @ONLY + ) +else() + set(RUNTIME_OUTPUT_NAME lib${PROJECT_NAME}.so) +endif() + +configure_file( + ${CMAKE_CURRENT_LIST_DIR}/config.h.in + ${CMAKE_CURRENT_BINARY_DIR}/config.h + @ONLY +) + + diff --git a/_cmake/post_build.cmake b/_cmake/post_build.cmake new file mode 100644 index 0000000..39c5ee3 --- /dev/null +++ b/_cmake/post_build.cmake @@ -0,0 +1,40 @@ + +# Файл для подключения в основной проект через include + +# Вспомогательные действия после сборки проекта + +if(IS_DEBUG) + message(STATUS "Project '${PROJECT_NAME}' [post-build] DEBUG building: skip post-build actions") + return() +endif() + +# Включение/выключение поиска зависимостей: +set(COPY_DEPENDS ON) + +get_target_property(TARGET_TYPE ${PROJECT_NAME} TYPE) + +if(TARGET_TYPE STREQUAL "EXECUTABLE") + message(STATUS "${PROJECT_NAME} is an executable.") +elseif(TARGET_TYPE STREQUAL "STATIC_LIBRARY" OR TARGET_TYPE STREQUAL "SHARED_LIBRARY") + message(STATUS "${PROJECT_NAME} is a library.") +else() + message(STATUS "${PROJECT_NAME} is of another type: ${TARGET_TYPE}") +endif() + +if (COPY_DEPENDS) + + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "Copying runtime dependencies..." + COMMAND ${CMAKE_COMMAND} + -DTARGET_NAME="${RUNTIME_OUTPUT_NAME}" + -DTARGET_TYPE="${TARGET_TYPE}" + -DOUTPUT_DIR="${DISTRIB_DIR}" + -DPREFIX="${CMAKE_PREFIX_PATH}" + -DQT_VERSION_MAJOR=${QT_VERSION_MAJOR} + -DCMAKE_SYSTEM_NAME="${CMAKE_SYSTEM_NAME}" + -P "${CMAKE_INC_DIR}/copy_depends.cmake" + COMMENT "Copying runtime dependencies for ${PROJECT_NAME}" + ) + +endif() + diff --git a/_cmake/setup_directories.cmake b/_cmake/setup_directories.cmake new file mode 100644 index 0000000..99adfb9 --- /dev/null +++ b/_cmake/setup_directories.cmake @@ -0,0 +1,25 @@ + +set(ROOT_PROJECT_DIR + ${CMAKE_CURRENT_LIST_DIR}/.. +) + +cmake_path(NORMAL_PATH ROOT_PROJECT_DIR OUTPUT_VARIABLE ROOT_PROJECT_DIR) + +set(CMAKE_INC_DIR + ${ROOT_PROJECT_DIR}_cmake +) + +set(RESOURCES_DIR + ${ROOT_PROJECT_DIR}_resources +) + +set(COMMON_SOURCES_DIR + ${ROOT_PROJECT_DIR}_include +) + +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(SYSTEM_INCLUDE_DIR "${CMAKE_PREFIX_PATH}/include") +else () + set(SYSTEM_INCLUDE_DIR "/usr/include") +endif () + diff --git a/_cmake/target_options.cmake b/_cmake/target_options.cmake new file mode 100644 index 0000000..a8a8890 --- /dev/null +++ b/_cmake/target_options.cmake @@ -0,0 +1,55 @@ + +# Файл для подключения в основной проект через include + +# Настройки компиляции для всего + +if (DISABLE_HARD_WARNING_ERROR) + target_compile_options(${PROJECT_NAME} PRIVATE + -Wall + -Wextra + -Wpedantic + ) +else() + target_compile_options(${PROJECT_NAME} PRIVATE + -Wall # Все стандартные предупреждения + -Wextra # Дополнительные предупреждения + -Wpedantic # Соответствие стандарту C++ + -Werror # Превратить предупреждения в ошибки + -Wconversion # Предупреждения о неявных преобразованиях + -Wsign-conversion # Предупреждения о знаковых/беззнаковых преобразованиях + -Wshadow # Предупреждения о "тенях" переменных + -Wunused # Предупреждения о неиспользуемом коде + -Wold-style-cast # Запрет C-style кастов (только static_cast/dynamic_cast/...) + -Wnull-dereference # Предупреждения о возможных разыменованиях nullptr + -Wdouble-promotion # Предупреждения о неявном преобразовании float → double + -Wformat=2 # Строгая проверка printf/scanf + ) +endif() + +target_compile_definitions(${PROJECT_NAME} PRIVATE + QT_NO_FOREACH + QT_NO_URL_CAST_FROM_STRING + QT_NO_NARROWING_CONVERSIONS_IN_CONNECT + QT_STRICT_ITERATORS +) + +find_program(CLANG_TIDY_EXE NAMES "clang-tidy") + +if (CLANG_TIDY_EXE) + set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE};-checks=*") +endif() + +if (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + target_compile_options(${PROJECT_NAME} PRIVATE + -fsanitize=address # AddressSanitizer (поиск утечек, выходов за границы) + -fsanitize=undefined # UndefinedBehaviorSanitizer (UB-проверки) + -fsanitize=leak # LeakSanitizer (поиск утечек памяти) + -fno-omit-frame-pointer # Для лучшего стека вызовов в санитайзерах + ) + target_link_options(${PROJECT_NAME} PRIVATE + -fsanitize=address + -fsanitize=undefined + -fsanitize=leak + ) +endif() + diff --git a/_cmake/update_translations.cmake b/_cmake/update_translations.cmake new file mode 100644 index 0000000..f21186f --- /dev/null +++ b/_cmake/update_translations.cmake @@ -0,0 +1,70 @@ + +# Файл для подключения в основной проект через include + +# Обновление и генерация файлов переводов +# Языки переводов перечислить в переменной LNG + +if(IS_DEBUG) + message(STATUS "Project '${PROJECT_NAME}' [translations] DEBUG building: skip .qm generation") + return() +endif() + +# Если список языков не задан в основном CMakeLists.txt — применяем по умолчанию +if(NOT DEFINED LNG) + set(LNG en ru) + message(STATUS "Project '${PROJECT_NAME}' [translations] LNG not set, use default: ${LNG}") +else() + message(STATUS "Project '${PROJECT_NAME}' [translations] Use LNG: ${LNG}") +endif() + +# Подключить LinguistTools +find_package(Qt${QT_VERSION_MAJOR}LinguistTools REQUIRED) + +# Полные пути утилит +get_target_property(LUPDATE_EXECUTABLE Qt${QT_VERSION_MAJOR}::lupdate IMPORTED_LOCATION) +get_target_property(LRELEASE_EXECUTABLE Qt${QT_VERSION_MAJOR}::lrelease IMPORTED_LOCATION) + +# Каталог генерации файлов ts и qm файлов +set(TRANSLATIONS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/translations") +set(QM_OUTPUT_DIR "${DISTRIB_DIR}/translations") +file(MAKE_DIRECTORY "${TRANSLATIONS_DIR}") +file(MAKE_DIRECTORY "${QM_OUTPUT_DIR}") + +# Список TS файлов в зависимости от списка языков +set(TS_FILES "") + +# Обновление каждого файла с добавлением в список +foreach(LANG ${LNG}) + + set(TS_FILE "${TRANSLATIONS_DIR}/${PROJECT_NAME}_${LANG}.ts") + list(APPEND TS_FILES "${TS_FILE}") + message(STATUS "Project '${PROJECT_NAME}' Update translation file: ${TS_FILE}") + + execute_process(COMMAND ${LUPDATE_EXECUTABLE} + ${PROJECT_SOURCES} + -ts ${TS_FILE} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + +endforeach() + +# Генерация файлов qm +set(QM_FILES "") + +foreach(TS ${TS_FILES}) + + get_filename_component(TS_NAME_WE ${TS} NAME_WE) + set(QM ${QM_OUTPUT_DIR}/${TS_NAME_WE}.qm) + list(APPEND QM_FILES ${QM}) + + add_custom_command( + OUTPUT ${QM} + COMMAND ${LRELEASE_EXECUTABLE} ${TS} -qm ${QM} + DEPENDS ${TS} + COMMENT "Generating ${QM}" + ) +endforeach() + +add_custom_target(translations_qm ALL DEPENDS ${QM_FILES}) + +add_dependencies(translations_qm ${PROJECT_NAME}) diff --git a/_cmake/version.cmake b/_cmake/version.cmake new file mode 100644 index 0000000..c320df2 --- /dev/null +++ b/_cmake/version.cmake @@ -0,0 +1,52 @@ + +# Variables for generating the version. +# Used in the application (config.h file and versioninfo.rc) +# + +# Получаем полный временной штамп в UTC (пример: "2025-04-16 12:33:58 UTC") +string(TIMESTAMP BUILD_DATE "%Y-%m-%d %H:%M:%S UTC" UTC) +string(TIMESTAMP VERSION_DOY "%j" UTC) # день в году (001..366) + +# Извлекаем компоненты даты и времени +string(SUBSTRING "${BUILD_DATE}" 0 4 YEAR_STR) +string(SUBSTRING "${BUILD_DATE}" 2 2 VERSION_YY) +string(SUBSTRING "${BUILD_DATE}" 5 2 VERSION_MM) +string(SUBSTRING "${BUILD_DATE}" 8 2 VERSION_DD) +string(SUBSTRING "${BUILD_DATE}" 11 2 VERSION_HH) +string(SUBSTRING "${BUILD_DATE}" 14 2 VERSION_MIN) +string(SUBSTRING "${BUILD_DATE}" 17 2 VERSION_SS) + +# Убираем ведущие нули путём преобразования в числа +math(EXPR VERSION_YY_NOZERO "${VERSION_YY}") +math(EXPR VERSION_MM_NOZERO "${VERSION_MM}") +math(EXPR VERSION_DD_NOZERO "${VERSION_DD}") +math(EXPR VERSION_HH_NOZERO "${VERSION_HH}") +math(EXPR VERSION_MIN_NOZERO "${VERSION_MIN}") +math(EXPR VERSION_SS_NOZERO "${VERSION_SS}") +math(EXPR VERSION_DOY_NOZERO "${VERSION_DOY}") + +if (VERSION_FULLDATE) + # Полная дата: YY.MM.DD.NNN + + # Считаем количество минут с начала суток + math(EXPR VERSION_NNN + "${VERSION_HH_NOZERO} * 60 + ${VERSION_MIN_NOZERO}" + ) + + set(PROJECT_VERSION_MAJOR ${VERSION_YY_NOZERO}) + set(PROJECT_VERSION_MINOR ${VERSION_MM_NOZERO}) + set(PROJECT_VERSION_PATCH ${VERSION_DD_NOZERO}) + set(BUILD_NUM ${VERSION_NNN}) + +else() + # Версия от MAJOR, MINOR + YY + день года + # MAJOR и MINOR должны быть заданы ранее через `project(... VERSION ...)` + + set(PROJECT_VERSION_PATCH ${VERSION_YY_NOZERO}) + set(BUILD_NUM ${VERSION_DOY_NOZERO}) +endif() + +set(ORIGINAL_FILE_NAME ${RUNTIME_OUTPUT_NAME}) + +message(STATUS "Project '${PROJECT_NAME}' version: ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}.${BUILD_NUM}") + diff --git a/_cmake/versioninfo.rc.in b/_cmake/versioninfo.rc.in new file mode 100644 index 0000000..14c4204 --- /dev/null +++ b/_cmake/versioninfo.rc.in @@ -0,0 +1,35 @@ +1 TYPELIB "versioninfo.rc" + +1 VERSIONINFO + FILEVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, @PROJECT_VERSION_PATCH@, @BUILD_NUM@ + PRODUCTVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, @PROJECT_VERSION_PATCH@, @BUILD_NUM@ + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "@SOFT_DEVELOPER@" + VALUE "FileDescription", "@PROJECT_DESCRIPTION@" + VALUE "FileVersion","@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.@BUILD_NUM@" + VALUE "InternalName", "@PROJECT_NAME@" + VALUE "LegalCopyright", "Copyright (c) @YEAR_STR@ @SOFT_DEVELOPER@" + VALUE "OriginalFilename", "@ORIGINAL_FILE_NAME@" + VALUE "ProductName", "@PROJECT_NAME@" + VALUE "ProductVersion","@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.@BUILD_NUM@" + VALUE "BuildDate", "@BUILD_DATE@" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END diff --git a/_include/application_config.cpp b/_include/application_config.cpp new file mode 100644 index 0000000..72c8d67 --- /dev/null +++ b/_include/application_config.cpp @@ -0,0 +1,225 @@ +/**************************************************************************** +** Copyright (c) 2025 Evgeny Teterin (nayk) +** All right reserved. +** +** Permission is hereby granted, free of charge, to any person obtaining +** a copy of this software and associated documentation files (the +** "Software"), to deal in the Software without restriction, including +** without limitation the rights to use, copy, modify, merge, publish, +** distribute, sublicense, and/or sell copies of the Software, and to +** permit persons to whom the Software is furnished to do so, subject to +** the following conditions: +** +** The above copyright notice and this permission notice shall be +** included in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +** LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +** OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +** WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +** +****************************************************************************/ +#include "application_config.h" +#include "config.h" +#include +#include +#include +#include +#include +#include + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +# include +#endif + +//============================================================================== +void Application::initialize() +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); +#endif + +#if defined (PROG_NAME) + QCoreApplication::setApplicationName( QString(PROG_NAME) ); +#endif + +#if defined (SOFT_DEVELOPER) + QCoreApplication::setOrganizationName( QString(SOFT_DEVELOPER) ); +#endif + +#if defined (DEVELOPER_DOMAIN) + QCoreApplication::setOrganizationDomain( QString(DEVELOPER_DOMAIN) ); +#endif + +#if defined (PROG_VERSION) + QCoreApplication::setApplicationVersion( QString(PROG_VERSION) ); +#endif + +#if defined (BUILD_DATE) + qApp->setProperty( "buildDate", QString(BUILD_DATE) ); +#endif +} +//============================================================================== +QString Application::applicationRootPath() +{ + QString path { QCoreApplication::applicationDirPath() }; + + if ( path.right(1) != "/" ) { + + path += "/"; + } + + if (path.right(5) == "/bin/") { + + path.remove( path.length() - 4, 4 ); + } + + return path; +} +//============================================================================== +void Application::installTranslations(const QString &lng) +{ + const QString dirName { applicationRootPath() + "translations/" }; + QDir translationsDir { dirName }; + + QStringList filesList = translationsDir.entryList( + QStringList() << QString("*_%1.qm").arg(lng), + QDir::Files + ); + + for (const QString &fileName: std::as_const(filesList)) { + + QTranslator *translator = new QTranslator(QCoreApplication::instance()); + + if (translator->load( dirName + fileName )) { + + QCoreApplication::instance()->installTranslator( translator ); + } + else { + + delete translator; + } + } +} +//============================================================================== +QString Application::applicationProfilePath() +{ + QCoreApplication *application = QCoreApplication::instance(); + if (!application) return QString(); + + if (!application->property("profilePath").isNull()) + return application->property("profilePath").toString(); + + QString path = applicationRootPath(); + QString profileDir; + QDir dir(path); + + if (isPortable() && dir.mkpath(path + "profile")) { + + profileDir = path + "profile/"; + } + else { + + application->setProperty("isPortable", false); + + profileDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/"; + QString appDirectory = application->organizationName() + + "/" + application->applicationName() + "/"; + + if (!profileDir.contains( appDirectory )) { + + profileDir += appDirectory; + } + } + + application->setProperty("profilePath", profileDir); + return profileDir; +} +//============================================================================== +bool Application::isPortable() +{ + QCoreApplication *application = QCoreApplication::instance(); + if (!application) return false; + + if (!application->property("isPortable").isNull()) + return application->property("isPortable").toBool(); + + QFileInfo file(applicationRootPath() + "portable"); + + bool portable = (file.exists() && file.isFile()) || parameterExists("portable"); + + application->setProperty("isPortable", portable); + return portable; +} +//============================================================================== +bool Application::isDebug() +{ + QCoreApplication *application = QCoreApplication::instance(); + if (!application) return false; + + if (!application->property("isDebug").isNull()) + return application->property("isDebug").toBool(); + + QFileInfo file(applicationRootPath() + "debug"); + + bool debug = (file.exists() && file.isFile()) || parameterExists("debug"); + + application->setProperty("isDebug", debug); + return debug; +} +//============================================================================== +bool Application::parameterExists(const QString &name, const QString &shortName, QString *value) +{ + QCoreApplication *application = QCoreApplication::instance(); + if (!application) return false; + + const QStringList params {application->arguments()}; + int index {1}; + + while (index < params.size()) { + + QString param = params.at(index); + ++index; + + while (!param.isEmpty() && ((param.at(0) == '/') || (param.at(0) == '-'))) { + + param.remove(0, 1); + } + + if (param.isEmpty()) continue; + + if (!shortName.isEmpty() && (param == shortName)) { + + if (value && (index < params.size())) *value = params.at(index); + return true; + } + else { + + auto n = param.indexOf('='); + + if (n > 0) { + + QString val = param.mid(n + 1); + param = param.left(n); + + if (param == name) { + + if (value) *value = val; + + return true; + } + } + else if (param == name) { + + if (value && (index < params.size())) *value = params.at(index); + return true; + } + } + } + + return false; +} +//============================================================================== diff --git a/_include/application_config.h b/_include/application_config.h new file mode 100644 index 0000000..c6ba2e4 --- /dev/null +++ b/_include/application_config.h @@ -0,0 +1,51 @@ +/**************************************************************************** +** Copyright (c) 2025 Evgeny Teterin (nayk) +** All right reserved. +** +** Permission is hereby granted, free of charge, to any person obtaining +** a copy of this software and associated documentation files (the +** "Software"), to deal in the Software without restriction, including +** without limitation the rights to use, copy, modify, merge, publish, +** distribute, sublicense, and/or sell copies of the Software, and to +** permit persons to whom the Software is furnished to do so, subject to +** the following conditions: +** +** The above copyright notice and this permission notice shall be +** included in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +** LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +** OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +** WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +** +****************************************************************************/ +#pragma once +#ifndef APP_CONFIG_H +#define APP_CONFIG_H + +#include + +//============================================================================== +class Application +{ + Q_DISABLE_COPY(Application) + +public: + static void initialize(); + static QString applicationRootPath(); + static void installTranslations(const QString &lng = "ru"); + static QString applicationProfilePath(); + static bool isPortable(); + static bool isDebug(); + static bool parameterExists(const QString &name, + const QString &shortName = QString(), + QString *value = nullptr); +private: + Application() = delete; +}; + +#endif // APP_CONFIG_H +//============================================================================== diff --git a/_resources/images/main_icon.png b/_resources/images/main_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5770cc449ea2d5591c392cde89172247a93e5fec GIT binary patch literal 410 zcmV;L0cHM)P)?Uw8Cgd+{x?0#IXXn-U<@K1d&mmD(Km5)V8X!BB@p6obc+N8T4-tTJrEkXiDQ-Oe#l29A7k~^ z!ayGA79Q<*)7ZfMng)dlok_$hWeu#Qu>k-80P!FA1D0C#{UmF7@c;k-07*qoM6N<$ Ef*7!@8UO$Q literal 0 HcmV?d00001 diff --git a/_resources/images/main_title.png b/_resources/images/main_title.png new file mode 100644 index 0000000000000000000000000000000000000000..b0edb75308c671027b78da604ad7c043b525ac80 GIT binary patch literal 5469 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 MYplc2I)CYZ006#56#xJL literal 0 HcmV?d00001 diff --git a/_resources/images/screenshot.png b/_resources/images/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..7fa5c2ea5fc849651b0dc7afa8aa342a392841f4 GIT binary patch literal 26292 zcmc$`Wl&tvx-HrfNFV`%g#-yf8;9Ud2yTtL1$URG(FB4!jRa^QcyM=jhd^*maBYG+ zyhZlj=j?Out-7!3?W*@9t4MdVm~*bR#y7q(#_V7Phy(^YAvy>I!jO^_RRVz?asmG; zpFRX$v9}G41cBayq(p^P+z`79UQzTLy3FTG2sZB;d7PFvJ)L{;#b!7x;c{=eA^!9U zKTx9CF;$(PbK!Z&PH5gi>O z7XDs%Gm4w7Sg|3}$g}ilnYiU6XHAVCN|a^pSS5`Dmyr*EIq$&vS_tStMYDJvuu}h+TA{ zxV;-GS#medTwERw%uICWa=+xxr~asdD0{flduJZnlES3(t%`!)h6s3@OIzEe19*S4J=`uwU7v^bc02l6&7b@8j`<%I*y4adpX~Mu zk7WpKe@7x3b{0gx_zQwSG)Yd-rmNlyZ7%hN=fJNA9M#yq;Tf?htbYys?>T|(9KUZ% z=tIya!+KyMlWfq-&&;0Na?IB<{g}*+TRAOWJsulVbiilTU1QyUKD#?aVqFg;1lrBV zr`AAyHFn(X?UbZ>Ed^?YT9bi5l(V5Pr(TjKknFwdpVr{97bz&Dy%JJ#QoTaD$lrXBps(d30{TUMB9q+Hvah_(R?_%p z#{0%(N55hJD*ko0;JH8C=aT96QCfxX=j}zJ5?9V)9c$Ov%it@a&AV&6#kN_46%s!J zronmO*84g$N?|q9czF5DECV-yU>FfDU;1*JZKUb=^N42K`?bLD=a1=Ic4wc~O9^5&``Q5#sXKoJ%N?9p2hyfvKzJ}>I>-|bWqlZHfW z*GN9wt3TS2d_s5qOM+E5oq4~yM}+Ri_IjA^`bKv7Pv=7i!}psHl3Fgx_8lVPfQr)@TL^Rmd&d$te!tYP9(gV@l{6O5lw|-jACUyLl&2=mwws@KGOlf8lzyDH(!rBHR)!mq@!koh9|Ug`e5p zPR{X|ZOldL-cITBod84R;d`7p4t(4bIWcu(KIJ~=0Dk_c)xhQV1`=|yM|R8a-4~CS zo!_(cXY=QhtGD-V9f8o@wak9Y9xwUj^%1iGu;QY}1NxrZX6HM>n>RJO1WVJvSqyb? zNo$(h3X!Kq2}@ob6Sw1gz(m=ka@moD1o z5VaiqCn;EV^=tgFOI0wdjgZ53M{-Y!8qeZs0KKh{Yu!@&hMpIRBKj|T5n@AhZ(Tze zz4wxn*D~0Tg*leK{^O>{e&RTL_^NHH>_82b_lFnf(ur$Hyz4L4uUIW8PFxS zp#?6TCSaUkeb0%d^M?J}z50R4;GjB(t6*i|R^sP|N}a84sr&5g=@@5LByGr-HeV!N z#eIb?xXBG0+6lQ{G}twETTE9a6g|oOg}Zd2?$eI~DrMgo)~~%~?e<)Wr)L&`dgm=Q zNBCrp+=bmgajA9FR7L0at8i*X{mgI}EHWpL$baMGS8)K`W~*N?)`1%)uElE`>iwts zegSEErf?T$?sDzs?;d%(Sos{o%!(p|5H2lMLYLPydVD)mF%<;zsop2i9$YD{Th49J zB{NsoC8$}cA1xaX=o9SXCPn%F`H#8HhguA2P+a=DIW3Zsi|QjA^8nULR()IR0;WlIkkH$FF*~zEdfN~iPWu850$~>H z;tK%?ttw0o13;Ncl=lC6EOsHUKlyz17q&lrhhqtb)!~6acmx^$Uytghu`%p?wpr){ z=EQ^WZxN2E_17N7@mY04VRum*G@rbpEBU9QzeOOLu1Qb(U@$$|n&lJXD-D6NG0qzw z>7)f69lwvak6ny=8z*pih7nV#ps2tZTMY34M%+<_#GV1mia{*=;&grfnq_`PbWC>P zh4>zH_0f%5v$jo)VT+2& z&6gKlR?_dMik;ENWa_#n9xaGiwi)i3rKJVtr8}PV2ct&Hd~vhhav9Sf%nc)PF5l?V zKP3f!&usmD+ni4QCFdqDWtQQnqbXdmHhs{R^=#1Gr~NG-h9L**0i|@3&74-~!kKfl zSmU8uBNpuw@l%rs29Mg6N4T@hH`344(oR_os@_TM(wU`1|Dg}Npu<$ze%a|+w6{aW zqurvUZ#7D8)Fk;%&S^e{Pj%UZ!ufrFt;rJB(G0`U%EELkg9rOpMuW5)exu~YnTv&} z;M1EuG(B6T&=u}7i9=Vn!t5eK&GeL_)LsV!>#^&P=deP1;vNUXlJVDh54SKPcH+ZZ zHR0?HbzsN1YN;60aa8gi2jW}>wyRE5B9^_f=nfQ@RnifTm@9KdIZfAMA4(QI(wv@v@U&3)zt;QWAhzG>aGoCuL*^FeJN}%$Ld9h9Q zX5o^3&%~r9o|-m2@#6KgDCu%A93dBUR}7ddo=Ks@z&D$R<+jAEGr~~a2j(}eN$7Mo z&wtrdrQx9?U%j5N7x0}eq@Qk+~8cO2EHeZ&4x>u#6?=b!O`G}9(^B&=^~bsAtc)|T>OK~vYAj* zW#73hPh-VW)YfzRYbqGQnnF?RH>A#>&fDCN~`6m zP&KQB%$p#%^xIy`d47QZ0Cku3y$ala)Ua&%w{o?<;1l@)8)~ItN9ssXxHO6w|b+`jT}@ds5g=J^kg%qS0Q;%tz!^xzN5(PVTh8h1>2bhJC>~^>t7*MZA0dJJL>B`22f2NTuRmaA`gbqRcr zZ;d}N+4SA=m7QA{?kE26K7HHMK#Ih#Aej87s%Tzb{$lSc77?oS!ipce0Gp{vh?3l_ zd2@ePHcpzz`+?}awHIS}fJy&~S6~?2H zIx1<7wz6xM(4D}QOStH@7IvHv;tacQ%X_x{B;`)jQ}@=0QXh9T(EzQH9@xeM9IRttIqs)>_9AB(~M`(lf4D z@`Ai|5~IyY{|N z9ecLkX$ri%BdD9l)pLo=&OZn)KM|RjR**>|Q7YY58`X*4A1^#jghagb^Spe1vH9d33%^3Ix|ygU-zs?XFbWCi;B#e zIk~MIkPH;XzC_OBlb5_*@7@x-tZ7FstMXTfusA>Y_6$tw`AS}rj!xKS_;}%S$H!E! z$m^l^r&D@2k7|rl3nkgU5sUGJ#x1aCo^1nvoQZG7j) zU~bvN!gCYqqVz#Z@i%5N{F?HPNL+Etc(;b>J_LJk;&)myWr)@!v3Ob8smE6-EhWco zW3zC>)jz@NoN`cBc)z*Q*ycK6`20+;AzJ+OlYpco*8`S{*-ljOO??P8LLL%=J2JuU z(M@8SU)5>K%{kzCjBT}!{8|#+l5vq2N#60u9%Le($1i2Y3V$Wm`4jVESyKatt32kL zsMdh&cr0z|6{Q+O=FQ+Ldt#k0YlV1Mx6gJb|DX!fU>0loLOX4VFN$eoH(sz=J6gQP z6Y-=Z#{=0=m@am-N%YlY2)Qr=n5rw?tvDx<4OuGAm-m`!ouUwW%8)U_etl5KomPSp zD5DnJpIjM$qlA00CGO}|J}~&c10vI5jY@GcaNHNNTqp0imJkBY?Th|E%{C%EJg)ZH zRCM5@dYddA?gC@%p=Cbxnk+xAd}${)%ez3$(y78i&ev0V()F2bA9n?JbY}DSFk;}# zosJws2!}w9*MLx%=l-&4!K66tG1dR;RRGu8E)4eIcg3t9UXH zIG6g5SeI<7^-=j_1~8*OlFoO(H?zTCS#K?#??9FvDklEnTkJL0U?FGTl-{L_31TyD z8z)+Y86#|=m03xC@|G3JBFMVq;_RNc-(@67uo4(((+I#Vigfsjx$%4M77N zSzA4D$Q{`_&xNcS!bH{FBd(R&R_S-~ zSPK!ni7>Scf$gayFtK#LZw$O&)>ZjCt{g1xN0vFAE;I=#H6DJ_VG6l_in)WL^Z|(q9YYZ~hCKU^9<8crs4S|f zv+owG_oC@2_sPFgXu~NPy-K!-IF(Rv#uZCpzP#nSPDR!a)&<^#wiBCyK3!;c>PV0A zPNcUx#JL&<=m;+qV{`@-b9D;aczixH5hj@tuN$b-qFk;<2ie;*n0JlL&PiQWy`41w z_*GOC!2bA}F|g_Gme|2WsdKH9NaJXClqi9I@;syh2IkO)AjhQ&y^{+Md`;`d59*15O<{KN`+%SCAzJMaLL6et!MvhLBunuA0j^J-aW(>UO=Em2w6LlO@SHuKg(Xy@O+rN$jy+#i#{5a(I8g&(S z1g;0AT?5oieBeghS<(S(f@s(jn-(!epyP1Z5e}_ zd>|uf^OhlKO)8s*FmdMBpdJV`sIL}_1CXu#FA!1V>IRYSH;W}Fw^4>pE6}&sp|0A_lQnOXzG}Ozx~D;$+m0HXU@I= z5?;|4R@bFOh*#TSceN^}Y<7ytkL2Wp&l}g2Eefi+zN#cm^p2x~ZI89)B(I+1fIgCj z{E<8UGaeE&e|F0_8&RA|Wn;z`{@JM{&9_uhdg(K`oZ6r*S%tZIQ+n z=(O&1MPOU%kfEM0BdXNI&fNh=rhx`=Ib(4`F=VM)hfv6I!$p3HX=j`0Z=$gQ>*+I z6@bO4<}*HV7s;2?Y+ii9B_3b{4eH$v#y8vh@#;r4O{E;XH&OZoJ{5Y5+S4dRZ4j@H2=5J)2xX!&)% z*ku4emy$_80KIqk&mMXHOzRLTsotoD&BCH@$O+A$U?S&Lk-b1u)NqD>J+SY_Y zC-Do=C;Vg>lgWu_#0qzWa(B4_Gjf>v`&`xM=^rNn+}~}OS)8A4_j2~@4o{F{L#+Uf zW9}{_ZSjSNT2=Em?fU-#JlhUAk1lGS>?qc(m9=wR&u^MGg!6BKKn3v>(y)P#6sjgA z^@-iBSjupYiq>PxpUiDM+_mlm=ZT7n|10v8R1SI0N!h8)QYY?7i$J%32i}-#pfiB# z1^%iC(a0~Z$Mt(>GGmQJaF?<3aW<_FjoZEAHI8bDx122xqT^+|brd8PZGS&VJbOqF zYPDv~jLmv|sNpc#YB{l+V)e+3N+CVRBVpxVn32mc^Nt=+6#g4#c!uj-{tIRd_x41& z?t#Xk|A+3Pb4W1MY|Ig|8^_z$(C{q+Xomy0F`(hj8a4qvWO@%PEDv95#ic_j8)OT> zsHed-oh8r$akYrD2Oy2OLK-UT*uq7$l%QS!M1rq0J@`c1#e>~uN`Z!wPnG$bMw8l{ zb7bU`#5_7f*_?$L*zVZLX!T9GISUi%B;$UFKmP3MCKw<$P| zcA}uHDTC1gYfqtu&r~g&tM>dF7xZy!{XouHxKIw6D$;dzD{PHUVI6y_iMzsC#Kj$9 zHR|e2!f3&#Z9mFdGKl;mI!XD<4gMOy(TXAcEO!pp-$sAWTQy)WL!4|0kJYJ=1$~<6 z3sC}SBDAIa?RUA!khBV|i61E`wDn)bI;TW8*k0UKq@@Vlr2+%8!1o8s1M@3ml2{zm zX?b(QKxM&K@C8{qi}sjZSan92ev-rqDhrQXOM<^L_=_y~t^Y(8&#c2NoDW&<6G@)p z5@;snzA9N%qN0uSc^HTC><>U{#sXGPn$pMjB5a_&+b01kN7YB=z@tBG_I7bL>Mycf zJtL&k@e3++fX!9I%=}uiHe#hZezO#j|NM8PNl3p(n!SG@O~ms#WYk;{x>JiA6rsoW zL`$Fyr*QB_w+O5T`ByhHTBo9gCMrt#okAohbvj6vlEMT~&hl_nn;3SXgbn_;iP^#=xW8nTB+;Ld#Ldno$w9j>pVS@Q=$S}6wvz! z{FZF&@o2IJ(&Fd%wye7E=I|qeBzyR-7^16&JY`a5)K9);soFyc!2E9xH2D-% z>c&=dzPGEHjR7WqYG^aRqX_$XMSu}zAu_c4@FZ~$sj(>ijAYnj|9Z~v}6g@-L3guivrEdH2@rk}`;6md&*XIS*)@kx)5ua;c-2J@Y580Jf9F zS#Bfe_!3Q(vC(avdG?*pfTcJk4XU9TFxzU~{hoD8ssAHh-NfFjp5AUaO^SnTA zf4wc?R13jJ_Iu57=|W)S>w_+z0qbr`ngBxl)%lk@UJ=g*zbI9M4GN8t{sLQ6VQR13 z;EHA38}pVaZ!rL*LZ%6K5-=yGV;l=Mb2FOl@Tiy z@G$6WMeGGHA4CpqSIy?kNjEocdD?FH+xk#oV^92(cy$L2DG)Fd*}k)tTjX(+6I6-a z&OUz7s}78<-bADTR}p444vdcNr{u(7#|>jygp5{T{er`A+Ycf(&q6d%7uOEgDD<>& zETB@+(Y7_`-M4aXO`jubG-B{+;ROe$h?}LDDDrW(*xaZ~W!9c`&bw0TC4xSQX`1Z) zR$NVZR8^RW+|!8g+CUkZ_5&NJ^01adk?VF+RnJ+>#L{+gpXqQSnHg?PEegV6Cp|Kc$k?Q zRS<{wn;~N^3VxXMwRd_2`&5a9P_5+bFkL!dq3gp3atf-hKMtFbn?A87$cVmBd*4~a zd`*#XK}J-~L+*i~vK{o2tas%87SF#|SH;~3B=D~RenFUHjnWooA7Lfkvfj@-N>(k7 zoj+OmYY>|kNtO}_BUI>+$7r9c^LM-bJ7$uuiAHpZQ^#?AY<@7q=N zmgWsQB3yF|Wea54(eL2>40kmi+4^yPTX3SvAy=_c>$zL3J5lI7b^)(vZ97NIj;lJ6 zF3^FDY*4+|vMXAHH0&R_7)szfo9l*`O%@x>cw(%V~Gg+3)+vy>Z(=HBBr5 z3q;R05kd+AwH_io{}}M&lKjWp)_*j?Rzix>0BN+!LJ!mLu4d(;j;U-N25_5$w$cKV z(k2TXsC{ziHLPX&PeOmjZMnA*;{vSUD~B}Nzu4kCM;W#`{NgcAT0CoNqTFKJe@F3% zL2nW~i05q1%bp?5RS-ywMH^TM8^9;Q5DKlCv1Mk>toh3=A#w|4jd3q|5q0lx_uAa! z=S%z@_#*|#9eOt!VrCoW6^8v=Zwd+W(@ zZJBcA(KOPvV_+-azL@}2LB!Itnpgj2FI9W&oS}yN1?EYne*iNuHd-<)ivsa@?cd_T z`5cx!_~9lLFJ6d*8xTJODNt%EN>9^hHsSJO0}c!3&E5g`?{aE*YNcTpq0p{)4kwZ! zQ(2C?x8tjxNxu)ZRWmXbV?~mZksxy z!6ffN^T60osPQs*(YP@q+<=tm8&|3IQxHA-&kZF26!T9myo*{rLPs17nOAhZ{uk7& zIF|VrV18l~XHgAiAjo4mQZ_)U$JrU%x!rd_pTVr%AQCN1%_JGUC)2Sw6V% z4u%a;NLIZ&Vzl~^PqSLIll>R@P&2`Fi5d60B1C9lLMn8 zzK;+y6aM^v(9EfC_cU|ko@T}s=ug%*+RQcnbUQQRaOmLsu^k+z@Wb9)9Z=?DZK>m0 z2aw#@*s5S5Z4Ym&`IDZHn$inIi;`9k!)Hw15RqHBI>~otkb(+(2?dVNi?HU?>^3GA z${xE^$`wh^J-@ExB^XW79@}*nS|X4Kf!6m{{jki2vqRdfYfvdf=2X8^MB*dKpJH{r zohaz=rtlB~eL~@RtJr^G9}E!3xl5}48d-EuDT}fg?R*9Ct0$mORBddd4?y2)Uu1*+ z<3~qvT7cCRN7FG>O9cjgi?EiHKF`jY!OiodvnU`vK1V6dIweRy0+oK?$%qBs&YWM< zVZ?#kHolV%mx({>7_2J!+s5kkvczpX)G)sqa)mP3;X7J`J|2{3#J-(8HxP1HM=!jauC}p? zsbHa~O9soZeg}HXFtOUy(lTT>eXIQnQ^{9| zmCgfYfF1SL#(QkL_1Kt=wYn{?v~?sA-=vb)mHXGt3NgC2lrje$oy71|e%@iV@SDo~1IB6_5WIb8tlvtT zlC*`MSU$(2c3s$%vPoT``%Q_Y?9=0&5Pw5RPk6uPT)Se8qD-hHt}w>64#!~(Bf#Ys`^|JlM^-7c|J`| zlH8IpAIQe^X{~x`p5$n4Xg43Qx~KLKToWQP_TghHeo;=nwjzp`S>V@Z<1#TpmB{{wuR3)Sl9IO*mb=Te&4$KG z-%RQ2;uq4*l+%u)c{*HA0WMNhEj9f0&zjs{;cw3VpEUDaf-`0^-4=?-&0me_@J%;V z3J2Qvg~a*C+3pVMkZ5w)WkFXPL4yx8BFOwa2GsqQCCCHL4RA0uq^>y_}R#D~w^WKTaD1wNt6)8H&J1?!4mRxie zP>q95%PfG3cW;Q(o9TXk=f3nzS*dBYxs!Eucl^2fQ#r3SGqJp1(h$9Naayk@sl6em zrph*`wnRr{69v(*PiLIbE1jtvGxXJ2RfR7hgw&JN%yH??vBMgLSd@iHMX|4@N*qIAdt0=Cvwb1iuE93F-}{;wkTVu~5;?e>ORUN+T&%4v%tl%g zC0%L;cSDrQutuC7Fx1`v3pC=@$&RX4YkBJOPpjBF&2Y#_2Q2HWu>#~i+(rX3_*SHi zxwx}i5jc8;XpBM6^e-;-%nwk%jC^QJmfA}^RgEY2?kgKGtE5by^5tdiUy#n`qXa5b zkcZrvYFqRAOi>8u#}FgUL%r`n6d~WnX6;T~eK=sq-v7l_jAU7AQ|RI^*YW=oW%mD> zLeYh>|GogM=DP>=xvwd@NrNL!()5Wz2zqHivUp06|6gT_kv!(!2FQ0$DIlC>no5&+ zCTH}1F5?HM9QpqcS5AMM0A+3K;RZfZW#`s60DbE1@0;J^2#>B;o-tu(c26ml`lpM0 z&<6yKPOgQC=!kb7ixSP3v!Y(y)6v6~|k*2b;fbLp+AE zPGzs?WUyOSm)F4IvWKtKoXO9M0%wY8fC!L_zg21R#TQdCzTQ{Gv3hk}47s`(aGELq zZT6abkNolFcA6J?hUKQ24VPUmt6djbQE;=$&r`k3z&9;GHp?jvVH)CL5G@G^zw!Yz zha)%Gf{nC4(F;Zh?!m zE0b#adE(FkYCv0ZO~_pk%ego;Z+o@Jg{ATY6w(9J)D$R-IU{?7;xRO2)rAFQlZQ)} zpMXIA0W>HeP?!HdkN9sb%ASzsWO031&x{M=mu0(-`tfI<+4m=WpB!&I`c7f|1|#Gs z=*MxEMd+(j^L)-+5`s1~!B;Omd6^FP&r#6sdlmeh0mfFtlMg=V6ae8kG|FAQ2`&R# zMC{)srU;>sS3Eu+cvNcrxQHS7)ggC}pU%Jpd)jt-e^MUs@RW4(Am`lfwCP*7Db7 zhc0W<(;ep1?7^e;&qc#1Ns zk;#UtG@+KA`rXLP7&?tzI5`EjV8_9#U0yE#*L)iw?smp0KFZsX`IY&%;;^<_;qmUE z?BIiuMlZRPuXJbGF?TL;NnaR}m>IX6jn^2H4~Jzh1QQJTpCrVMQ<}xR86ZF7i4yhI zfb>^rc}tR*5s_`st-*szdnElzsVY z3b$Cv#CTu7v0vY_n(S0j)}d3b5;_pX9Y~0G2EZu|m=*g~4fvImoFnyCdRwc^3nrV1 zQ`DcUnu+B8_zm%$$j9ZyEEU9zTzkr{=!i~%#Q87@m=d`tu9b;tiNS6Zp8=NGX(H1o z=A)I;G!nFi^z}i+$I;y`A;_zPi`}JMYIc2 zrk{r*B*K!rTcy1te-ZDg2+^_;w5(jBfziu~5#~75)x-4lBcaM2?k@ zG){Szr$yOM?Cpm}r<@BZi9x1eO0T8l#^A>rVA$#>E=*Hsvt z_cA(!feP)l6F-i8;1r5!&JHbdusAUTk1tQ^mQo<+9nD|w>{S|liqtKh8d?<#nxPG` ztrY*j=;4^)n#^Z-sagCWtu%L&g6S#YB<2O~se*Od4DQ|wSYGKh)DB`8PD@(a*Z=-y zzeHSAMF@UYIpoLSMfUO&X&|l?%5*2z^=r^Yn&?BR0yKYBZ%Q;~o2v(=%H05A<%{DA zHm?}WjZM^PX|c~e1D^>1az;`{nEl3#pZrcJ=9h&4(1 zmUtxNk6B1qy&GxSem|2q6;1Hhu-+NEvEC>?+mfQx&88q<%>rj~-cc*HHA}#2j5onP zh-SSBmpXZ(RK-)0Ev3blI}%MVkq+TTQ;y-UVKAl6${LiNOx0&!83!*kn@Y*XCCxF= zm{I2r%J`-*hr967AYjD?-L@?}`-9DoV)ZSBRV8Q5S31XTb4+76(J;fsOlb#-FI&M% zj<>7Sdp3O^l)@Nco0uJmT4?+>@j=JkI3>hFHaMUc(ML&d>0L}*v-4&@t^uJVnO4c< z$AP_u6|p2r@cGSAv*1z&0aYTrr-w3tDJX1kkMRF%IsumPSM&)v1`N7ve)^XnkQ%CD z2tvsJl=KAH;?NZhmhKvZulAR~qj;$h24Z8rGfEF(%HsJ^Nwi?L3v@mQc_mBuugrF! zlVf4cRN)tp8ouVO!(shLH1+-KVLwqI#VI=w+krQ@-d-rcQx#mtm-54QFa9Wh#A8qJC~P1sObHJS*cWSbXrW*jG%i zqvn|TNrN+?vy*XmoEqpz=6h6tL7A@~R?Z+UT+ovZOvtvZfC8EZ5jK9QFMHqtDAi2ctW`w6mvK;V^le(5jqHiNeo zwJN4y3%EL>NUFegc-tTKivu(I^{u_G>xG_#FR$b2p^pZclo0}Dz1#CpEDsQmRJZ~A z)`VMx3dF)u0CtQ(Ef2&&c|z&Ve9!C-z3DM6yZi0N2TTD$GRXjltwuK3IE_w7&9W}> zuZ1Z6O=)X7D}z8e;O|4*3VCj4rWFg@(Ty%;LqbEYCKme^K!5bxI81-Tp83sT&?#X(WZDRlDi4>N`IKZkISW+$lRJjQ;#a?`Ar7Te9peDPASgrFLa-6zv1?0Z{E!1!^9!E8N zd)qU>K8J6@U?)XsL2|grc;E=vBJK$mh^a|&KMAOLs$fIyItv@<(yMu{G6ZB!eo$@{ z1+zMPjQ?1{fY7sW6ZS_j+p!X{Z$~Y=3eNkx?MZnZ=YL&pluTN}^4C~dV=j7pg-_~g zsta8Am->y}d>yaf>`kA%K#Fy@u3PINw>3yciHN5dpQuMr4%s4uFHl*?e|*}tqW(ca z;TRd(sQ1-NYRhFe>XEA^wGqQf>D5c@9o((&eX?HN%Zt76pQUWT0KZ^_|yAqr68AX-`i7YGw z&x#JPw0k}7467T^rW&TcW4bh~^r``(T*nV|x(3;%bcG&d)N%_;5fJhg-*#E?0kdGA z=&z=X&*W8T=WeUZx%)^)Lk)LP&77WLv3qA!+l-?wuM&hYeR<~^l#;~1AvR5O;rBCd z-JJvXT|kPax@YV$u0VknmQG;V&SwGjG@Kov`OqEvwuMow%~{Ed9akehBrxig-1(-p zb0^&EW@Af{g|dvfr88Zo=$J(}9um0h(esF4=dl=Z>(TkSuanCG!o1_N^XF`@?{tY{(+m(d;6_=_Fi167vZsHpu{b-fw(y@}ygFuA{i9@!cEUZ8kXj9H+72 zFry}7%4{>4MKd#=BzxLyexBq?QB5JautNMFo;sw|+YHcD`Snc_X#W|1u8b-P_MIv> z&<6w2h~d5$omyJvFFOhC>fq+$Mvc#Ofn~EJTE>9Tve4a{j@&ofu+XQ8x*zvi&COGf z43QDECI;+f&qfW*Chu{_k8yuu$;oCTl z`^%=JC$S5UIf}zyYqUnh1bFH0V+GI0QTiBNe@_t*;;$Dr`}j2)S`r;2B0#ZxQ&J;o zxz3|6e^_{5kMDdGFNIso{`?paqO0eTrPyH zLrN^d##8#4qW`*6`^4F}(cT$T?9a8St+-{qW*Fai0MtP++Ob*7|D_7 zWuTIzacEG>upq)SW_`9MBjPX9NNU0^=vZUXleFh)E23IboS(ku=+*z`$YTFZUrFYd zIo)O%f$o<6fMNc^wrEr6yp5W?^`>YU4e&sV)G6;xyXyX~2Y!gdXfV~Zppt?6(UXTi zuUm;opTM2lveEfx!_4Nc^mf>6IeVWuwA>1XNKXZj2pDcKw@(E0=mE=|LWp~1+WsEsH&o6 z?QQLaKcJqKx!*!h&Z76}otOaU^)M9$upRW5m+O52j~fQF zy&D5IIhw5ZNy$9J8*q+eKRn1TwD4A4oUg)?m3o=dwhI#brC4AOo0=6p^?$Ov-h5Vy$PFz=bPU&`w7ugR-ya_m^Rf-e`V8x{g zdz4A|-2T zOTG{*>Un7NeIIn~f~j07NY&pkQnsi>`Rr|h5%j)s_~K*MbDaGPSp>=vj}$o8_#dqdt{gCQ z=a)E+*i=v+BUXH(_{OcDYVqO02J3~8L9a1K1#IjU-|;KGuQA(H`-!V{Ov%P|B_o>x z;M9kf-FVK4D=+Mh@HF$#Q-?xRDEd8qxNEWI<2~zRUSoseBF9GySs<T22K#_*cd;$E#GnZV`(|n7vrXVS5Y7P2TWa~Js_2f zc}&$&;|^a@t!OYma+Y{$i+bn%N>*qRs(UT14S0(jX8q`J3Sm6@WJqd^rl#I>aP)ct zDGh_fz|X_xQa0T(alvGZ=fHW3ln=+5pz$S}GGPJ%VU+U0nn{PIC=}hAlKg&Xx~ZIS zv&CbJf`M+linKyV-WAJ|Sp~D?&sPdIR#t~nuEPV=nu|MZpBk4=8r;6jbT#KVU^#m-w(!Lu^#=i7~$OUOPv z(J#bB*gbQ}zK3_j1io0uKb5NvuShTJ%ZG-9GBOwuYdALhUZyvNCJxcZP|)E(oYh$K zWr531^*z(Za%fMUJRX7mG%@g$*$k4>j(M_*uJ?iWTd2;*F9~TO!J3NL!>|X*vDqv$ zxrYKalYk9Z#lJ-Nh~}*n+iF<_s8`43^9A;}DXlyc^b_rBKl8l`m_hWg%caz5`*D&- ztCsgSZXdAGNoUB}qK9gN+ymyRhn!n)g}v{$n$oN$08PmUk$-DV`cL+PKO|j;i@yJr z4F9}D@cSwj1Y+@fB#?kEU_x=s@>HsVk7~bW`J3tP*#R{WLPdYVD^2B)s*RLS8+0v@ zxP8O}0P?TiFeQ43Z#x~G%r7!Wrk}55Pb95@KNx^;%8R2KO?*fHd}TDFj`SrOAV5a% z%;8qq_@RC_dx`o6S=+<^sm$-LQ9iO%F~a~F^H{9d4yUVSIZBfNS3d%ubEFUz_^ZUQ zb)^CsUsmnKJcYw&xx{7FQO6&E*EL`nqFwbV|=1-_Zi9YZ`_x&kGsJNEyJPLi^C{G-kG)MN z2k0Uuglj&EdvFe1+uIr8St(|*o5|RBB3NT2C?-`vpTxB1Yrt-GQj8HRop1W)qOVqf zh<%?BW2j|DgWucu8-1xHAf*cId_{bas|kuVTo`i)HoErPD+TeZ-FK{I>f7fv>SkOK z&zLmCiC&h_H?f=aCU5~3w4FVqA#lPpg2?@wp0Lfm4Y=zouOM+7WRD?`E&${N??Vn4 zuLc4Ln!Wp0|m>2&@l0SabQ?|4a;XYOLsTWMc9T(g0Aac)fA3mLrUGT5s=V_ z2MxMHX{NY>8_nNZkZjYf46ouAwtWOTxV-?#n3|0pOYvk7qJdH0MG%cF!$7D>gPi<6 z80>8ydVeQR)t!`MB?fUYs1Z=b0j?e+E(Sj2tiPO3H8)D2h8>#rNBs0t>3t%_)lV^Q zrh$a~5c(4cL(LKCeL5p#QTgzjX34yiD?dL;@dfC8YfbY>(UhLY-u~U~e#>z-^V*D% zdTjkhsz-6l?WK3i-EPGQWQo!s6YqQqNQh-;z$ekH*&)Z(HI!p{dW4A0gterz(B3_E zZrY~=JGaexRi_ZyrOwWP>~Z{M&rrZ`p>W9^qjni5^Ivhs86S0ZBMtHHp;RgXjF!K- zd-660W*~mMwkpnhQsCR1toGE2U4zJ}QU3wxY2C$o@_zFba)7B}_nH3rhxOqZdT;20 z%hbt~{@rF~B;<958x&vueoMoC0HGcd*txK5w<2exV3}{3@=zHCIPbv;t7#cio?aEI zxOt8-G9pjw3?ON0nStkB<%BN_=o{FZ8>K2W*Nd7+&fc54(4L?h4gnEa>#h;NU2}Us zy*GLVcM<<9K>qZmh!h}1+rR{UnJrT59lZyu_r%~{--yUT;}!H$wGd54n#aE<}$fr>ua>X#odOaP2dty zXM1UN@ykiw4-QrTdYQoISi**@nGq~=4+MPYS&*)s&TmT`8 zXdU=29R;1B|6h+gGDX&2H>n;-9`Eb(gO#4(&G9_tTu930{DvSDIbUbO32bb@3|SM) z<3rZ22<%_=jl5|(C6$KW4iMQ?sw4B<}ck5rDm3Xev%UA(5gcwgIMCtwx z>yErW_i4K6*$UvgBGRug;Tj3qQzBD(*d_O3Ik$)#IIIoMDU zML`h|LFq+7DH7s|C<;OZkxrtbpp?*C5)xDp6cLdoQlcP&gpPpJpa>|%P=rVakrF~r zs3FOnobQ}(t=rc9bH26iUF-hLN@m`9_srhUo|)&_0W`SZ3!ljOHj4YZnh&K9v80Yx z_@&HNzeJaZsO*7yKh$IF4PrgiB>Wr{yyP>|lBkYiPgy3kNdV!O+yTzaqot*#m8v<} zrMVzMA$zh3Y#~LKJH^2gCT2U92QoO6epXB-u?n3Mevv#{y~6&E5ZB)84n(K>UcR4P z9+$qWUXt-FXcI)PAk^TUe+?rgO1wHA9l$7|%it$2&9F-nh!Q9*|JhIF-m1jI+!2-w zc*20D__2zS)+q7vWnjM>aX0?ZUhuR8isUkZsV}8;jpT)S_!2+l)#L$*40O%f_=J)F z-u7zCP9it^vWQ9uK4|00i_C-+YYOr0$0cvrH{?FAoWyOmt{ePm2X?#7egqxetEhCKqK| zCEh*PDFRdmgKV`Pxjp6TM^>Dc3(>V6_yhg-50q(X(u}SjY;!;VigR{=lgh!5*s}*@ z`;ybyyA6Bad>%G=0#8d9J}d(XzHL2-T0(vqJhoj`tkkW~FD&-qtZRoD00W@%0_^0$ zug%8Jo`8sPad7w06<)!FL6}wJg%^KnEM763Q}`~Rs&1eC2O`L{K5KkTRukQ}8{lB< z{7r|dcf7g@-zw7J_auO(>)hi`vERkl&Uo%hX&*YA{u`vg1H3m_krlL1r$>{CR9chm zht=Jd1D}G}JB8YnGZ9eMbaBqf4T{(D%a3=TgAryP>d);u{RpTss#(x-mLI4>u$3cz zrdvSbb!0^CRD^A%$OlkqVHe9}o51q|xai&EaO6Pr1?IdW@rNraR$DcQ>4jv}Mt_q?N>k@bElI?kI4bj;525 z_v#ZgWs5fh>4gOj_Gif7`OWr6Bj%&D&Q)tsvT96keYIon%a`W)m_o(Qw!}xCF@Z-A zw?g6m6;Fh=K&+mFbw~-C^`6t?Dq}cAa?n$Wpt=SM{Q(KBoGT8k@MBJPmBs6=KH@HX zDeOaO88G|RVK`17kv;9y*#wS}+-GL?xW%V%W59cGsW@Zds(}rbwyMALgMIgs&ai%D zFj#v(fg>?h3v>Eln1Qk8>9O5BA;`}NAD;03i!D?m@?KoUGZD#SI2C>fsbHQw1@)gz zUJ&83*>l?5M@?Zhf$^Nhmwj|eZi{llULh!3hg~1R{A7JJjfnr*8Ks+z`cS*Ue4Mb$ z_Bf9ToZ+@N@t+~N7I)__9lgkEOXhx?td7J+72_P$kCEugrH3C*oOG=moqZeg22{2% z?drK;e(L?eXGC*y-GK9*ERV=G)oB|<&ilUo8LpMAopDyyiW0{l5b9qPx}vb`>={2S zv8x|oiaXnXj{SM1N_I6whRWb>bfVZKRc;pwzt)TzG}y%>6@9K-|N4lhK$nTt=WK^~ z9J$?)04Ts!ZzfiQXhtX<@9`EcW-Yog$AxUVUgPvsTsF z~vv$Y^0bb(42+XRsTKUU=-A=1d{2G>+9>$(gsftC*Dr;;%tDYQHW5i z-)!zpc&trnNlA%j%|fK5oo9V}x&gJj+hX7_L@P_h?t=%w083=x7?aisT2KhX*DURQ zTTfB7?m0iir0nrsXVSpew;$GJhRT(ggZ|4o>+~oI==u~XKlO}1x~p|l@PnDj_H5bhpOC-zhVT9K@I1mZEN?fp(MziRPEp@VAGJbau(r_-r+ z-mP{Pp55KuPsPvpjkhG8S7j@!8V>jm1+5T<`BD(Dk2T0)g)i=nhwBLU}?s* zHbe7x{1uy<$`N_N;j@)M>{c)@=i97YrZqI#%xuym za`Wca^WH=MpyVa#fAl@;?{$oKrS#qOjind+o^+>$K`Z z`5?gu#lt677mC0Ymz)2^yC3!)tR7LR+(j~0t)2h#UgmLL{UzH|i%xnu4$>`tq2ShR zQswmNpF?`F?@iwM|C}c%3PG-C0NhOt98VWCV zC~%1mBzLJWO~(VE+^8kVvxvyAtih2gk(e{BVUVLd32-Ys{VRwqBH;BI#o?ffrNA2TxFS{|c%ZNvOyRYay z?cg0#`pZ*9Xa^8DNU~cSInyF&Cr*JOz(}WW$!n{`jRn<>IdW=^EkjyR?+v>wB=Z=i z+4F+iuu?Hi79#^7+&!W3-dY=vvk1x5m3EeRJe}XA`C46Ig+^{xXpL3=r_{fT73Mi zK#D&1d#2tJ4MA8xC%N9nCNGn|4V@2S(@|wgbf?*b_%_P@fU!7BUOmWLvyfozijBo) zFqko2tj_kGs?{3wh|O!sQ0(%R^YTY^u*=g1AhWNJ&DH$CH{WQKi6A0TDDq;HA__Zm zO82lhWSIsWnD;h`>P|@9LcK2QjJ98XDlcCLes-1i?x0r9*ZCYMnXl9t4k=#>Yy~_?Bgh$ zj5MHg@M14WcQprmI$i0$A}V&P4VpEDA=a^crp~t=YrxI_GHtr2n4-&Q$+nGdz8CP6 zci#TyUqSBv-`{7q z;pPGUu)cEdiTOnO$UoVB!>jE>xM>Q0gzG+shl^>m_@2U+vx=&_b==IV2+rqS>Z+=) zbRNcRdwr#^{NkRkz1e?-Ts<-;QP6)=Fg5OLxl~04^@u1aBP>1 zRMM)M!1TqRz6Iw`uOi%Bl&bq4aIBtl-A+Frzp=r#ON*0i-AJy*2TI77_F{4xyyD|J zQW8HUit#;9GpjuUz5Q^3ECp5xODU`dp`map!Y(reE$%Vz&;wrSXi*{q}DHo9rGD zp__cJ6@P`xMMtmq>b_$A z`3+}CUOjThWMSNH*=C{Q6}8!H@eajB0n??e@x6p8SCU5S7mF}8)nN=6R6^8>_^7>k zuS&$QA<4m6>5!6MvkpW|X9QcSNu4BCvlqgsIaao| zwkVWj-&xVLv9YlS5BM@OAs1ZGXtaYv9Zh~-2R&^x7K6}U07WxO_n-H~?6@8xe1610 zp;9c)t2jM9{oOlxUW+gEB?$=$9Vq{WaKrMR!NF9wK6Q2V>8xSB$7x`lPE1edSdg@# z0vEy&h!583?OEmu3D{?2K{iGcYi~VzH4I>S)$#aPy{CT?q>CbX^HVH4+Ae`eF`W ze<6In9J?~hWcp;7NtvrOJR&9yRQe-KV@F3v9gAs9rP^nwl3HV8_T^L0z+iJsB~dAb zF~{QK;!?LWP<}P|5@uk6q5Tg_CQ|9QMfm+)PA64OOrigL>GzZJiXvBSqq?lV6`8HC z%HE-U+Z1fL=>aqLpn|B4F08vUyRhErC0NVxN(QNI@~=V>X?#K7tyH9*+f-=GiY0WU zV9{};(HiX|%FAYFqq=zQCMvTA9coh>ot3D1$@EkKzbHCJaHc}F{c1|a1U*mJ)NaRW zbDu-L%n?}yexEhz=<6rEM^76f5~ID$fTySz-P6}}=*nr=D^gs}b$6%O7=*6{I2o&9 zXoab%hQAe!2Oz{Y1%jpePuB^>8jeN1uy|AaN-5-u2w&QZySLxOeiN2fG=kgNm$vP@ zD*VJ_Zdpl4muv(4T>Tusv@VBn;j9H%zY?}IkD|jTu!5s`y;9Xj$$PF5uD@evek(QX zd@??-OJ5j|c*94;KF142rn$@C| z-M349?|MciyhiJTeDU<=vR$(H+E->e$vdqaO+-g#nrl*{#3Hmii}u39ol7Uw<`iZ` zLW(Z@Xt`kA`8eFU@CP};OoZ>~i#zu3gY2L4Z#(h--t|8?;r|)$3fOwZL7Q~D-+znD zg^b|eF-<%7?^I5eoO4SkRp)tF^e&l6;WqGwpKw}Hd;3?7SP-u;*Dv$QRu5a-$@& z_@D#w&c`c;!sn~#kNv7~0ffq#cQa4IVOF{a5mfT~niEk+&6x-yE z^Ww4|bi^_8Lb}8DAT2~-1|kj1?b6$S^z;b9UD0SA zr~qW+Rx&th|C{0H?*+GiX>YnFg2C-(!ebmZ3s@4FPo-H0-zaipO-|l%ch7IXqs7Yt z49gQ6M+2FY(T$_;sPxRtOp_?SoiS;0{y-p~nwn>>5YkkywcuS0fK>jm7Y^6&dO9VQ zU$n@vIMB=M=MrXOVgh`Y*3am7_4Dg4b>rnAS;%}AP7f@EZKm>?3{3TO+AzA+F=oXM zOwuOE`@Q3n5Xcrw-hTxAs?U@imV@3tc0V_)D@VvAAz6HDu^8ML$N5=|qo`+OhB{w2 zy#`{1fuRVd$$6%TX-u{I*P?LdsJs@*nWL=Bh!&f=AX7Uo<9dLwo zyB8}`A?W-zsXuW2WxNP{622;JdgG4GrO2@s*}_a}RmL?#6#kFdhSsYlKSHHeIeKo_F8dN=P@aL z`4o9S)n8FBEN5U_<_4?-zkd3NEm=8|5*29Y^Ywm4Fu7_VN!0hm5glw}O`>Xg-ouUh zmbgclfe6%cr0|ZhWo;5Vq$zZ9SaI*sn1ca(MM2UL7Mpq?`(xaoeirHVJu;567Mg`z zu*|Ozoz%-Kg+)XwZAqsRO-n41r3GAMid)>ip=D#wJ02)#OR&W-~c=(q? z4-(u^9ft24DmhqX-gcl%+?4Km2j5V?bz-{O`mO6ReY1URZi7x(c5uvb{qf>57-#esw$j^$1qbky^TA<-;eh8_7)QpGeO*=dODyKh`1(@$@Y}y%02G9 z3Xy&zw*zL@GmsUVBrYy)Y-|h?Ry8;>e`S8YjV7-KuTmXz1d+9~vlDCsO5G~yVd?%H zk>mY0AE#Y5HU=ebNL|VQ@ZX&FJLU>;Yiof-N?Ad{FjK2mZDw|Mc5aU9d= zLJt>!!C*#1FJJFLyfY>j|-nS4cQaJWe_8KG@gXwV(=+;nF)b z3l>2Of>GQ_1b(#{g|Ol3u_*-EkQXmr+?mTj*5Kqr0@k)hsB%{ks4e)lzFO7KbDWww zX+8)#@)27y*6`9hH0K+Go7%sYckP(eXD;6|!d7R9-~|N+gIqdqqMrx9e*fP9Bf`4b literal 0 HcmV?d00001 diff --git a/_resources/main.ico b/_resources/main.ico new file mode 100644 index 0000000000000000000000000000000000000000..e2713393f91790acfc88361df19b93e1866421d3 GIT binary patch literal 52014 zcmeI5O^g&p6vvyGz!C2p+=~fEPF@TP(Io=A#Do=Lmj#wAN`gQ%LIR2hg0ib7qKP41 z{5%|Hy?d+>&0Zh1DDfw#jzzktu<64I?2yh)#$I6?W zbD_o7A)5uPMU*+riJ2HH9kn&(*t!<+TIw9rYr}O4Lrx6l<@WFIt0HWn z{9!sAwv=2^zZqd)X zSZ1li`b%NRiNSP>em)ChR}R)+4qHktrt9(=vM!WAOozgd6NBl7WbD!kq7a%bSMltF_>;h#xAW;{xBU5TS_jb>tYhJE|fn^hr*B(gXxB3?9vM557XhW zrQ~9|E+!%CLixjVC=594s$N(hB7d z)8VkCq7a%bSMltF_>;h#xAW;{xBU5TS_jb>tYhJE|fn^hr*B( zgXxB3?9vM557XhWrQ~9|az5YhUkHEUUxYb8SwlE6Md)LZJ#g4ka*N#eB6y=P@#=wp#R zP#AJzTzy~r;u5v=vn{Ig&|tiEfBLFg`$8A`ywv|vuUc6CLCyW^pLknTI;_@g=t1K_ zd(Y5zb@j?+RjpQY*{=M3DfWPI;IO6SV!Hhso>g7LgKF_l?dA4H*xJuscaA}x?9G_N7+@SI3^_5F?$R%|Cb-+<0M|h}@0mltB)Hq-0M|h}-jof4 zy!XaWde31?$xZ8-a_9F$DE}_K9+^0^pL^k7lz;pATldAAyu7&^ed8y+r!eHir1ebM z#UJhmCeG|#{Na9J;?jH5|Gf8UU#84qOUX4l>3#R-ThRFr_vCc^qTwIW`49Kxbi66& z&E4o5Kj}S%AtxrSXUg#G)1~jV_PP(pa8HIh6KD4D?9+bk#(iS|_hhIuap}F>fARnS z2F`FDz*+_zCD9zB6fiqkOuol4v_hfi3GWE6gAH4Ir8o!#~^CyfyJOkJA2In|) zo02=wyWl3K-RkFo4TT{mW?gT`(N^b-^?#seLDPSKUXSr+OuyC75A<|2{oh~Mv~b~! zbv+%&Tb*^f|C;?VV8dBksJZJ`KQ&9n;NEixum>hoevOEOv4*v1pts|Q9*bM)`S8$} zb)VzDuXuV>sCnOrag#QLfDjM@LO=)z0U;m+gn$qb0zyCt2mv7=1i}(fMfAOwWKgF!%@ z*f)KmQXZ;S$`|wcUTCbXJ%E>bse$*reAmz&tk-{@+|&N6mC}WJ{o8H0epiKBm!&;B zNBr~t4N;qAmN66l!r#oH(DTRWHfk^a{a16NSl4PNG=Hj|E6(nRE6%R7bNs}U5q7TK zX4c~@?b$g=cm9%{o7JEbZAje#glO zJJcp``dw$2s2-^#IH{ZX8KA1Z2Zhl__`e4q(-h7{&v$xwzy7QOp z+y~wNs_ngc{_Kk95X>dhw-s;gQ1j1@$8!kgk~t6i(7Hg;0d8$6NOVNjMucd>Xqv}Ju z6#XLf-;7s1`$mpG=+?XW^r1Zpe;I$7|Jb!&>-@ub{nzRzcCH!ykK2U%3S~QUU~;V( z`38@lT2H9ge?9A7JcV1L|^V^`{0hqw=Y{NTg@Brn#dv@0K9@KE9 z>UV$qO8onzzyWd>A1KEjfCH4@8{DRDT)#&00KY*=)z|O!Moes@_xhdPsIUL4mUs4P i52H=c9_d@&<$eD4`IGvGi$MClg8tg(%w}$Bz5f9%%-|jX literal 0 HcmV?d00001 diff --git a/_resources/main.qrc b/_resources/main.qrc new file mode 100644 index 0000000..f13e3d1 --- /dev/null +++ b/_resources/main.qrc @@ -0,0 +1,6 @@ + + + images/main_icon.png + images/main_title.png + + diff --git a/_resources/main_icon.rc b/_resources/main_icon.rc new file mode 100644 index 0000000..59f679d --- /dev/null +++ b/_resources/main_icon.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON "main.ico" diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt new file mode 100644 index 0000000..d768079 --- /dev/null +++ b/application/CMakeLists.txt @@ -0,0 +1,81 @@ + +cmake_minimum_required(VERSION 4.0) + +project(logger_example + VERSION 1.0 + DESCRIPTION "Example use Logger lib" + LANGUAGES CXX +) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../_cmake/app_settings.cmake) + +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS + Core Widgets +) + +set(PROJECT_SOURCES + main.cpp + main_window.cpp + main_window.h + main_window.ui + ${COMMON_SOURCES_DIR}/application_config.h + ${COMMON_SOURCES_DIR}/application_config.cpp +) + +set(PROJECT_RESOURCES + ${RESOURCES_DIR}/main.qrc +) + +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(PROJECT_RC_FILES + ${RESOURCES_DIR}/main_icon.rc + ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc + ) +endif() + +if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) + qt_add_executable(${PROJECT_NAME} + MANUAL_FINALIZATION + ${PROJECT_SOURCES} + ${PROJECT_RESOURCES} + ${PROJECT_RC_FILES} + ) +else() + add_executable(${PROJECT_NAME} + ${PROJECT_SOURCES} + ${PROJECT_RESOURCES} + ${PROJECT_RC_FILES} + ) +endif() + +target_link_directories(${PROJECT_NAME} PRIVATE + ${DISTRIB_DIR} +) + +include(${CMAKE_INC_DIR}/target_options.cmake) + +target_link_libraries(${PROJECT_NAME} PRIVATE + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Widgets + _logger +) + +target_include_directories(${PROJECT_NAME} PRIVATE + ${SYSTEM_INCLUDE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${COMMON_SOURCES_DIR} + ${ROOT_PROJECT_DIR}lib_logger +) + +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set_target_properties(${PROJECT_NAME} PROPERTIES + WIN32_EXECUTABLE TRUE + ) +endif() + +if(QT_VERSION_MAJOR EQUAL 6) + qt_finalize_executable(${PROJECT_NAME}) +endif() + +include(${CMAKE_INC_DIR}/post_build.cmake) + diff --git a/application/main.cpp b/application/main.cpp new file mode 100644 index 0000000..a21b5bc --- /dev/null +++ b/application/main.cpp @@ -0,0 +1,26 @@ +#include "main_window.h" +#include +#include "application_config.h" +#include "logger.h" + +//============================================================================== +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + + Application::initialize(); + Application::installTranslations(); + + // Создание главного окна: + MainWindow w; + + // Запуск логгера: + Logger::instance().start(); + + w.show(); + a.setQuitOnLastWindowClosed(true); + + return a.exec(); +} +//============================================================================== + diff --git a/application/main_window.cpp b/application/main_window.cpp new file mode 100644 index 0000000..c2edd0c --- /dev/null +++ b/application/main_window.cpp @@ -0,0 +1,163 @@ +/**************************************************************************** +** Copyright (c) 2025 Evgeny Teterin (nayk) +** All right reserved. +** +** Permission is hereby granted, free of charge, to any person obtaining +** a copy of this software and associated documentation files (the +** "Software"), to deal in the Software without restriction, including +** without limitation the rights to use, copy, modify, merge, publish, +** distribute, sublicense, and/or sell copies of the Software, and to +** permit persons to whom the Software is furnished to do so, subject to +** the following conditions: +** +** The above copyright notice and this permission notice shall be +** included in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +** LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +** OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +** WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +** +****************************************************************************/ +#include "main_window.h" +#include "./ui_main_window.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "logger.h" + +//============================================================================== +Q_LOGGING_CATEGORY(mainWnd, "MainWindow") + +//============================================================================== +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + initialize(); +} +//============================================================================== +MainWindow::~MainWindow() +{ + delete ui; +} +//============================================================================== +void MainWindow::initialize() +{ + setWindowTitle( QApplication::applicationName() ); + + connect(ui->radioButton1, &QRadioButton::toggled, this, &MainWindow::radioButtonToggled); + connect(ui->lineEdit1, &QLineEdit::editingFinished, this, &MainWindow::lineEditFinishEditing); + connect(ui->toolButton1, &QToolButton::clicked, this, &MainWindow::buttonClick); + connect(ui->pushButton1, &QPushButton::clicked, this, &MainWindow::buttonClick); + connect(ui->pushButton2, &QPushButton::clicked, this, &MainWindow::buttonClick); + connect(ui->pushButton2, &QPushButton::clicked, this, &MainWindow::buttonClick); + connect(ui->pushButtonOpen, &QPushButton::clicked, this, &MainWindow::buttonOpenClick); +} +//============================================================================== +void MainWindow::radioButtonToggled(bool checked) +{ +#ifndef QT_NO_DEBUG + LOG_FUNCTION(); +#endif + + qCInfo(mainWnd) << "Переключение радиобаттона"; + + QAbstractButton *button = qobject_cast(sender()); + + if (button) { + + qDebug() << "objectName:" << button->objectName() + << "text: " << button->text() + << "isChecked:" << checked; + } + else { + + qCritical() << "Указатель не найден"; + return; + } +} +//============================================================================== +void MainWindow::lineEditFinishEditing() +{ +#ifndef QT_NO_DEBUG + LOG_FUNCTION(); +#endif + + qCInfo(mainWnd) << "Окончание редактирования текстового поля"; + + QLineEdit *lineEdit = qobject_cast(sender()); + + if (lineEdit) { + + qDebug() << "objectName:" << lineEdit->objectName() + << "text: " << lineEdit->text(); + } + else { + + qCritical() << "Указатель не найден"; + return; + } +} +//============================================================================== +void MainWindow::buttonClick() +{ +#ifndef QT_NO_DEBUG + LOG_FUNCTION(); +#endif + + qCInfo(mainWnd) << "Нажатие кнопки"; + + QAbstractButton *button = qobject_cast(sender()); + + if (button) { + + qDebug() << "objectName:" << button->objectName() + << "text: " << button->text(); + } + else { + + qCritical() << "Указатель не найден"; + } +} +//============================================================================== +void MainWindow::buttonOpenClick() +{ +#ifndef QT_NO_DEBUG + LOG_FUNCTION(); +#endif + + qCInfo(mainWnd) << "Нажатие кнопки открытия каталога логов"; + + QString path {Logger::logDirectory()}; + QDir dir(path); + + if (!dir.exists()) { + + qWarning() << "Каталог не найден:" << path; + QMessageBox::critical(this, tr("Ошибка"), tr("Каталог не найден!")); + return; + } + + bool success = QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + + if (!success) { + + qWarning() << "Не удалось открыть каталог:" << path; + QMessageBox::critical(this, tr("Ошибка"), tr("Не удалось открыть каталог!")); + } +} +//============================================================================== diff --git a/application/main_window.h b/application/main_window.h new file mode 100644 index 0000000..35a6722 --- /dev/null +++ b/application/main_window.h @@ -0,0 +1,63 @@ +/**************************************************************************** +** Copyright (c) 2025 Evgeny Teterin (nayk) +** All right reserved. +** +** Permission is hereby granted, free of charge, to any person obtaining +** a copy of this software and associated documentation files (the +** "Software"), to deal in the Software without restriction, including +** without limitation the rights to use, copy, modify, merge, publish, +** distribute, sublicense, and/or sell copies of the Software, and to +** permit persons to whom the Software is furnished to do so, subject to +** the following conditions: +** +** The above copyright notice and this permission notice shall be +** included in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +** LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +** OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +** WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +** +****************************************************************************/ +#pragma once +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include + +//============================================================================== +QT_BEGIN_NAMESPACE +namespace Ui { +class MainWindow; +} + +QT_END_NAMESPACE + +//============================================================================== +// Главное окно +//============================================================================== +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + Ui::MainWindow *ui; + void initialize(); + +private slots: + void radioButtonToggled(bool checked); + void lineEditFinishEditing(); + void buttonClick(); + void buttonOpenClick(); +}; + +#endif // MAINWINDOW_H +//============================================================================== diff --git a/application/main_window.ui b/application/main_window.ui new file mode 100644 index 0000000..53eecd0 --- /dev/null +++ b/application/main_window.ui @@ -0,0 +1,197 @@ + + + MainWindow + + + + 0 + 0 + 565 + 660 + + + + MainWindow + + + + :/images/main_icon.png:/images/main_icon.png + + + + + 20 + + + + + <html><head/><body><p align="justify">Приложение использует библиотеку <span style=" font-weight:700;">lib_logger</span> для сохранения лога в файл. Логирование событий осуществляется через стандартный механизм Qt:</p><p>qInfo() &lt;&lt; &quot;Строка информации&quot;;</p><p>qDebug() &lt;&lt; &quot;Строка отладки&quot;;</p><p>qWarning() &lt;&lt; &quot;Строка предупреждения&quot;;</p><p>qCritical() &lt;&lt; &quot;Строка ошибки&quot;;</p><p align="justify">Строки отладки (qDebug, qCDebug) сохраняются только если приложение запущено с параметром /debug или в каталоге ПО находится файл <span style=" font-weight:700;">debug</span> (без расширения с любым содержанием).</p><p align="justify">Логи сохраняются в каталоге профиля приложения. Если в каталоге ПО находится файл <span style=" font-weight:700;">portable</span> (без расширения с любым содержанием), то каталог логов будет создан в подкаталоге <span style=" font-weight:700;">profile</span> каталога ПО.</p></body></html> + + + true + + + + + + + Пример логирования событий + + + + 40 + + + 20 + + + 20 + + + 20 + + + 20 + + + + + 10 + + + + + Вариант 1 + + + true + + + buttonGroup + + + + + + + Ввод текста: + + + + + + + Вариант 2 + + + buttonGroup + + + + + + + + + + + 30 + 24 + + + + ... + + + + + + + + + 20 + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Кнопка 1 + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Кнопка 2 + + + + + + + Кнопка 3 + + + + + + + Qt::Orientation::Vertical + + + + 20 + 28 + + + + + + + + Открыть каталог с логами + + + + + + + + + + + + + + + + + + + + diff --git a/lib_logger/CMakeLists.txt b/lib_logger/CMakeLists.txt new file mode 100644 index 0000000..1caddf7 --- /dev/null +++ b/lib_logger/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 4.0) + +project(_logger + VERSION 1.0 + DESCRIPTION "Logger library for Qt projects" + LANGUAGES CXX) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../_cmake/lib_settings.cmake) + +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS + Core +) + +set(PUBLIC_LIBRARY_HEADERS + logger.h +) + +set(PROJECT_SOURCES + private/logger.cpp + private/log_worker.h + ${COMMON_SOURCES_DIR}/application_config.h + ${COMMON_SOURCES_DIR}/application_config.cpp +) + +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(PROJECT_RC_FILES + ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc + ) +endif() + +add_library(${PROJECT_NAME} SHARED + ${PUBLIC_LIBRARY_HEADERS} + ${PROJECT_SOURCES} + ${PROJECT_RC_FILES} +) + +include(${CMAKE_INC_DIR}/target_options.cmake) + +target_include_directories(${PROJECT_NAME} PRIVATE + ${SYSTEM_INCLUDE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${COMMON_SOURCES_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/private +) + +target_link_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} +) + +target_link_libraries(${PROJECT_NAME} PRIVATE + Qt${QT_VERSION_MAJOR}::Core +) + +target_compile_definitions(${PROJECT_NAME} PRIVATE + LIB_LOGGER +) + diff --git a/lib_logger/logger.h b/lib_logger/logger.h new file mode 100644 index 0000000..55519e2 --- /dev/null +++ b/lib_logger/logger.h @@ -0,0 +1,107 @@ +/**************************************************************************** +** Copyright (c) 2025 Evgeny Teterin (nayk) +** All right reserved. +** +** Permission is hereby granted, free of charge, to any person obtaining +** a copy of this software and associated documentation files (the +** "Software"), to deal in the Software without restriction, including +** without limitation the rights to use, copy, modify, merge, publish, +** distribute, sublicense, and/or sell copies of the Software, and to +** permit persons to whom the Software is furnished to do so, subject to +** the following conditions: +** +** The above copyright notice and this permission notice shall be +** included in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +** LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +** OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +** WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +** +****************************************************************************/ +#pragma once +#ifndef LOGGER_H +#define LOGGER_H + +#if defined (LIB_LOGGER) +# define LOGGER_EXPORT Q_DECL_EXPORT +#else +# define LOGGER_EXPORT Q_DECL_IMPORT +#endif + +#include +#include +#include +#include +#include + +class LogWorker; + +//============================================================================== +class LOGGER_EXPORT Logger : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(Logger) + +public: + enum class LogType: quint8 { + LogInfo = 0, // Информация + LogWarning = 1, // Предупреждение + LogError = 2, // Ошибка + LogInput = 3, // Входящие данные + LogOutput = 4, // Исходящие данные + LogDebug = 5 // Отладочная информация + }; + Q_ENUM(LogType) + + static bool isRunning(); + static Logger& instance(); + static QString logTypeToString(LogType logType); + static QString logTypeToString(quint8 logType); + static LogType stringToLogType(const QString &stringType); + static LogType qtMsgToLogType(QtMsgType type); + static const QLoggingCategory &inputData(); + static const QLoggingCategory &outputData(); + static QString logDirectory(); + +public slots: + void start(); + void stop(); + void writeLogQtType(QtMsgType type, const QMessageLogContext &context, const QString &msg); + void writeLog(const QString &msg, LogType type = LogType::LogInfo, const QString &objectName = QString()); + +private: + const int maximumLogCount {100}; + + LogWorker *logWorker {nullptr}; + QThread *workThread {nullptr}; + explicit Logger(QObject *parent = nullptr); + virtual ~Logger(); +}; +//============================================================================== +class LOGGER_EXPORT FunctionLogger +{ +public: + explicit FunctionLogger(const char *file, + int line, + const char *name, + const QLoggingCategory &category = QLoggingCategory("Function")); + ~FunctionLogger(); +private: + static std::atomic counter; + size_t id; + QString functionName; + QString logCategory; +}; + +//============================================================================== +#define LOG_FUNCTION(...) FunctionLogger funcLogger(__FILE__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, __VA_ARGS__)) + +//============================================================================== + +Q_DECLARE_METATYPE(Logger::LogType) + +#endif // LOGGER_H diff --git a/lib_logger/private/log_worker.h b/lib_logger/private/log_worker.h new file mode 100644 index 0000000..7ab28c6 --- /dev/null +++ b/lib_logger/private/log_worker.h @@ -0,0 +1,85 @@ +/**************************************************************************** +** Copyright (c) 2025 Evgeny Teterin (nayk) +** All right reserved. +** +** Permission is hereby granted, free of charge, to any person obtaining +** a copy of this software and associated documentation files (the +** "Software"), to deal in the Software without restriction, including +** without limitation the rights to use, copy, modify, merge, publish, +** distribute, sublicense, and/or sell copies of the Software, and to +** permit persons to whom the Software is furnished to do so, subject to +** the following conditions: +** +** The above copyright notice and this permission notice shall be +** included in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +** LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +** OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +** WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +** +****************************************************************************/ +#pragma once +#ifndef LOG_WORKER_H +#define LOG_WORKER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +//============================================================================== +class LogWorker : public QObject +{ + Q_OBJECT +public: + explicit LogWorker(const QString &logFilePath, QObject *parent = nullptr); + virtual ~LogWorker(); + +signals: + void started(); + void stopped(); + +public slots: + Q_INVOKABLE void start(); + Q_INVOKABLE void stop(); + Q_INVOKABLE void writeLog(const QString &msg, + int type = 0, + const QString &objectName = QString()); +private slots: + void checkQueue(); + +private: + const int queueIntervalMs {5000}; + struct LogRecord { + int type {0}; + QString msg; + QString objectName; + QDateTime dateTime; + }; + std::atomic running {false}; + qint64 linesCounter {0}; + qint64 linesPosition {0}; + QFile file; + QMutex mutex; + QDateTime startTime; + QQueue queue; + QTimer *timer {nullptr}; + // + void writeFirstLines(); + void writeLastLines(); + void writeLine(const QString &msg, int type = 0, + const QString &objectName = QString(), + const QDateTime &dateTime = QDateTime::currentDateTime()); + void writeAllFromQueue(); +}; +//============================================================================== + +#endif // LOG_WORKER_H diff --git a/lib_logger/private/logger.cpp b/lib_logger/private/logger.cpp new file mode 100644 index 0000000..4c13157 --- /dev/null +++ b/lib_logger/private/logger.cpp @@ -0,0 +1,605 @@ +/**************************************************************************** +** Copyright (c) 2025 Evgeny Teterin (nayk) +** All right reserved. +** +** Permission is hereby granted, free of charge, to any person obtaining +** a copy of this software and associated documentation files (the +** "Software"), to deal in the Software without restriction, including +** without limitation the rights to use, copy, modify, merge, publish, +** distribute, sublicense, and/or sell copies of the Software, and to +** permit persons to whom the Software is furnished to do so, subject to +** the following conditions: +** +** The above copyright notice and this permission notice shall be +** included in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +** LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +** OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +** WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +** +****************************************************************************/ +#include "logger.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "log_worker.h" +#include "application_config.h" + +//============================================================================== +std::atomic FunctionLogger::counter {0}; + +//============================================================================== +void qtLogMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + if ((type == QtWarningMsg) || (type == QtCriticalMsg) || (type == QtFatalMsg)) { + + std::cerr << msg.toLocal8Bit().constData() << std::endl; + } + + Logger::instance().writeLogQtType(type, context, msg); +} + +//============================================================================== +// +// Logger - general class +// +//============================================================================== +bool Logger::isRunning() +{ + QCoreApplication *application = QCoreApplication::instance(); + + return application + && !application->property("loggerRun").isNull() + && application->property("loggerRun").toBool(); +} +//============================================================================== +Logger &Logger::instance() +{ + static Logger loggerInstance; + return loggerInstance; +} +//============================================================================== +QString Logger::logTypeToString(LogType logType) +{ + switch (logType) { + case LogType::LogInfo: + return "[inf]"; + case LogType::LogWarning: + return "[wrn]"; + case LogType::LogError: + return "[err]"; + case LogType::LogInput: + return "[<<<]"; + case LogType::LogOutput: + return "[>>>]"; + case LogType::LogDebug: + return "[dbg]"; + default: + break; + } + + return "[inf]"; +} +//============================================================================== +QString Logger::logTypeToString(quint8 logType) +{ + QMetaEnum metaEnum = QMetaEnum::fromType(); + const char* name = metaEnum.valueToKey(logType); + + if (!name) { + + return "[inf]"; + } + + return logTypeToString( static_cast(logType) ); +} +//============================================================================== +Logger::LogType Logger::qtMsgToLogType(QtMsgType type) +{ + switch (type) { + case QtDebugMsg: + return Logger::LogType::LogDebug; + case QtInfoMsg: + return Logger::LogType::LogInfo; + case QtWarningMsg: + return Logger::LogType::LogWarning; + case QtCriticalMsg: + return Logger::LogType::LogError; + case QtFatalMsg: + return Logger::LogType::LogError; + } + + return Logger::LogType::LogInfo; +} +//============================================================================== +const QLoggingCategory &Logger::inputData() +{ + static const QLoggingCategory category("input"); + return category; +} +//============================================================================== +const QLoggingCategory &Logger::outputData() +{ + static const QLoggingCategory category("output"); + return category; +} +//============================================================================== +QString Logger::logDirectory() +{ + return Application::applicationProfilePath() + "log/"; +} +//============================================================================== +void Logger::start() +{ + if (logWorker || workThread) return; + + QString logDir { logDirectory() }; + QDir logPath {logDir}; + + if (!logPath.mkpath(logDir)) { + + qCritical() << "Error make path: " << logDir; + return; + } + + QDateTime startDateTime {QDateTime::currentDateTime()}; + + QString logFileName {startDateTime.toString("yyMMdd_HHmmss_zzz") + ".log"}; + + logWorker = new LogWorker(logDir + logFileName, nullptr); + workThread = new QThread(this); + logWorker->moveToThread(workThread); + + connect(workThread, &QThread::started, logWorker, &LogWorker::start, Qt::QueuedConnection); + connect(logWorker, &LogWorker::stopped, workThread, &QThread::quit); + + workThread->start(); + + qInstallMessageHandler( qtLogMessageOutput ); + + writeLog(tr("Удаление старых лог файлов из '%1', " + "количество сохраняемых лог файлов: %2") + .arg(logDir).arg(maximumLogCount)); + QDir dir(logDir); + QStringList list = dir.entryList(QStringList() << "*.log", + QDir::Files | QDir::Writable, + QDir::Name); + writeLog(tr("Найдено лог файлов: %1").arg(list.size()), LogType::LogDebug); + + while (list.size() > maximumLogCount) { + + QString fileName = list.takeFirst(); + + if (QFile::remove(logDir + fileName)) { + + writeLog(tr("Удаление файла '%1'").arg(fileName), LogType::LogDebug); + } + else { + + writeLog(tr("Ошибка при удалении файла '%1'").arg(fileName), LogType::LogError); + } + } +} +//============================================================================== +void Logger::stop() +{ + if (!logWorker && !workThread) return; + + qInstallMessageHandler(0); + + if (logWorker) { + + QMetaObject::invokeMethod( + logWorker, + &LogWorker::stop, + Qt::QueuedConnection + ); + } + + if (workThread && workThread->isRunning()) { + + if (!workThread->wait(1500)) { + + workThread->quit(); + + if (!workThread->wait(1500)) { + + workThread->terminate(); + workThread->wait(); + } + } + } + + if (workThread) delete workThread; + if (logWorker) delete logWorker; + + workThread = nullptr; + logWorker = nullptr; +} +//============================================================================== +void Logger::writeLogQtType(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + if (!logWorker) return; + + QString category(context.category); + LogType logType {LogType::LogInfo}; + QString objectName; + + if (category.indexOf("input") == 0) { + + logType = LogType::LogInput; + + if (category.length() > 5) objectName = category.mid(5); + } + else if (category.indexOf("output") == 0) { + + logType = LogType::LogOutput; + + if (category.length() > 6) objectName = category.mid(6); + } + else { + + logType = qtMsgToLogType(type); + + if (!category.isEmpty() && (category != "default")) objectName = category; + } + + writeLog(msg, logType, objectName); +} +//============================================================================== +void Logger::writeLog(const QString &msg, LogType type, const QString &objectName) +{ + if (!logWorker) return; + + static bool dbg {Application::isDebug()}; + + if (!dbg && ((type == LogType::LogDebug) || (type == LogType::LogInput) + || (type == LogType::LogOutput))) return; + + int typeInt { static_cast(type) }; + + QMetaObject::invokeMethod( + logWorker, + "writeLog", + Qt::QueuedConnection, + Q_ARG(QString, msg), + Q_ARG(int, typeInt), + Q_ARG(QString, objectName) + ); +} +//============================================================================== +Logger::LogType Logger::stringToLogType(const QString &stringType) +{ + if (stringType.contains("wrn", Qt::CaseInsensitive) + || stringType.contains("warn", Qt::CaseInsensitive)) + return LogType::LogWarning; + else if (stringType.contains("dbg", Qt::CaseInsensitive) + || stringType.contains("debug", Qt::CaseInsensitive)) + return LogType::LogDebug; + else if (stringType.contains("err", Qt::CaseInsensitive) + || stringType.contains("critic", Qt::CaseInsensitive) + || stringType.contains("fatal", Qt::CaseInsensitive)) + return LogType::LogError; + else if (stringType.contains(">>>", Qt::CaseInsensitive) + || stringType.contains("output", Qt::CaseInsensitive)) + return LogType::LogOutput; + else if (stringType.contains("<<<", Qt::CaseInsensitive) + || stringType.contains("input", Qt::CaseInsensitive)) + return LogType::LogInput; + + return LogType::LogInfo; +} +//============================================================================== +Logger::Logger(QObject *parent) : QObject{parent} +{ + if ( !QMetaType::isRegistered( qMetaTypeId() ) ) + qRegisterMetaType(); + + QCoreApplication *application = QCoreApplication::instance(); + + if (application) application->setProperty("loggerRun", true); +} +//============================================================================== +Logger::~Logger() +{ + stop(); + + QCoreApplication *application = QCoreApplication::instance(); + + if (application) application->setProperty("loggerRun", false); +} +//============================================================================== + +//============================================================================== +// +// LogWorker - internal class +// +//============================================================================== +LogWorker::LogWorker(const QString &logFilePath, QObject *parent) : QObject{parent} +{ + file.setFileName(logFilePath); +} +//============================================================================== +LogWorker::~LogWorker() +{ + stop(); +} +//============================================================================== +void LogWorker::start() +{ + if (running) return; + + if (!file.open(QIODevice::ReadWrite | QIODevice::Truncate)) { + + qCritical() << "Не удалось открыть файл для записи: " << file.fileName(); + return; + } + + linesPosition = 0; + linesCounter = 0; + startTime = QDateTime::currentDateTime(); + + writeFirstLines(); + writeLastLines(); + + timer = new QTimer(nullptr); + timer->setInterval(queueIntervalMs); + timer->setSingleShot(false); + + connect(timer, &QTimer::timeout, this, &LogWorker::checkQueue); + + timer->start(); + running = true; + emit started(); +} +//============================================================================== +void LogWorker::stop() +{ + if (!running) return; + + QMutexLocker locker(&mutex); + running = false; + + if (timer) { + + timer->stop(); + delete timer; + timer = nullptr; + } + + if (file.isOpen()) { + + writeAllFromQueue(); + writeLastLines(); + + file.flush(); + file.close(); + } + + emit stopped(); +} +//============================================================================== +void LogWorker::writeLog(const QString &msg, int type, const QString &objectName) +{ + if (!running) return; + + QMutexLocker lock(&mutex); + LogRecord rec {type, msg, objectName, QDateTime::currentDateTime()}; + + queue.enqueue(rec); +} +//============================================================================== +void LogWorker::checkQueue() +{ + if (!running) return; + if (!mutex.tryLock()) return; + + writeAllFromQueue(); + + mutex.unlock(); +} +//============================================================================== +void LogWorker::writeFirstLines() +{ + if (!file.isOpen()) return; + + writeLine( tr("----- Начало %1 -") + .arg(startTime.toString("yyyy-MM-dd HH:mm:ss")) + .leftJustified(80,'-'), + -1); + + QString runMode; + + if (Application::isDebug()) { + + runMode = "DEBUG"; + } + + if (Application::isPortable()) { + + if (!runMode.isEmpty()) runMode += ", "; + runMode += "PORTABLE"; + } + + if (runMode.isEmpty()) runMode = "NORMAL"; + + writeLine(tr("ПО: %1").arg(QCoreApplication::applicationName()), -1); + writeLine(tr("Версия: %1").arg(QCoreApplication::applicationVersion()), -1); + writeLine(tr("Разработчик: %1 %2") + .arg( QCoreApplication::organizationName(), + QCoreApplication::organizationDomain().isEmpty() + ? QString() + : QString("(" + QCoreApplication::organizationDomain() + ")") + ), + -1 + ); + writeLine(tr("Строка запуска: '%1'").arg( QCoreApplication::arguments().join(' ') ), -1); + writeLine(tr("Режим запуска: %1").arg(runMode), -1); + + if (file.write(QByteArray(1,'\n')) == 1) { + + ++linesCounter; + ++linesPosition; + } + + file.flush(); +} +//============================================================================== +void LogWorker::writeLastLines() +{ + if (!file.isOpen()) return; + + if ((file.pos() != linesPosition) && !file.seek(linesPosition)) { + + return; + } + + QDateTime endTime = QDateTime::currentDateTime(); + qint64 n = endTime.toMSecsSinceEpoch() - startTime.toMSecsSinceEpoch(); + qint64 hr = n / 3600000; + qint64 ms = n - 3600000 * hr; + qint64 min = ms / 60000; + ms = ms - 60000 * min; + qint64 sec = ms / 1000; + ms = ms - 1000 * sec; + + file.write(QByteArray(1,'\n')); + + writeLine(tr("Текущее состояние: %1") + .arg( running ? "в процессе работы" : "работа завершена" ), + -2 + ); + writeLine(tr("Общее время: %1 ч, %2 мин, %3 сек, %4 мсек") + .arg(hr).arg(min).arg(sec).arg(ms), + -2 + ); + + writeLine(tr("Общее количество строк: %1") + .arg( linesCounter + 5 ), + -2 + ); + writeLine(tr("----- Окончание %1 -") + .arg( QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss") ) + .leftJustified(80,'-'), + -2); + + file.flush(); +} +//============================================================================== +void LogWorker::writeLine(const QString &msg, int type, + const QString &objectName, const QDateTime &dateTime) +{ + QByteArray data { + QString( + dateTime.toString("[HH:mm:ss.zzz] ") + + QString(type < 0 ? "[sys]" : Logger::logTypeToString(static_cast(type))) + + " " + + QString(objectName.isEmpty() ? QString() : "{" + objectName + "} ") + + msg + + "\n" + ).toUtf8() + }; + + qint64 bytesWrite {0}; + int cnt {3}; + + while ((bytesWrite < data.size()) && (cnt > 0)) { + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + qint64 n = file.write(data.mid(bytesWrite)); +#else + qint64 n = file.write(data.mid(static_cast(bytesWrite))); +#endif + + if (n > 0) { + + bytesWrite += n; + } + else { + + --cnt; + } + } + + if (type < -1) return; + + linesPosition += bytesWrite; + + if (bytesWrite == data.size()) ++linesCounter; +} +//============================================================================== +void LogWorker::writeAllFromQueue() +{ + if (queue.isEmpty()) return; + + if (!file.isOpen()) { + + queue.clear(); + return; + } + + if ((file.pos() != linesPosition) && !file.seek(linesPosition)) { + + return; + } + + while (!queue.isEmpty()) { + + LogRecord rec = queue.dequeue(); + writeLine( rec.msg, rec.type, rec.objectName, rec.dateTime ); + } + + writeLastLines(); +} +//============================================================================== +// +// FunctionLogger +// +//============================================================================== +FunctionLogger::FunctionLogger(const char *file, + int line, + const char *name, + const QLoggingCategory &category) + : functionName{name} + , logCategory{category.categoryName()} +{ + id = counter++; + + if (logCategory.isEmpty()) + logCategory = "Function"; + + quint64 threadId = reinterpret_cast(QThread::currentThreadId()); + + qDebug() << QString("{%1} ID: %2, Thread: %3, File: '%4', Line: %5, Entering function: %6") + .arg(logCategory) + .arg(id) + .arg(threadId) + .arg(file) + .arg(line) + .arg(functionName) + .toStdString().c_str(); +} +//============================================================================== +FunctionLogger::~FunctionLogger() +{ + qDebug() << QString("{%1} ID: %2, Exiting function: %3") + .arg(logCategory) + .arg(id) + .arg(functionName) + .toStdString().c_str(); +} +//============================================================================== +