pax_global_header00006660000000000000000000000064133106657700014522gustar00rootroot0000000000000052 comment=48d2ccc82eb99321933b4f6ea039f2c037a05587 Charm-1.12.0/000077500000000000000000000000001331066577000126355ustar00rootroot00000000000000Charm-1.12.0/.gitignore000066400000000000000000000004071331066577000146260ustar00rootroot00000000000000.DS_Store b/ Charm.app CharmCMake.h cmake_install.cmake CMakeCache.txt CMakeFiles/ CMakeLists.txt.user CPackConfig.cmake CPackSourceConfig.cmake CTestTestfile.cmake Makefile Tests/*Tests Thumbs.db *.a *.moc *.moc_parameters *.qrc.depends qrc_*.cxx Charm.pro.user Charm-1.12.0/.krazy000066400000000000000000000006031331066577000137750ustar00rootroot00000000000000CHECKSETS qt4,c++,foss #exclude intrusive checks to investigate later EXCLUDE nullstrassign #KDAB-specific checks EXTRA kdabcopyright #additional checks EXTRA camelcase,null,defines,crud #skip Keychain SKIP /Charm/Keychain/keychain #if you have a build subdir, skip it SKIP /build- #other skips SKIP /CharmCMake.h.cmake #skip the borrowed code in the cmake subdir SKIP /cmake/ECM/ Charm-1.12.0/BUILD000066400000000000000000000051301331066577000134160ustar00rootroot00000000000000= Requirements = Charm supports both Qt 4 and Qt 5 right now. We recommend to use the latest Qt release for the respective major version, i.e. Qt 4.8.7 or Qt 5.5 as of now. We don't run CI or manual tests for older minor releases. The Qt modules required on all platforms are: QtWidgets, QtXml, QtSql with SQLite3 plugin, QtNetwork, QtTest, QtScript (or equivalent Qt 4 features) Optional: QtPrintSupport Charm code uses C++11, so using a recent enough compiler is advised (see below for details). == Linux == * g++ >= 4.8 * CMake 3.x * Qt 4.8.7 or Qt 5; we still use Qt 4 for the distro packages on OpenSUSE Build Service [1] because the Qt 5 packages of the various distros tend to have issues with desktop integration (tray icons in Unity, for example). If you're building from source, we're encouraging you to use Qt 5. * Extra Qt modules/features required: QtDBus [1] https://build.opensuse.org/package/repositories/isv:KDAB/charmtimetracker == OS X == * A new enough clang to support the C++11 features we use. We recommend to use the latest XCode if possible. (7.0.2 right now) * Qt 5 (Qt 4's support for recent OS X versions is lacking, thus Qt4-based builds for OS X are not supported or tested by us); Using the official Qt build is recommended * Extra Qt 5 modules required: QtMacExtras * CMake 3.x == Windows == On Windows we require: * MSVC 2013 or MinGW (gcc >= 4.8; lower versions untested) * A Qt 5 build for MSVC 2013 or MinGW, matching the compiler you want to use (Qt 4-based builds for Windows might work but is not supported or tested by us) * Extra Qt 5 modules required: QtWinExtras * Install OpenSSL and make sure the libraries are in the PATH (TODO: add more details like suggested downloads) * CMake 3.x = Build Instructions = == Building from the Terminal == The build steps for a Debug build when building from the Terminal are: cd Charm mkdir build cd build cmake -DCMAKE_BUILD_TYPE=Debug .. make # Windows: nmake, mingw32-make or jom, see below Windows-specific hints: * Ensure that your compiler's environment is sourced (Call vcvarsall.bat for MSVC2013). * For MinGW: pass -G "MinGW Makefiles". "make" becomes "mingw32-make" * When using MSVC: call "nmake" instead of make. * You can use jom.exe (parallel builds using multiple cores) which is shipped as part of Qt Creator for both MSVC and MinGW builds == Building in Qt Creator == * Choose File -> Open File or Project * Select the top-level CMakeLists.txt * Select the Generator matching your architecture (32bit vs. 64bit) and enter the wanted arguments, e.g. "-DCMAKE_BUILD_TYPE=Debug" * Click "Run CMake" Charm-1.12.0/CMakeLists.txt000066400000000000000000000135761331066577000154110ustar00rootroot00000000000000CMAKE_MINIMUM_REQUIRED( VERSION 2.8.12 ) PROJECT( Charm CXX ) include(FeatureSummary) set(ECM_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ECM/modules/") set(CMAKE_MODULE_PATH ${ECM_MODULE_DIR} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ECM/kde-modules" ) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMInstallIcons) IF( NOT Charm_VERSION ) FIND_PACKAGE( Git QUIET ) IF( EXISTS ${GIT_EXECUTABLE} ) EXECUTE_PROCESS( COMMAND ${GIT_EXECUTABLE} describe --tags --abbrev=1 RESULT_VARIABLE GIT_RESULT OUTPUT_VARIABLE Charm_VERSION WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_STRIP_TRAILING_WHITESPACE ) IF( NOT GIT_RESULT EQUAL 0 ) MESSAGE( FATAL_ERROR "Cannot get 'git describe' version!" ) ENDIF() ENDIF() ENDIF() STRING( REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.(.*)$" Charm_VERSION "${Charm_VERSION}") SET( Charm_VERSION_MAJOR "${CMAKE_MATCH_1}" ) SET( Charm_VERSION_MINOR "${CMAKE_MATCH_2}" ) SET( Charm_VERSION_PATCH "${CMAKE_MATCH_3}" ) SET( Charm_VERSION_COUNT 3 ) IF( NOT ( DEFINED Charm_VERSION_MAJOR AND DEFINED Charm_VERSION_MINOR AND DEFINED Charm_VERSION_PATCH ) ) MESSAGE( FATAL_ERROR "No Git executable or valid Charm version argument found.\n" "Please pass a version to CMake e.g. cmake -DCharm_VERSION=1.0.0" ) ENDIF() MESSAGE( STATUS "Building Charm ${Charm_VERSION} in ${CMAKE_BUILD_TYPE} mode" ) find_package( Qt5Core REQUIRED) find_package(Qt5Widgets REQUIRED) find_package(Qt5Xml REQUIRED) find_package(Qt5Network REQUIRED) find_package(Qt5Sql REQUIRED) find_package(Qt5Test REQUIRED) find_package(Qt5PrintSupport) if(WIN32) find_package(Qt5WinExtras REQUIRED) endif() IF(APPLE) find_package(Qt5MacExtras REQUIRED) ENDIF() IF(UNIX AND NOT APPLE) find_package(Qt5DBus QUIET) ENDIF() find_package(Qt5Keychain REQUIRED) set_package_properties(Qt5Keychain PROPERTIES DESCRIPTION "Provides support for secure credentials storage" URL "https://github.com/frankosterfeld/qtkeychain" TYPE REQUIRED) SET(CHARM_MAC_HIGHRES_SUPPORT_ENABLED ON) ENABLE_TESTING() IF( UNIX AND NOT APPLE ) set( Charm_EXECUTABLE charmtimetracker ) ELSE() set( Charm_EXECUTABLE Charm ) ENDIF() SET( BIN_INSTALL_DIR bin ) SET( DOC_INSTALL_DIR ${CMAKE_INSTALL_DOCBUNDLEDIR}/${Charm_EXECUTABLE} ) SET( ICONS_DIR "${CMAKE_SOURCE_DIR}/Charm/Icons" ) IF( CHARM_PREPARE_DEPLOY AND WIN32 OR APPLE) SET( BIN_INSTALL_DIR . ) SET( DOC_INSTALL_DIR . ) ENDIF() OPTION( CHARM_IDLE_DETECTION "Build the Charm idle detector" ON ) OPTION( CHARM_TIMESHEET_TOOLS "Build the Charm timesheet tools" OFF ) set( CHARM_IDLE_TIME "360" CACHE STRING "Set the idle timeout (in seconds, default 360)" ) OPTION( CHARM_CI_SUPPORT "Build Charm with command interface support" OFF ) IF( CHARM_CI_SUPPORT ) OPTION( CHARM_CI_TCPSERVER "Build Charm with TCP command interface support" ON ) OPTION( CHARM_CI_LOCALSERVER "Build Charm with local socket command interface support" ON ) ENDIF() OPTION(CHARM_PREPARE_DEPLOY "Deploy dependencies with install target(Windows, Apple)" ON) ADD_SUBDIRECTORY( Core ) ADD_SUBDIRECTORY( Charm ) IF( CHARM_TIMESHEET_TOOLS AND UNIX ) # Only build the tools if they are explicitly requested to avoid # the Qt MySQL driver dependency. ADD_SUBDIRECTORY( Tools/TimesheetProcessor ) ADD_SUBDIRECTORY( Tools/TimesheetGenerator ) MESSAGE( STATUS "Building the Charm timesheet tools") ENDIF() ADD_SUBDIRECTORY( Tests ) CONFIGURE_FILE( CharmCMake.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/CharmCMake.h ) SET( LICENSE_FILE "License.txt" ) SET( README_FILE "ReadMe.md" ) IF( NOT APPLE ) INSTALL( FILES "${LICENSE_FILE}" "${README_FILE}" DESTINATION ${DOC_INSTALL_DIR} ) ENDIF() IF (WIN32) FIND_PACKAGE( PythonInterp 3.5 QUIET) set_package_properties(PythonInterp PROPERTIES PURPOSE "Used to create Windows installer" TYPE OPTIONAL) IF(PYTHONINTERP_FOUND) OPTION(CHARM_SIGN_INSTALLER "Sign the installer and the contained files" ON) SET(EXTRA_PACKAGE_COMMANDS ) FIND_PACKAGE( OpenSSL QUIET) set_package_properties(OpenSSL PROPERTIES PURPOSE "Deployment of openssl libraries." TYPE OPTIONAL) IF (OPENSSL_FOUND) SET(EXTRA_PACKAGE_COMMANDS ${EXTRA_PACKAGE_COMMANDS} --deployOpenSSL "${OPENSSL_INCLUDE_DIR}/../") ENDIF() IF( CMAKE_BUILD_TYPE MATCHES "^([Dd][Ee][Bb][Uu][Gg])" ) SET(EXTRA_PACKAGE_COMMANDS ${EXTRA_PACKAGE_COMMANDS} --buildType debug) ENDIF() IF (CMAKE_SIZEOF_VOID_P EQUAL 8) set(EXTRA_PACKAGE_COMMANDS ${EXTRA_PACKAGE_COMMANDS} --architecture x64) ELSE() set(EXTRA_PACKAGE_COMMANDS ${EXTRA_PACKAGE_COMMANDS} --architecture x86) ENDIF() IF(CHARM_SIGN_INSTALLER) set(EXTRA_PACKAGE_COMMANDS ${EXTRA_PACKAGE_COMMANDS} --sign) ENDIF() ADD_CUSTOM_TARGET(package COMMAND ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/scripts/create-win-installer.py" --installerName "Charm-${Charm_VERSION}.exe" --applicationFileName "bin/Charm.exe" --buildDir "${CMAKE_CURRENT_BINARY_DIR}" --productName Charm --productVersion "${Charm_VERSION}" --companyName KDAB --applicationIcon "${CMAKE_CURRENT_SOURCE_DIR}/Charm/Icons/Charm.ico" --productLicence "${CMAKE_CURRENT_SOURCE_DIR}/License.txt" ${EXTRA_PACKAGE_COMMANDS} DEPENDS ${Charm_EXECUTABLE} VERBATIM) ENDIF() ENDIF() feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES) Charm-1.12.0/COPYING000077700000000000000000000000001331066577000160062License.txtustar00rootroot00000000000000Charm-1.12.0/Charm.pro000066400000000000000000000151751331066577000144220ustar00rootroot00000000000000!override_platform_check { !android: error("Building Charm with QMake is not supported, and used only for Qt/Android experiments. For everything else, please use the CMake build system.") } QT += core gui xml sql network widgets qml quick INCLUDEPATH += Core/ INCLUDEPATH += Charm/ TARGET = AndCharm TEMPLATE = app RESOURCES = Charm/CharmResources.qrc Charm/QtQuick/qml.qrc DEFINES += 'CHARM_VERSION=\'\"0.1a\"\'' DEFINES += 'CHARM_IDLE_TIME=0' DEFINES += QT_NO_DBUS QT_NO_PRINTER SOURCES += $$files(Core/*.cpp) SOURCES += \ Charm/ApplicationCore.cpp \ Charm/Data.cpp \ Charm/EventModelAdapter.cpp \ Charm/EventModelFilter.cpp \ Charm/GUIState.cpp \ Charm/ModelConnector.cpp \ Charm/TaskModelAdapter.cpp \ Charm/ViewFilter.cpp \ Charm/ViewHelpers.cpp \ Charm/WeeklySummary.cpp \ Charm/UndoCharmCommandWrapper.cpp \ Charm/Commands/CommandRelayCommand.cpp \ Charm/Commands/CommandModifyEvent.cpp \ Charm/Commands/CommandDeleteEvent.cpp \ Charm/Commands/CommandSetAllTasks.cpp \ Charm/Commands/CommandAddTask.cpp \ Charm/Commands/CommandModifyTask.cpp \ Charm/Commands/CommandDeleteTask.cpp \ Charm/Commands/CommandMakeEvent.cpp \ Charm/Commands/CommandExportToXml.cpp \ Charm/Commands/CommandImportFromXml.cpp \ Charm/Commands/CommandMakeAndActivateEvent.cpp \ Charm/HttpClient/HttpJob.cpp \ Charm/HttpClient/GetProjectCodesJob.cpp \ Charm/HttpClient/UploadTimesheetJob.cpp \ Charm/Idle/IdleDetector.cpp \ Charm/Reports/MonthlyTimesheetXmlWriter.cpp \ Charm/Reports/TimesheetInfo.cpp \ Charm/Reports/WeeklyTimesheetXmlWriter.cpp \ Charm/Widgets/ActivityReport.cpp \ Charm/Widgets/BillDialog.cpp \ Charm/Widgets/CharmPreferences.cpp \ Charm/Widgets/CharmWindow.cpp \ Charm/Widgets/CharmAboutDialog.cpp \ Charm/Widgets/ConfigurationDialog.cpp \ Charm/Widgets/HttpJobProgressDialog.cpp \ Charm/Widgets/DateEntrySyncer.cpp \ Charm/Widgets/EnterVacationDialog.cpp \ Charm/Widgets/EventEditor.cpp \ Charm/Widgets/EventEditorDelegate.cpp \ Charm/Widgets/EventView.cpp \ Charm/Widgets/ExpandStatesHelper.cpp \ Charm/Widgets/IdleCorrectionDialog.cpp \ Charm/Widgets/MessageBox.cpp \ Charm/Widgets/MonthlyTimesheet.cpp \ Charm/Widgets/MonthlyTimesheetConfigurationDialog.cpp \ Charm/Widgets/ReportConfigurationDialog.cpp \ Charm/Widgets/ReportPreviewWindow.cpp \ Charm/Widgets/SelectTaskDialog.cpp \ Charm/Widgets/TaskIdDialog.cpp \ Charm/Widgets/TaskEditor.cpp \ Charm/Widgets/TasksView.cpp \ Charm/Widgets/TasksViewDelegate.cpp \ Charm/Widgets/TimeTrackingView.cpp \ Charm/Widgets/TimeTrackingWindow.cpp \ Charm/Widgets/TimeTrackingTaskSelector.cpp \ Charm/Widgets/TrayIcon.cpp \ Charm/Widgets/Timesheet.cpp \ Charm/Widgets/WeeklyTimesheet.cpp \ SOURCES += \ Charm/Keychain/keychain.cpp \ Charm/Keychain/keychain_unsecure.cpp SOURCES += Charm/QtQuick/Charm.cpp HEADERS += $$files(Core/*.h) HEADERS += \ Charm/MakeTemporarilyVisible.h \ Charm/EventModelAdapter.h \ Charm/EventModelFilter.h \ Charm/Idle/IdleDetector.h \ Charm/Uniquifier.h \ Charm/ViewModeInterface.h \ Charm/GUIState.h \ Charm/UndoCharmCommandWrapper.h \ Charm/ViewFilter.h \ Charm/Reports/MonthlyTimesheetXmlWriter.h \ Charm/Reports/TimesheetInfo.h \ Charm/Reports/WeeklyTimesheetXmlWriter.h \ Charm/Widgets/TasksViewDelegate.h \ Charm/Widgets/IdleCorrectionDialog.h \ Charm/Widgets/Timesheet.h \ Charm/Widgets/WeeklyTimesheet.h \ Charm/Widgets/CharmWindow.h \ Charm/Widgets/DateEntrySyncer.h \ Charm/Widgets/MonthlyTimesheetConfigurationDialog.h \ Charm/Widgets/TaskIdDialog.h \ Charm/Widgets/MessageBox.h \ Charm/Widgets/TaskEditor.h \ Charm/Widgets/ReportConfigurationDialog.h \ Charm/Widgets/TimeTrackingTaskSelector.h \ Charm/Widgets/EventEditor.h \ Charm/Widgets/TasksView.h \ Charm/Widgets/CharmPreferences.h \ Charm/Widgets/EnterVacationDialog.h \ Charm/Widgets/TimeTrackingWindow.h \ Charm/Widgets/TrayIcon.h \ Charm/Widgets/TimeTrackingView.h \ Charm/Widgets/ActivityReport.h \ Charm/Widgets/EventEditorDelegate.h \ Charm/Widgets/ExpandStatesHelper.h \ Charm/Widgets/SelectTaskDialog.h \ Charm/Widgets/MonthlyTimesheet.h \ Charm/Widgets/EventView.h \ Charm/Widgets/ConfigurationDialog.h \ Charm/Widgets/HttpJobProgressDialog.h \ Charm/Widgets/ReportPreviewWindow.h \ Charm/Widgets/BillDialog.h \ Charm/Widgets/CharmAboutDialog.h \ Charm/Commands/CommandImportFromXml.h \ Charm/Commands/CommandModifyTask.h \ Charm/Commands/CommandAddTask.h \ Charm/Commands/CommandExportToXml.h \ Charm/Commands/CommandDeleteTask.h \ Charm/Commands/CommandModifyEvent.h \ Charm/Commands/CommandMakeAndActivateEvent.h \ Charm/Commands/CommandSetAllTasks.h \ Charm/Commands/CommandRelayCommand.h \ Charm/Commands/CommandDeleteEvent.h \ Charm/Commands/CommandMakeEvent.h \ Charm/ViewHelpers.h \ Charm/ModelConnector.h \ Charm/WeeklySummary.h \ Charm/HttpClient/GetProjectCodesJob.h \ Charm/HttpClient/UploadTimesheetJob.h \ Charm/HttpClient/HttpJob.h \ Charm/ApplicationCore.h \ Charm/Keychain/keychain.h \ Charm/Keychain/keychain_p.h \ Charm/TaskModelAdapter.h \ Charm/Data.h \ FORMS += $$files(Charm/Widgets/*.ui) CONFIG += mobility MOBILITY = # Disable some of the noise for now. *-g++*|*-clang*|*-llvm* { QMAKE_CXXFLAGS += -Wno-unused-variable -Wno-unused-parameter -Wno-unused-function } # CMake works with include files named "filename.moc" while qmake expects # "moc_filename.cpp". Since qmake does not provide any way to change that # to the requirement we just hack around. #new_moc.output = ${QMAKE_FILE_BASE}.moc #new_moc.commands = moc -i -nw ${QMAKE_FILE_NAME} -o ${QMAKE_FILE_OUT} #new_moc.depend_command = $$QMAKE_CXX -E -M ${QMAKE_FILE_NAME} | sed "s/^.*: //" #new_moc.input = HEADERS #QMAKE_EXTRA_COMPILERS += new_moc MOC_HEADERS = $$HEADERS #MOC_HEADERS -= $$files(Charm/keychain_p.h) for(hdr, MOC_HEADERS) { fdir=$$dirname(hdr) base=$$basename(hdr) fname=$$section(base, ".", 0, 0) in=$${LITERAL_HASH}include out=$${OUT_PWD}/$${fname}.moc exists( $${fdir}/$${fname}.cpp ) { system(echo \"$$in\" > \"$$out\") } } # Create that CharmCMake.h file that is auto-created by cmake and included # everywhere. Here we could also hard-code defines or whatever that file # includes when cmake creates it. system('echo "" > "$${OUT_PWD}/CharmCMake.h"') ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android OTHER_FILES += \ android/AndroidManifest.xml Charm-1.12.0/Charm/000077500000000000000000000000001331066577000136675ustar00rootroot00000000000000Charm-1.12.0/Charm/ApplicationCore.cpp000066400000000000000000000731641331066577000174620ustar00rootroot00000000000000/* ApplicationCore.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ApplicationCore.h" #include "CharmCMake.h" #include "Data.h" #include "ViewHelpers.h" #include "Core/CharmConstants.h" #include "Core/CharmExceptions.h" #include "Core/SqLiteStorage.h" #include "Idle/IdleDetector.h" #include "Lotsofcake/Configuration.h" #include "Widgets/ConfigurationDialog.h" #include "Widgets/NotificationPopup.h" #include "Widgets/TasksView.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #include #include #endif #ifdef Q_OS_WIN #include #include #endif #include #include #include #ifdef CHARM_CI_SUPPORT # include "CI/CharmCommandInterface.h" #endif namespace { static const QByteArray StartTaskCommand = QByteArrayLiteral("start-task: "); static const QByteArray RaiseWindowCommand = QByteArrayLiteral("raise-window"); } ApplicationCore *ApplicationCore::m_instance = nullptr; ApplicationCore::ApplicationCore(TaskId startupTask, bool hideAtStart, QObject *parent) : QObject(parent) , m_actionStopAllTasks(this) , m_actionQuit(this) , m_actionAboutDialog(this) , m_actionPreferences(this) , m_actionExportToXml(this) , m_actionImportFromXml(this) , m_actionSyncTasks(this) , m_actionImportTasks(this) , m_actionExportTasks(this) , m_actionCheckForUpdates(this) , m_actionEnterVacation(this) , m_actionActivityReport(this) , m_actionWeeklyTimesheetReport(this) , m_actionMonthlyTimesheetReport(this) , m_uiElements( { &m_timeTracker, &m_tasksView, &m_eventView }), m_startupTask(startupTask) , m_hideAtStart(hideAtStart) #ifdef Q_OS_WIN , m_windowsJumpList(new QWinJumpList(this)) #endif , m_dateChangeWatcher(new DateChangeWatcher(this)) { // QApplication setup QApplication::setQuitOnLastWindowClosed(false); // application metadata setup // note that this modifies the behaviour of QSettings: QCoreApplication::setOrganizationName(QStringLiteral("KDAB")); QCoreApplication::setOrganizationDomain(QStringLiteral("kdab.com")); QCoreApplication::setApplicationName(QStringLiteral("Charm")); QCoreApplication::setApplicationVersion(CharmVersion()); QLocalSocket uniqueApplicationSocket; QString serverName(QStringLiteral("com.kdab.charm")); QString charmHomeEnv(QString::fromLocal8Bit(qgetenv("CHARM_HOME"))); if (!charmHomeEnv.isEmpty()) { serverName.append(QStringLiteral("_%1").arg( charmHomeEnv.replace(QRegExp(QLatin1String(":?/|:?\\\\")), QStringLiteral("_")))); } #ifndef NDEBUG serverName.append(QStringLiteral("_debug")); #endif uniqueApplicationSocket.connectToServer(serverName, QIODevice::ReadWrite); if (uniqueApplicationSocket.waitForConnected(1000)) { QByteArray command; if (startupTask != -1) { command = StartTaskCommand + QByteArray::number(startupTask); } else { command = RaiseWindowCommand; } command += '\n'; qint64 written = uniqueApplicationSocket.write(command); if (written == -1 || written != command.length()) { qWarning() << "Failed to pass " << command << " to running charm instance, error: " << uniqueApplicationSocket.errorString(); } uniqueApplicationSocket.flush(); uniqueApplicationSocket.waitForBytesWritten(); throw AlreadyRunningException(); } connect(&m_uniqueApplicationServer, &QLocalServer::newConnection, this, &ApplicationCore::slotHandleUniqueApplicationConnection, Qt::QueuedConnection); QFile::remove(QDir::tempPath() + QLatin1Char('/') + serverName); bool listening = m_uniqueApplicationServer.listen(serverName); if (!listening) qDebug() << "Failed to create QLocalServer for unique application support:" << m_uniqueApplicationServer.errorString(); Q_INIT_RESOURCE(CharmResources); Q_ASSERT_X(m_instance == 0, "Application ctor", "Application is a singleton and cannot be created more than once"); m_instance = this; qRegisterMetaType("State"); qRegisterMetaType("Event"); // exit process (app will only exit once controller says it is ready) connect(&m_controller, &Controller::readyToQuit, this, &ApplicationCore::slotControllerReadyToQuit); connectControllerAndModel(&m_controller, m_model.charmDataModel()); Charm::connectControllerAndView(&m_controller, &m_timeTracker); // save the configuration (configuration is managed by the application) connect(&m_timeTracker, &CharmWindow::saveConfiguration, this, &ApplicationCore::slotSaveConfiguration); connect(&m_timeTracker, &TimeTrackingWindow::showNotification, this, &ApplicationCore::slotShowNotification); connect(&m_timeTracker, &TimeTrackingWindow::taskMenuChanged, this, &ApplicationCore::slotPopulateTrayIconMenu); // save the configuration (configuration is managed by the application) connect(&m_tasksView, &TasksView::saveConfiguration, this, &ApplicationCore::slotSaveConfiguration); // due to multiple inheritence we can't use the new style connects here connect(&m_tasksView, SIGNAL(emitCommand(CharmCommand*)), &m_timeTracker, SLOT(sendCommand(CharmCommand*))); connect(&m_tasksView, SIGNAL(emitCommandRollback(CharmCommand*)), &m_timeTracker, SLOT(sendCommandRollback(CharmCommand*))); connect(&m_eventView, SIGNAL(emitCommand(CharmCommand*)), &m_timeTracker, SLOT(sendCommand(CharmCommand*))); connect(&m_eventView, SIGNAL(emitCommandRollback(CharmCommand*)), &m_timeTracker, SLOT(sendCommandRollback(CharmCommand*))); // my own signals: connect(this, &ApplicationCore::goToState, this, &ApplicationCore::setState, Qt::QueuedConnection); // system tray icon: m_actionStopAllTasks.setText(tr("Stop Current Task")); m_actionStopAllTasks.setShortcut(Qt::Key_Escape); m_actionStopAllTasks.setShortcutContext(Qt::ApplicationShortcut); mainView().addAction(&m_actionStopAllTasks); // for the shortcut to work connect(&m_actionStopAllTasks, &QAction::triggered, this, &ApplicationCore::slotStopAllTasks); m_systrayContextMenu.addAction(&m_actionStopAllTasks); m_systrayContextMenu.addSeparator(); m_systrayContextMenu.addAction(m_timeTracker.openCharmAction()); m_systrayContextMenu.addAction(&m_actionQuit); m_trayIcon.setContextMenu(&m_systrayContextMenu); m_trayIcon.setToolTip(tr("No active events")); m_trayIcon.setIcon(Data::charmTrayIcon()); m_trayIcon.show(); QApplication::setWindowIcon(Data::charmIcon()); // set up actions: m_actionQuit.setShortcut(Qt::CTRL + Qt::Key_Q); m_actionQuit.setText(tr("Quit")); m_actionQuit.setIcon(Data::quitCharmIcon()); connect(&m_actionQuit, &QAction::triggered, this, &ApplicationCore::slotQuitApplication); m_actionAboutDialog.setText(tr("About Charm")); connect(&m_actionAboutDialog, &QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotAboutDialog); m_actionPreferences.setText(tr("Preferences")); m_actionPreferences.setIcon(Data::configureIcon()); connect(&m_actionPreferences, &QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotEditPreferences); m_actionPreferences.setEnabled(true); m_actionImportFromXml.setText(tr("Import Database from Previous Export...")); connect(&m_actionImportFromXml, &QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotImportFromXml); m_actionExportToXml.setText(tr("Export Database...")); connect(&m_actionExportToXml, &QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotExportToXml); m_actionSyncTasks.setText(tr("Update Task Definitions...")); //the signature of QAction::triggered does not match slotSyncTasks connect(&m_actionSyncTasks,&QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotSyncTasksVerbose); m_actionImportTasks.setText(tr("Import and Merge Task Definitions...")); connect(&m_actionImportTasks, &QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotImportTasks); m_actionExportTasks.setText(tr("Export Task Definitions...")); connect(&m_actionExportTasks, &QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotExportTasks); m_actionCheckForUpdates.setText(tr("Check for Updates...")); #if 0 // TODO this role should be set to have the action in the app menu, but that // leads to duplicated entries, as each of the three main windows adds the action to the menu // and Qt doesn't prevent duplicates (#222) m_actionCheckForUpdates.setMenuRole(QAction::ApplicationSpecificRole); #endif connect(&m_actionCheckForUpdates, &QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotCheckForUpdatesManual); m_actionEnterVacation.setText(tr("Enter Vacation...")); connect(&m_actionEnterVacation, &QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotEnterVacation); m_actionActivityReport.setText(tr("Activity Report...")); m_actionActivityReport.setShortcut(Qt::CTRL + Qt::Key_A); connect(&m_actionActivityReport, &QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotActivityReport); m_actionWeeklyTimesheetReport.setText(tr("Weekly Timesheet...")); m_actionWeeklyTimesheetReport.setShortcut(Qt::CTRL + Qt::Key_R); connect(&m_actionWeeklyTimesheetReport, &QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotWeeklyTimesheetReport); m_actionMonthlyTimesheetReport.setText(tr("Monthly Timesheet...")); m_actionMonthlyTimesheetReport.setShortcut(Qt::CTRL + Qt::Key_M); connect(&m_actionMonthlyTimesheetReport, &QAction::triggered, &m_timeTracker, &TimeTrackingWindow::slotMonthlyTimesheetReport); // set up idle detection m_idleDetector = IdleDetector::createIdleDetector(this); Q_ASSERT(m_idleDetector); connect(m_idleDetector, SIGNAL(maybeIdle()), SLOT(slotMaybeIdle())); setHttpActionsVisible(Lotsofcake::Configuration().isConfigured()); // add default plugin path for deployment QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath() + QStringLiteral("/plugins")); if (QCoreApplication::applicationDirPath().endsWith(QLatin1String("MacOS"))) QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath() + QStringLiteral("/../plugins")); // set up command interface #ifdef CHARM_CI_SUPPORT m_cmdInterface = new CharmCommandInterface(this); #endif // CHARM_CI_SUPPORT // Ladies and gentlemen, please raise upon your seats - // the show is about to begin: emit goToState(StartingUp); } ApplicationCore::~ApplicationCore() { m_instance = nullptr; } void ApplicationCore::slotPopulateTrayIconMenu() { const auto newActions = m_timeTracker.menu()->actions(); if (m_taskActions == newActions) return; for (const auto action : m_taskActions) m_systrayContextMenu.removeAction(action); m_taskActions = newActions; m_systrayContextMenu.insertActions(m_systrayContextMenu.actions().first(), m_taskActions); } void ApplicationCore::slotHandleUniqueApplicationConnection() { QLocalSocket *socket = m_uniqueApplicationServer.nextPendingConnection(); connect(socket, &QLocalSocket::readyRead, socket, [this, socket](){ if (!socket->canReadLine()) return; while (socket->canReadLine()) { const QByteArray data = socket->readLine().trimmed(); if (data.startsWith(StartTaskCommand)) { bool ok = true; const TaskId id = data.mid(StartTaskCommand.length()).toInt(&ok); if (ok) { m_timeTracker.slotStartEvent(id); } else { qWarning() << "Received invalid argument:" << data; } } else if (data.startsWith(RaiseWindowCommand)) { // nothing to do, see below } } socket->deleteLater(); showMainWindow(ApplicationCore::ShowMode::ShowAndRaise); }); } void ApplicationCore::showMainWindow(ShowMode mode) { m_timeTracker.show(); m_timeTracker.setWindowState(m_timeTracker.windowState() & ~Qt::WindowMinimized); if (mode == ShowMode::ShowAndRaise) { m_timeTracker.raise(); m_timeTracker.activateWindow(); #ifdef Q_OS_WIN //krazy:cond=captruefalse,null int idActive = GetWindowThreadProcessId(GetForegroundWindow(), NULL); int threadId = GetCurrentThreadId(); if (AttachThreadInput(threadId, idActive, TRUE) != 0) { HWND wid = reinterpret_cast(m_timeTracker.winId()); SetForegroundWindow(wid); SetFocus(wid); AttachThreadInput(threadId, idActive, FALSE); } //krazy:endcond=captruefalse,null #endif } } void ApplicationCore::createWindowMenu(QMenuBar *menuBar) { auto menu = new QMenu(menuBar); menu->setTitle(tr("Window")); menu->addAction(tr("Show Tasks Editor Window"), this, SLOT(slotShowTasksEditor()), QKeySequence(tr("Ctrl+1"))); menu->addAction(tr("Show Event Editor Window"), this, SLOT(slotShowEventEditor()), QKeySequence(tr("Ctrl+2"))); menu->addSeparator(); menu->addAction(&m_actionEnterVacation); menu->addSeparator(); menu->addAction(&m_actionActivityReport); menu->addAction(&m_actionWeeklyTimesheetReport); menu->addAction(&m_actionMonthlyTimesheetReport); #ifndef Q_OS_OSX menu->addSeparator(); #endif menu->addAction(&m_actionPreferences); menuBar->addMenu(menu); } void ApplicationCore::createFileMenu(QMenuBar *menuBar) { auto menu = new QMenu(menuBar); menu->setTitle(tr("File")); menu->addAction(&m_actionImportFromXml); menu->addAction(&m_actionExportToXml); menu->addSeparator(); menu->addAction(&m_actionSyncTasks); menu->addAction(&m_actionImportTasks); menu->addAction(&m_actionExportTasks); #ifdef Q_OS_OSX if (!CharmUpdateCheckUrl().isEmpty()) { menu->addSeparator(); menu->addAction(&m_actionCheckForUpdates); } #else menu->addSeparator(); // Separator before quit #endif menu->addAction(&m_actionQuit); menuBar->addMenu(menu); } void ApplicationCore::createHelpMenu(QMenuBar *menuBar) { auto menu = new QMenu(menuBar); menu->setTitle(tr("Help")); menu->addAction(&m_actionAboutDialog); #ifdef Q_OS_WIN if (!CharmUpdateCheckUrl().isEmpty()) menu->addAction(&m_actionCheckForUpdates); #endif menuBar->addMenu(menu); } CharmWindow &ApplicationCore::mainView() { m_timeTracker.setHideAtStartup(m_hideAtStart); return m_timeTracker; } void ApplicationCore::setState(State state) { if (m_state == state) return; qDebug() << "ApplicationCore::setState: going from" << StateNames[m_state] << "to" << StateNames[state]; State previous = m_state; try { switch (m_state) { case Constructed: break; // ignore, but this state is never re-entered case StartingUp: leaveStartingUpState(); break; case Configuring: leaveConfiguringState(); break; case Connecting: leaveConnectingState(); break; case Connected: leaveConnectedState(); break; case Disconnecting: leaveDisconnectingState(); break; case ShuttingDown: leaveShuttingDownState(); break; default: Q_ASSERT_X(false, "ApplicationCore::setState", "Unknown previous application state"); } m_state = state; Q_FOREACH (auto e, m_uiElements) e->stateChanged(m_state); switch (m_state) { case StartingUp: m_model.charmDataModel()->stateChanged(previous, state); m_controller.stateChanged(previous, state); // FIXME unnecessary? // m_mainWindow.stateChanged(previous); // m_timeTracker.stateChanged( previous ); enterStartingUpState(); break; case Configuring: enterConfiguringState(); break; case Connecting: m_model.charmDataModel()->stateChanged(previous, state); m_controller.stateChanged(previous, state); // FIXME unnecessary? // m_mainWindow.stateChanged(previous); // m_timeTracker.stateChanged( previous ); enterConnectingState(); break; case Connected: m_model.charmDataModel()->stateChanged(previous, state); m_controller.stateChanged(previous, state); // FIXME unnecessary? // m_mainWindow.stateChanged(previous); // m_timeTracker.stateChanged( previous ); enterConnectedState(); break; case Disconnecting: // FIXME unnecessary? // m_timeTracker.stateChanged( previous ); // m_mainWindow.stateChanged(previous); m_model.charmDataModel()->stateChanged(previous, state); m_controller.stateChanged(previous, state); enterDisconnectingState(); break; case ShuttingDown: // FIXME unnecessary? // m_timeTracker.stateChanged( previous ); // m_mainWindow.stateChanged(previous); m_model.charmDataModel()->stateChanged(previous, state); m_controller.stateChanged(previous, state); enterShuttingDownState(); break; default: Q_ASSERT_X(false, "ApplicationCore::setState", "Unknown new application state"); } } catch (const CharmException &e) { showCritical(tr("Critical Charm Problem"), e.what()); QCoreApplication::quit(); } } State ApplicationCore::state() const { return m_state; } ApplicationCore &ApplicationCore::instance() { Q_ASSERT_X(m_instance, "ApplicationCore::instance", "Singleton not constructed yet"); return *m_instance; } bool ApplicationCore::hasInstance() { return m_instance != nullptr; } void ApplicationCore::enterStartingUpState() { emit goToState(Configuring); } void ApplicationCore::leaveStartingUpState() { } void ApplicationCore::enterConfiguringState() { if (configure()) { // if all ok, go to connecting state emit goToState(Connecting); } else { // user has cancelled configure, exit the application QCoreApplication::quit(); } } void ApplicationCore::leaveConfiguringState() { } void ApplicationCore::showCritical(const QString &title, const QString &message) { QMessageBox::critical(&mainView(), title, message); } void ApplicationCore::showInformation(const QString &title, const QString &message) { QMessageBox::information(&mainView(), title, message); } void ApplicationCore::enterConnectingState() { try { if (!m_controller.initializeBackEnd(CHARM_SQLITE_BACKEND_DESCRIPTOR)) QCoreApplication::quit(); } catch (const CharmException &e) { showCritical(tr("Database Backend Error"), tr("The backend could not be initialized: %1").arg(e.what())); slotQuitApplication(); return; } // tell storage to connect to database CONFIGURATION.failure = false; try { if (m_controller.connectToBackend()) { // delay switch to Connected state a bit to show the start screen: QTimer::singleShot(0, this, SLOT(slotGoToConnectedState())); } else { // go back to StartingUp state and reconfigure emit goToState(StartingUp); } } catch (const UnsupportedDatabaseVersionException &e) { qDebug() << e.what(); QFileInfo info(Configuration::instance().localStorageDatabase); QString message = QObject::tr("" "

Your current Charm database is not supported by this version. " "The error message is: %1." "You have two options here:

    " "
  • Start over with an empty database by moving or deleting your %2 folder " "then re-running this version of Charm.
  • " "
  • Load an older version of Charm that supports your current database and select " "File->Export, and save that file somewhere. Then, either rename or delete your " "%2 folder and restart this version of Charm and select File->Import from " "previous export and select the file you saved in the previous step.
  • " "
").arg( e.what().toHtmlEscaped(), info.absoluteDir().path()); showCritical(QObject::tr("Charm Database Error"), message); slotQuitApplication(); return; } } void ApplicationCore::leaveConnectingState() { } void ApplicationCore::enterConnectedState() { if (m_startupTask != -1) m_timeTracker.slotStartEvent(m_startupTask); #ifdef Q_OS_WIN updateTaskList(); #endif #ifdef CHARM_CI_SUPPORT m_cmdInterface->start(); #endif } void ApplicationCore::leaveConnectedState() { #ifdef CHARM_CI_SUPPORT m_cmdInterface->stop(); #endif m_controller.persistMetaData(CONFIGURATION); } void ApplicationCore::enterDisconnectingState() { // just wait for controller to emit readyToQuit() } void ApplicationCore::leaveDisconnectingState() { } void ApplicationCore::enterShuttingDownState() { QTimer::singleShot(0, qApp, SLOT(quit())); } void ApplicationCore::leaveShuttingDownState() { } void ApplicationCore::slotGoToConnectedState() { if (state() == Connecting) emit goToState(Connected); } static QString charmDataDir() { const QByteArray charmHome = qgetenv("CHARM_HOME"); if (!charmHome.isEmpty()) return QFile::decodeName(charmHome) + QLatin1String("/data/"); return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/'); } bool ApplicationCore::configure() { if (CONFIGURATION.failure == true) { qDebug() << "ApplicationCore::configure: an error was found within the configuration."; if (!CONFIGURATION.failureMessage.isEmpty()) { showInformation(tr("Configuration Problem"), CONFIGURATION.failureMessage); CONFIGURATION.failureMessage.clear(); } } // load configuration: QSettings settings; settings.beginGroup(CONFIGURATION.configurationName); bool configurationComplete = CONFIGURATION.readFrom(settings); if (!configurationComplete || CONFIGURATION.failure) { qDebug() << "ApplicationCore::configure: no complete configuration found for configuration name" << CONFIGURATION.configurationName; // FIXME maybe move to Configuration::loadDefaults const QString storageDatabaseDirectory = charmDataDir(); const QString storageDatabaseFileRelease = QStringLiteral("Charm.db"); const QString storageDatabaseFileDebug = QStringLiteral("Charm_debug.db"); const QString storageDatabaseRelease = storageDatabaseDirectory + storageDatabaseFileRelease; const QString storageDatabaseDebug = storageDatabaseDirectory + storageDatabaseFileDebug; QString storageDatabase; #ifdef NDEBUG Q_UNUSED(storageDatabaseDebug); storageDatabase = storageDatabaseRelease; #else Q_UNUSED(storageDatabaseRelease); storageDatabase = storageDatabaseDebug; #endif CONFIGURATION.localStorageDatabase = QDir::toNativeSeparators(storageDatabase); ConfigurationDialog dialog(CONFIGURATION, &mainView()); if (dialog.exec()) { CONFIGURATION = dialog.configuration(); CONFIGURATION.writeTo(settings); mainView().show(); } else { qDebug() << "ApplicationCore::configure: user cancelled configuration. Exiting."; // quit(); return false; } } return true; } QString ApplicationCore::titleString(const QString &text) const { QString dbInfo; const QString userName = CONFIGURATION.user.name(); if (!text.isEmpty()) { if (!userName.isEmpty()) { dbInfo = QStringLiteral("%1 - %2").arg(userName, text); } else { dbInfo = text; } return tr("Charm (%1)").arg(dbInfo); } else { return tr("Charm"); } } void ApplicationCore::slotCurrentBackendStatusChanged(const QString &text) { const QString title = titleString(text); // FIXME why can't this be done on stateChanged()? and if not, is // maybe an app-wide metadataChanged() or configurationChanged() // missing? (the latter exists) // MIRKO_TEMP_REM /* m_mainWindow.setWindowTitle( title ); */ m_trayIcon.setToolTip(title); } void ApplicationCore::slotStopAllTasks() { DATAMODEL->endAllEventsRequested(); } void ApplicationCore::slotQuitApplication() { emit goToState(Disconnecting); } void ApplicationCore::slotControllerReadyToQuit() { emit goToState(ShuttingDown); } void ApplicationCore::slotSaveConfiguration() { QSettings settings; settings.beginGroup(CONFIGURATION.configurationName); CONFIGURATION.writeTo(settings); if (state() == Connected) { m_controller.persistMetaData(CONFIGURATION); #ifdef CHARM_CI_SUPPORT m_cmdInterface->configurationChanged(); #endif } Q_FOREACH (auto e, m_uiElements) e->configurationChanged(); } ModelConnector &ApplicationCore::model() { return m_model; } DateChangeWatcher *ApplicationCore::dateChangeWatcher() const { return m_dateChangeWatcher; } IdleDetector *ApplicationCore::idleDetector() { return m_idleDetector; } CharmCommandInterface *ApplicationCore::commandInterface() const { return m_cmdInterface; } void ApplicationCore::setHttpActionsVisible(bool visible) { m_actionSyncTasks.setVisible(visible); } void ApplicationCore::slotMaybeIdle() { if (DATAMODEL->activeEventCount() > 0) m_timeTracker.maybeIdle(idleDetector()); // there are four parameters to the idle property: // - the initial start time of the currently active event(s) // - the time the machine went idle // - the time it resumed from idling // - the current time // all this information is available in the data model, plus the // argument to this call // things that make it complicated: // - there may be multiple active events // - there may be multiple idle periods before the user deals with // it } TrayIcon &ApplicationCore::trayIcon() { return m_trayIcon; } void ApplicationCore::updateTaskList() { #ifdef Q_OS_WIN const auto recentData = DATAMODEL->mostRecentlyUsedTasks(); auto recentJumpList = m_windowsJumpList->recent(); recentJumpList->clear(); int count = 0; Q_FOREACH (const auto &id, recentData) { if (count++ > 5) break; recentJumpList->addLink(Data::goIcon(), DATAMODEL->getTask( id).name(), qApp->applicationFilePath(), { QLatin1String("--start-task"), QString::number(id) }); } recentJumpList->setVisible(true); #endif } void ApplicationCore::saveState(QSessionManager &manager) { Q_UNUSED(manager) } void ApplicationCore::commitData(QSessionManager &manager) { Q_UNUSED(manager) // Before QApplication closes all windows, save their state. // Doing this in saveState is too late because then we would store that they are all hidden. if (m_state == Connected) { Q_FOREACH (auto e, m_uiElements) e->saveGuiState(); } } void ApplicationCore::slotShowNotification(const QString &title, const QString &message) { if (m_trayIcon.isSystemTrayAvailable() && m_trayIcon.supportsMessages()) { m_trayIcon.showMessage(title, message); } else { NotificationPopup *popup = new NotificationPopup(nullptr); popup->showNotification(title, message); } } void ApplicationCore::slotShowTasksEditor() { CharmWindow::showView(&m_tasksView); } void ApplicationCore::slotShowEventEditor() { CharmWindow::showView(&m_eventView); } Charm-1.12.0/Charm/ApplicationCore.h000066400000000000000000000127751331066577000171300ustar00rootroot00000000000000/* ApplicationCore.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef APPLICATIONCORE_H #define APPLICATIONCORE_H #include #include #include // this is an application, not a library: // no pimpling, and data members instead of forward declarations #include "Core/User.h" #include "Core/State.h" #include "Core/TimeSpans.h" #include "Core/Controller.h" #include "Core/Configuration.h" #include "Core/SqlStorage.h" #include "Widgets/CharmWindow.h" #include "Widgets/EventView.h" #include "Widgets//TimeTrackingWindow.h" #include "Widgets/TasksView.h" #include "Widgets/TrayIcon.h" #include "ModelConnector.h" // FIXME read configuration name from command line class CharmCommandInterface; class IdleDetector; class QSessionManager; class QWinJumpList; class ApplicationCore : public QObject { Q_OBJECT public: enum class ShowMode { Show, ShowAndRaise }; explicit ApplicationCore(TaskId startupTask, bool hideAtStart, QObject *parent = nullptr); ~ApplicationCore() override; static ApplicationCore &instance(); static bool hasInstance(); // FIXME broken by design? /** Configure the application. Returns true if configuring failed. The application can only leave StartingUp state once a valid configuration is available. */ bool configure(); /** Access to the model. */ ModelConnector &model(); /** Access to the time spans object. */ DateChangeWatcher *dateChangeWatcher() const; IdleDetector *idleDetector(); CharmCommandInterface *commandInterface() const; State state() const; void createWindowMenu(QMenuBar *menuBar); void createFileMenu(QMenuBar *menuBar); void createHelpMenu(QMenuBar *menuBar); /** The main view is the window responsible for managing state during command execution. * It is an internal concept, not a notion for the end user. */ CharmWindow &mainView(); TrayIcon &trayIcon(); void updateTaskList(); public Q_SLOTS: void showMainWindow(ShowMode mode = ShowMode::Show); void setState(State state); void slotStopAllTasks(); void slotQuitApplication(); void slotControllerReadyToQuit(); void slotSaveConfiguration(); void slotGoToConnectedState(); void setHttpActionsVisible(bool visible); void saveState(QSessionManager &manager); void commitData(QSessionManager &manager); private Q_SLOTS: void slotCurrentBackendStatusChanged(const QString &text); void slotMaybeIdle(); void slotHandleUniqueApplicationConnection(); void slotPopulateTrayIconMenu(); void slotShowNotification(const QString &title, const QString &message); void slotShowTasksEditor(); void slotShowEventEditor(); Q_SIGNALS: void goToState(State state); protected: QAction m_actionStopAllTasks; TimeTrackingWindow m_timeTracker; QAction m_actionQuit; private: void showCritical(const QString &title, const QString &message); void showInformation(const QString &title, const QString &message); QString titleString(const QString &text) const; void enterStartingUpState(); void leaveStartingUpState(); void enterConfiguringState(); void leaveConfiguringState(); void enterConnectingState(); void leaveConnectingState(); void enterConnectedState(); void leaveConnectedState(); void enterDisconnectingState(); void leaveDisconnectingState(); void enterShuttingDownState(); void leaveShuttingDownState(); State m_state = Constructed; ModelConnector m_model; Controller m_controller; TrayIcon m_trayIcon; QMenu m_systrayContextMenu; QAction m_actionAboutDialog; QAction m_actionPreferences; QAction m_actionExportToXml; QAction m_actionImportFromXml; QAction m_actionSyncTasks; QAction m_actionImportTasks; QAction m_actionExportTasks; QAction m_actionCheckForUpdates; QAction m_actionEnterVacation; QAction m_actionActivityReport; QAction m_actionWeeklyTimesheetReport; QAction m_actionMonthlyTimesheetReport; QList m_taskActions; EventView m_eventView; TasksView m_tasksView; QVector m_uiElements; IdleDetector *m_idleDetector = nullptr; CharmCommandInterface *m_cmdInterface = nullptr; QLocalServer m_uniqueApplicationServer; TaskId m_startupTask; bool m_hideAtStart; #ifdef Q_OS_WIN QWinJumpList *m_windowsJumpList = nullptr; #endif // All statics are created as members of Application. This is // supposed to help on Windows, where constructors for statics // do not seem to called correctly. DateChangeWatcher *m_dateChangeWatcher; static ApplicationCore *m_instance; }; #endif Charm-1.12.0/Charm/CI/000077500000000000000000000000001331066577000141625ustar00rootroot00000000000000Charm-1.12.0/Charm/CI/CharmCommandInterface.cpp000066400000000000000000000050271331066577000210440ustar00rootroot00000000000000/* CharmCommandInterface.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmCommandInterface.h" #include "Core/CharmConstants.h" #include "CharmCommandServer.h" #include "CharmCMake.h" #ifndef CHARM_CI_SUPPORT #error Build system error: CHARM_CI_SUPPORT should be defined #endif #ifdef CHARM_CI_TCPSERVER # include "CharmTCPCommandServer.h" #endif #ifdef CHARM_CI_LOCALSERVER # include "CharmLocalCommandServer.h" #endif CharmCommandInterface::CharmCommandInterface(QObject *parent) : QObject(parent) { } CharmCommandInterface::~CharmCommandInterface() { stop(); } bool CharmCommandInterface::isStarted() const { return !m_servers.isEmpty(); } void CharmCommandInterface::start() { if (!CONFIGURATION.enableCommandInterface || isStarted()) return; // Create command line interface servers // #ifdef CHARM_CI_TCPSERVER m_servers.append(new CharmTCPCommandServer); #endif #ifdef CHARM_CI_LOCALSERVER m_servers.append(new CharmLocalCommandServer); #endif if (m_servers.isEmpty()) { qDebug("No command interface servers available!"); return; } qDebug("Starting command interface servers..."); foreach (CharmCommandServer *server, m_servers) server->listen(); } void CharmCommandInterface::stop() { if (!isStarted()) return; qDebug("Stopping command interface servers..."); foreach (CharmCommandServer *server, m_servers) { server->close(); server->deleteLater(); } m_servers.clear(); } void CharmCommandInterface::configurationChanged() { if (CONFIGURATION.enableCommandInterface && !isStarted()) { start(); } else if (!CONFIGURATION.enableCommandInterface && isStarted()) { stop(); } } Charm-1.12.0/Charm/CI/CharmCommandInterface.h000066400000000000000000000025521331066577000205110ustar00rootroot00000000000000/* CharmCommandInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_CI_CHARMCOMMANDINTERFACE_H #define CHARM_CI_CHARMCOMMANDINTERFACE_H #include class CharmCommandServer; class CharmCommandInterface : public QObject { Q_OBJECT public: explicit CharmCommandInterface(QObject *parent = nullptr); ~CharmCommandInterface(); bool isStarted() const; void start(); void stop(); public Q_SLOTS: void configurationChanged(); private: QList m_servers; }; #endif // CHARM_CI_CHARMCOMMANDINTERFACE_H Charm-1.12.0/Charm/CI/CharmCommandProtocol.h000066400000000000000000000035771331066577000204220ustar00rootroot00000000000000/* CharmCommandProtocol.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_CI_CHARMCOMMANDPROTOCOL_H #define CHARM_CI_CHARMCOMMANDPROTOCOL_H #define CHARM_CI_VERSION 0x0001 #define CHARM_CI_COMMAND_DISCONNECT "BYE" #define CHARM_CI_COMMAND_RECENT "RECENT" #define CHARM_CI_COMMAND_START "START" #define CHARM_CI_COMMAND_STATUS "STATUS" #define CHARM_CI_COMMAND_STOP "STOP" #define CHARM_CI_COMMAND_TASK "TASK" #define CHARM_CI_EVENT_TASK_ACTIVATED "TASK ACTIVATED" #define CHARM_CI_EVENT_TASK_ADDED "TASK ADDED" #define CHARM_CI_EVENT_TASK_DEACTIVATED "TASK DEACTIVATED" #define CHARM_CI_EVENT_TASK_MODIFIED "TASK MODIFIED" #define CHARM_CI_EVENT_TASK_RESET "TASK RESET" #define CHARM_CI_HANDSHAKE_RECV "READY" #define CHARM_CI_HANDSHAKE_SEND "HELLO CI" #define CHARM_CI_SERVER_ACK "ACK" #define CHARM_CI_SERVER_COMMENT "*" #define CHARM_CI_SERVER_NAK "NAK" #endif // CHARM_CI_CHARMCOMMANDPROTOCOL_H Charm-1.12.0/Charm/CI/CharmCommandServer.cpp000066400000000000000000000026271331066577000204150ustar00rootroot00000000000000/* CharmCommandServer.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmCommandServer.h" #include "CharmCommandSession.h" #include "CharmCMake.h" #ifndef CHARM_CI_SUPPORT #error Build system error: CHARM_CI_SUPPORT should be defined #endif CharmCommandServer::CharmCommandServer(QObject *parent) : QObject(parent) { } CharmCommandServer::~CharmCommandServer() { } void CharmCommandServer::spawnSession(QIODevice *device) { CharmCommandSession *session = new CharmCommandSession(this); session->setDevice(device); connect(device, SIGNAL(disconnected()), session, SLOT(deleteLater())); } Charm-1.12.0/Charm/CI/CharmCommandServer.h000066400000000000000000000024311331066577000200530ustar00rootroot00000000000000/* CharmCommandServer.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_CI_CHARMCOMMANDSERVER_H #define CHARM_CI_CHARMCOMMANDSERVER_H #include class QIODevice; class CharmCommandServer : public QObject { Q_OBJECT public: explicit CharmCommandServer(QObject *parent = nullptr); ~CharmCommandServer(); virtual bool listen() = 0; virtual void close() = 0; protected: void spawnSession(QIODevice *device); }; #endif // CHARM_CI_CHARMCOMMANDSERVER_H Charm-1.12.0/Charm/CI/CharmCommandSession.cpp000066400000000000000000000245031331066577000205670ustar00rootroot00000000000000/* CharmCommandSession.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmCommandSession.h" #include #include #include #include "Core/CharmDataModel.h" #include "ViewHelpers.h" #include "CharmCommandProtocol.h" #include "CharmCMake.h" #ifndef CHARM_CI_SUPPORT #error Build system error: CHARM_CI_SUPPORT should be defined #endif CharmCommandSession::CharmCommandSession(QObject *parent) : QObject(parent) , m_device(nullptr) , m_state(InvalidState) { qDebug("Command interface created."); DATAMODEL->registerAdapter(this); } CharmCommandSession::~CharmCommandSession() { DATAMODEL->unregisterAdapter(this); qDebug("Command interface destroyed."); } QIODevice *CharmCommandSession::device() const { return m_device; } void CharmCommandSession::setDevice(QIODevice *device) { if (m_device) disconnect(m_device, SIGNAL(readyRead()), this, SLOT(onReadyRead())); m_device = device; if (m_device) connect(m_device, SIGNAL(readyRead()), this, SLOT(onReadyRead())); reset(); } void CharmCommandSession::resetTasks() { if (!m_device) return; m_device->write(CHARM_CI_EVENT_TASK_RESET); m_device->write("\n"); } void CharmCommandSession::taskAdded(TaskId id) { if (!m_device) return; m_device->write(QStringLiteral("%1 %2\n") .arg(QStringLiteral(CHARM_CI_EVENT_TASK_ADDED)) .arg(DATAMODEL->taskIdAndSmartNameString(id)) .toLatin1()); } void CharmCommandSession::taskModified(TaskId id) { if (!m_device) return; m_device->write(QStringLiteral("%1 %2\n") .arg(QStringLiteral(CHARM_CI_EVENT_TASK_MODIFIED)) .arg(DATAMODEL->taskIdAndSmartNameString(id)) .toLatin1()); } void CharmCommandSession::eventActivated(EventId id) { if (!m_device) return; const Event &event = DATAMODEL->eventForId(id); m_device->write(QStringLiteral("%1 %2\n") .arg(QStringLiteral(CHARM_CI_EVENT_TASK_ACTIVATED)) .arg(DATAMODEL->taskIdAndSmartNameString(event.taskId())) .toLatin1()); } void CharmCommandSession::eventDeactivated(EventId id) { if (!m_device) return; const Event &event = DATAMODEL->eventForId(id); m_device->write(QStringLiteral("%1 %2\n") .arg(QStringLiteral(CHARM_CI_EVENT_TASK_DEACTIVATED)) .arg(DATAMODEL->taskIdAndSmartNameString(event.taskId())) .toLatin1()); } void CharmCommandSession::reset() { m_state = InvalidState; startHandshake(); } void CharmCommandSession::onReadyRead() { QByteArray payload = m_device->readAll(); switch (m_state) { case HandshakeState: handleHandshare(payload); break; case CommandState: handleCommand(payload); break; case InvalidState: qDebug("Received data while in invalid state. Discarding."); break; } } void CharmCommandSession::sendAck(const QString &comment) { m_device->write(QStringLiteral("%1 %2\n") .arg(QStringLiteral(CHARM_CI_SERVER_ACK)) .arg(comment) .toLatin1()); } void CharmCommandSession::sendNak(const QString &comment) { m_device->write(QStringLiteral("%1 %2\n") .arg(QStringLiteral(CHARM_CI_SERVER_NAK)) .arg(comment) .toLatin1()); } void CharmCommandSession::sendComment(const QString &comment) { m_device->write(QStringLiteral("%1 %2\n") .arg(QStringLiteral(CHARM_CI_SERVER_COMMENT)) .arg(comment) .toLatin1()); } void CharmCommandSession::startHandshake() { sendComment(QStringLiteral("Charm Command Line Interface")); m_device->write(QStringLiteral("%1 %2\n") .arg(QStringLiteral(CHARM_CI_HANDSHAKE_SEND)) .arg(QString::number(CHARM_CI_VERSION)) .toLatin1()); m_state = HandshakeState; } void CharmCommandSession::startCommand() { /* * simulate event activated events */ EventIdList activeEvents = DATAMODEL->activeEvents(); foreach (EventId id, activeEvents) eventActivated(id); m_state = CommandState; } void CharmCommandSession::handleHandshare(QByteArray payload) { QBuffer buffer(&payload); buffer.open(QIODevice::ReadOnly); if (!buffer.canReadLine()) { buffer.close(); return; } QString reply = QLatin1String(buffer.readLine()); if (reply.startsWith(QStringLiteral(CHARM_CI_HANDSHAKE_RECV), Qt::CaseInsensitive)) { sendAck(QStringLiteral("Entering Command Mode")); startCommand(); } else if (reply.startsWith(QStringLiteral(CHARM_CI_COMMAND_DISCONNECT), Qt::CaseInsensitive)) { qDebug("BYE command received. Closing connection."); m_device->close(); } } void CharmCommandSession::handleCommand(QByteArray payload) { QBuffer buffer(&payload); buffer.open(QIODevice::ReadOnly); if (!buffer.canReadLine()) { buffer.close(); return; } const QString command = QLatin1String(buffer.readLine().trimmed()); const QStringList segment = command.split(QChar::Space, QString::SkipEmptyParts); if (segment.isEmpty()) { qDebug("Received empty command..."); return; } if (segment[0].compare(QStringLiteral(CHARM_CI_COMMAND_START), Qt::CaseInsensitive) == 0) { bool tid_ok; TaskId tid; if (segment.count() == 2) { tid = segment[1].toInt(&tid_ok); } else { EventMap::const_reverse_iterator i = DATAMODEL->eventMap().rbegin(); tid_ok = (i != DATAMODEL->eventMap().rend()); tid = i->second.taskId(); } if (tid_ok && DATAMODEL->taskExists(tid)) { if (!DATAMODEL->isTaskActive(tid)) { qDebug("START command received. Starting task %d", tid); DATAMODEL->startEventRequested(DATAMODEL->getTask(tid)); } } else { sendNak(QStringLiteral("UNKNOWN TASK")); } } else if (segment[0].compare(QStringLiteral(CHARM_CI_COMMAND_STOP), Qt::CaseInsensitive) == 0) { bool tid_ok; TaskId tid; if (segment.count() == 2) { tid = segment[1].toInt(&tid_ok); } else { tid = DATAMODEL->activeEventCount() > 0 ? DATAMODEL->eventForId(DATAMODEL->activeEvents().last()).taskId() : 0; tid_ok = (tid > 0); } if (tid_ok && DATAMODEL->taskExists(tid) && DATAMODEL->isTaskActive(tid)) { qDebug("STOP command received. Stopping task %d", tid); DATAMODEL->endEventRequested(DATAMODEL->getTask(tid)); } else { sendNak(QStringLiteral("UNKNOWN TASK")); } } else if (segment[0].compare(QStringLiteral(CHARM_CI_COMMAND_TASK), Qt::CaseInsensitive) == 0) { bool tid_ok; TaskId tid; if (segment.count() == 2) { tid = segment[1].toInt(&tid_ok); } else { tid_ok = false; } if (tid_ok && DATAMODEL->taskExists(tid)) { qDebug("TASK command received. Task %d requested", tid); m_device->write(DATAMODEL->taskIdAndSmartNameString(tid).toLatin1()); m_device->write("\n"); } else { sendNak(QStringLiteral("UNKNOWN TASK")); } } else if (segment[0].compare(QStringLiteral(CHARM_CI_COMMAND_STATUS), Qt::CaseInsensitive) == 0) { qDebug("STATUS command received."); const EventIdList activeEvents = DATAMODEL->activeEvents(); if (!activeEvents.isEmpty()) { foreach (EventId id, activeEvents) { const Event &event = DATAMODEL->eventForId(id); m_device->write(QStringLiteral("%0 %1\n") .arg(event.taskId(), 4, 10, QLatin1Char('0')) .arg(event.duration()).toLatin1()); } } else { sendNak(QStringLiteral("WORK HARDER")); } } else if (segment[0].compare(QStringLiteral(CHARM_CI_COMMAND_RECENT), Qt::CaseInsensitive) == 0) { bool offset_ok; bool count_ok; int offset; int count; const EventIdList recent = DATAMODEL->mostRecentlyUsedTasks(); /* default params */ offset_ok = true; offset = 0; count_ok = true; count = 5; if (segment.count() > 1) { offset = segment[1].toInt(&offset_ok); if (segment.count() > 2) count = segment[2].toInt(&count_ok); } if (offset_ok && count_ok && offset >= 0 && count >= 1 && recent.size() > offset) { qDebug("RECENT command received. Sending %d entries starting from offset %d", count, offset); if ((offset + count) > recent.size()) count = recent.size() - offset; for (int i = 0; i < count; ++i) { m_device->write(DATAMODEL->taskIdAndSmartNameString(recent[offset + i]).toLatin1()); m_device->write("\n"); } } else { sendNak(QStringLiteral("INVALID REQUEST")); } } else if (segment[0].compare(QStringLiteral(CHARM_CI_COMMAND_DISCONNECT), Qt::CaseInsensitive) == 0) { qDebug("BYE command received. Closing connection."); m_device->close(); } /* unknown command sent */ else { sendNak(QStringLiteral("UNKNOWN COMMAND")); } } Charm-1.12.0/Charm/CI/CharmCommandSession.h000066400000000000000000000050171331066577000202330ustar00rootroot00000000000000/* CharmCommandSession.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_CI_CHARMCOMMANDSESSION_H #define CHARM_CI_CHARMCOMMANDSESSION_H #include #include "Core/CharmDataModelAdapterInterface.h" class QIODevice; class CharmCommandSession : public QObject, public CharmDataModelAdapterInterface { enum State { InvalidState = 0, HandshakeState = 1, CommandState = 2 }; Q_OBJECT public: explicit CharmCommandSession(QObject *parent = nullptr); ~CharmCommandSession(); QIODevice *device() const; void setDevice(QIODevice *device); public: /* CharmDataModelAdapterInterface */ void resetTasks(); void taskAboutToBeAdded(TaskId, int) { } void taskAdded(TaskId); void taskModified(TaskId); void taskParentChanged(TaskId, TaskId, TaskId) { } void taskAboutToBeDeleted(TaskId) { } void taskDeleted(TaskId) { } void resetEvents() { } void eventAboutToBeAdded(EventId) { } void eventAdded(EventId) { } void eventModified(EventId, Event) { } void eventAboutToBeDeleted(EventId) { } void eventDeleted(EventId) { } void eventActivated(EventId id); void eventDeactivated(EventId id); protected: void reset(); private Q_SLOTS: void onReadyRead(); private: void sendAck(const QString &comment); void sendNak(const QString &comment); void sendComment(const QString &comment); private: void startHandshake(); void startCommand(); void handleHandshare(QByteArray payload); void handleCommand(QByteArray payload); private: QIODevice *m_device; State m_state; }; #endif // CHARM_CI_CHARMCOMMANDSESSION_H Charm-1.12.0/Charm/CI/CharmLocalCommandServer.cpp000066400000000000000000000041401331066577000213600ustar00rootroot00000000000000/* CharmLocalCommandServer.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmLocalCommandServer.h" #include #include #include #include "CharmCommandSession.h" #include "CharmCMake.h" #ifndef CHARM_CI_LOCALSERVER #error Build system error: CHARM_CI_LOCALSERVER should be defined #endif CharmLocalCommandServer::CharmLocalCommandServer(QObject *parent) : CharmCommandServer(parent) , m_server(new QLocalServer(this)) { } CharmLocalCommandServer::~CharmLocalCommandServer() { } bool CharmLocalCommandServer::listen() { const QString name(QDir::tempPath().append(QStringLiteral("/charm.sock"))); #ifdef Q_OS_UNIX QFile::remove(name); // Try to clean up stale socket if possible #endif if (!m_server->listen(name)) { qWarning("Failed to listen on %s", qPrintable(name)); return false; } connect(m_server, SIGNAL(newConnection()), SLOT(onNewConnection())); return true; } void CharmLocalCommandServer::close() { m_server->close(); } void CharmLocalCommandServer::onNewConnection() { while (m_server->hasPendingConnections()) { QLocalSocket *conn = m_server->nextPendingConnection(); Q_ASSERT(conn); qDebug("New Local connection, creating command session..."); spawnSession(conn); } } Charm-1.12.0/Charm/CI/CharmLocalCommandServer.h000066400000000000000000000025501331066577000210300ustar00rootroot00000000000000/* CharmLocalCommandServer.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_CI_CHARMLOCALCOMMANDSERVER_H #define CHARM_CI_CHARMLOCALCOMMANDSERVER_H #include "CharmCommandServer.h" class QLocalServer; class CharmLocalCommandServer : public CharmCommandServer { Q_OBJECT public: explicit CharmLocalCommandServer(QObject *parent = nullptr); ~CharmLocalCommandServer(); bool listen() override; void close() override; private Q_SLOTS: void onNewConnection(); private: QLocalServer *m_server; }; #endif // CHARM_CI_CHARMLOCALSERVER_H Charm-1.12.0/Charm/CI/CharmTCPCommandServer.cpp000066400000000000000000000057611331066577000207660ustar00rootroot00000000000000/* CharmTCPCommandServer.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmTCPCommandServer.h" #include #include #include #include #include "CharmCommandSession.h" #include "CharmCMake.h" #ifndef CHARM_CI_TCPSERVER #error Build system error: CHARM_CI_TCPSERVER should be defined #endif static const quint16 sCharmDefaultPort(5323); static const int sCharmDiscoveryBroadcastRate(5000); CharmTCPCommandServer::CharmTCPCommandServer(QObject *parent) : CharmCommandServer(parent) , m_address(QHostAddress::Any) , m_port(sCharmDefaultPort) , m_server(new QTcpServer(this)) , m_discovery(new QUdpSocket(this)) , m_discoveryTimer(0) { } CharmTCPCommandServer::~CharmTCPCommandServer() { } const QHostAddress &CharmTCPCommandServer::address() const { return m_address; } void CharmTCPCommandServer::setAddress(const QHostAddress &address) { m_address = address; } quint16 CharmTCPCommandServer::port() const { return m_port; } void CharmTCPCommandServer::setPort(quint16 port) { m_port = port; } bool CharmTCPCommandServer::listen() { if (!m_server->listen(m_address, m_port)) { qWarning("Failed to bind to %s:%d", qPrintable(m_address.toString()), m_port); return false; } connect(m_server, SIGNAL(newConnection()), SLOT(onNewConnection())); m_discoveryTimer = startTimer(sCharmDiscoveryBroadcastRate); return true; } void CharmTCPCommandServer::close() { if (m_discoveryTimer) killTimer(m_discoveryTimer), m_discoveryTimer = 0; m_server->close(); } void CharmTCPCommandServer::timerEvent(QTimerEvent *event) { if (event->timerId() != m_discoveryTimer) return; static const char sBroadcastIdentifier[] = "LUCKY"; // CHARMS m_discovery->writeDatagram(sBroadcastIdentifier, sizeof(sBroadcastIdentifier), QHostAddress::Broadcast, m_port); } void CharmTCPCommandServer::onNewConnection() { while (m_server->hasPendingConnections()) { QTcpSocket *conn = m_server->nextPendingConnection(); Q_ASSERT(conn); qDebug("New TCP connection, creating command session..."); spawnSession(conn); } } Charm-1.12.0/Charm/CI/CharmTCPCommandServer.h000066400000000000000000000033021331066577000204200ustar00rootroot00000000000000/* CharmTCPCommandServer.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_CI_CHARMTCPCOMMANDSERVER_H #define CHARM_CI_CHARMTCPCOMMANDSERVER_H #include "CharmCommandServer.h" #include class QTcpServer; class QUdpSocket; class CharmTCPCommandServer : public CharmCommandServer { Q_OBJECT public: explicit CharmTCPCommandServer(QObject *parent = nullptr); ~CharmTCPCommandServer(); const QHostAddress &address() const; void setAddress(const QHostAddress &address); quint16 port() const; void setPort(quint16 port); bool listen() override; void close() override; protected: /* reimpl */ void timerEvent(QTimerEvent *event); private Q_SLOTS: void onNewConnection(); private: QHostAddress m_address; quint16 m_port; QTcpServer *m_server; QUdpSocket *m_discovery; int m_discoveryTimer; }; #endif // CHARM_CI_CHARMTCPSERVER_H Charm-1.12.0/Charm/CMake/000077500000000000000000000000001331066577000146475ustar00rootroot00000000000000Charm-1.12.0/Charm/CMake/Modules/000077500000000000000000000000001331066577000162575ustar00rootroot00000000000000Charm-1.12.0/Charm/CMake/Modules/FindXCB.cmake000066400000000000000000000030451331066577000205000ustar00rootroot00000000000000# Copyright (C) 2015-2018 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # Try to find XCB library and include path. # Once done this will define # # XCB_FOUND # XCB_INCLUDE_PATH # XCB_LIBRARIES # XCB_SCREENSAVER_LIBRARIES if(UNIX AND NOT APPLE) find_path(XCB_INCLUDE_PATH xcb/xcb.h /usr/include DOC "The directory where xcb/xcb.h resides" ) find_library(XCB_LIBRARIES NAMES xcb PATHS /usr/lib DOC "The xcb library" ) find_library(XCB_SCREENSAVER_LIBRARIES NAMES xcb-screensaver PATHS /usr/lib DOC "The xcb-screensaver library" ) endif() if(XCB_INCLUDE_PATH AND XCB_LIBRARIES AND XCB_SCREENSAVER_LIBRARIES) set(XCB_FOUND 1) else() set(XCB_FOUND 0) endif() mark_as_advanced(XCB_INCLUDE_PATH XCB_LIBRARIES XCB_SCREENSAVER_LIBRARIES) Charm-1.12.0/Charm/CMakeLists.txt000066400000000000000000000214351331066577000164340ustar00rootroot00000000000000INCLUDE_DIRECTORIES( ${Charm_SOURCE_DIR} ${Charm_BINARY_DIR} ) SET( CharmApplication_SRCS ApplicationCore.cpp Data.cpp EventModelAdapter.cpp EventModelFilter.cpp GUIState.cpp ModelConnector.cpp ViewFilter.cpp TaskModelAdapter.cpp ViewHelpers.cpp WeeklySummary.cpp UndoCharmCommandWrapper.cpp Commands/CommandRelayCommand.cpp Commands/CommandModifyEvent.cpp Commands/CommandDeleteEvent.cpp Commands/CommandSetAllTasks.cpp Commands/CommandAddTask.cpp Commands/CommandModifyTask.cpp Commands/CommandDeleteTask.cpp Commands/CommandMakeEvent.cpp Commands/CommandExportToXml.cpp Commands/CommandImportFromXml.cpp Commands/CommandMakeAndActivateEvent.cpp HttpClient/CheckForUpdatesJob.cpp HttpClient/GetProjectCodesJob.cpp HttpClient/HttpJob.cpp HttpClient/RestJob.cpp HttpClient/UploadTimesheetJob.cpp Idle/IdleDetector.cpp Lotsofcake/Configuration.cpp Reports/TimesheetInfo.cpp Reports/MonthlyTimesheetXmlWriter.cpp Reports/WeeklyTimesheetXmlWriter.cpp Reports/TimesheetXmlWriter.cpp Widgets/ActivityReport.cpp Widgets/BillDialog.cpp Widgets/CharmPreferences.cpp Widgets/CharmWindow.cpp Widgets/CharmAboutDialog.cpp Widgets/CharmNewReleaseDialog.cpp Widgets/CommentEditorPopup.cpp Widgets/ConfigurationDialog.cpp Widgets/DateEntrySyncer.cpp Widgets/EnterVacationDialog.cpp Widgets/EventEditor.cpp Widgets/EventEditorDelegate.cpp Widgets/EventView.cpp Widgets/ExpandStatesHelper.cpp Widgets/HttpJobProgressDialog.cpp Widgets/IdleCorrectionDialog.cpp Widgets/MessageBox.cpp Widgets/MonthlyTimesheet.cpp Widgets/MonthlyTimesheetConfigurationDialog.cpp Widgets/ReportConfigurationDialog.cpp Widgets/ReportPreviewWindow.cpp Widgets/SelectTaskDialog.cpp Widgets/TaskIdDialog.cpp Widgets/TaskEditor.cpp Widgets/TasksView.cpp Widgets/TasksViewDelegate.cpp Widgets/TimeTrackingView.cpp Widgets/TimeTrackingWindow.cpp Widgets/TimeTrackingTaskSelector.cpp Widgets/TrayIcon.cpp Widgets/Timesheet.cpp Widgets/WeeklyTimesheet.cpp Widgets/NotificationPopup.cpp Widgets/FindAndReplaceEventsDialog.cpp Widgets/WidgetUtils.cpp ) IF (APPLE) LIST(APPEND CharmApplication_SRCS MacApplicationCore.mm) ENDIF() SET(CharmApplication_LIBS) IF (APPLE) LIST(APPEND CharmApplication_SRCS MacApplicationCore.mm) FIND_LIBRARY( COREFOUNDATION_LIBRARY CoreFoundation ) LIST( APPEND CharmApplication_LIBS ${COREFOUNDATION_LIBRARY} ) FIND_LIBRARY( SECURITY_LIBRARY Security ) LIST( APPEND CharmApplication_LIBS ${SECURITY_LIBRARY} ) FIND_LIBRARY( APPKIT_LIBRARY AppKit ) LIST( APPEND CharmApplication_LIBS ${APPKIT_LIBRARY} ) ENDIF() IF( CHARM_CI_SUPPORT ) LIST( APPEND CharmApplication_SRCS CI/CharmCommandInterface.cpp CI/CharmCommandServer.cpp CI/CharmCommandSession.cpp ) IF( CHARM_CI_LOCALSERVER ) LIST( APPEND CharmApplication_SRCS CI/CharmLocalCommandServer.cpp ) ENDIF() IF( CHARM_CI_TCPSERVER ) LIST( APPEND CharmApplication_SRCS CI/CharmTCPCommandServer.cpp ) ENDIF() ENDIF() IF( CHARM_IDLE_DETECTION ) IF( APPLE ) LIST( APPEND CharmApplication_SRCS Idle/MacIdleDetector.mm ) ELSEIF( WIN32 ) LIST( APPEND CharmApplication_SRCS Idle/WindowsIdleDetector.cpp ) ELSEIF( UNIX ) SET( CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/CMake/Modules/ ) IF( Qt5Core_FOUND ) FIND_PACKAGE( XCB ) SET_PACKAGE_PROPERTIES(XCB PROPERTIES DESCRIPTION "Provides idle detection support for X11" URL "http://xcb.freedesktop.org" TYPE RECOMMENDED) IF( XCB_FOUND) INCLUDE_DIRECTORIES( ${XCB_INCLUDE_DIRS} ) LIST( APPEND CharmApplication_LIBS ${XCB_LIBRARIES} ${XCB_SCREENSAVER_LIBRARIES} ) LIST( APPEND CharmApplication_SRCS Idle/X11IdleDetector.cpp ) SET( CHARM_IDLE_DETECTION_AVAILABLE "1" CACHE INTERNAL "" ) ELSE() SET( CHARM_IDLE_DETECTION_AVAILABLE "0" CACHE INTERNAL "" ) MESSAGE( "QT5: Install Xcb headers and library for Xcb idle detection." ) ENDIF() ELSE() FIND_PACKAGE( X11 ) IF( X11_FOUND AND X11_Xscreensaver_LIB) INCLUDE_DIRECTORIES( ${X11_INCLUDE_DIR} ) LIST( APPEND CharmApplication_LIBS ${X11_X11_LIB} ${X11_Xscreensaver_LIB} ) LIST( APPEND CharmApplication_SRCS Idle/X11IdleDetector.cpp ) SET( CHARM_IDLE_DETECTION_AVAILABLE "1" CACHE INTERNAL "" ) ELSE() SET( CHARM_IDLE_DETECTION_AVAILABLE "0" CACHE INTERNAL "" ) MESSAGE( "QT4: Install X11/XScreenSaver headers and library for X11 idle detection." ) ENDIF() ENDIF() ENDIF() ENDIF() QT5_WRAP_UI( UiGenerated_SRCS Widgets/CommentEditorPopup.ui Widgets/ConfigurationDialog.ui Widgets/CharmPreferences.ui Widgets/TaskIdDialog.ui Widgets/TaskEditor.ui Widgets/EnterVacationDialog.ui Widgets/EventEditor.ui Widgets/SelectTaskDialog.ui Widgets/CharmAboutDialog.ui Widgets/CharmNewReleaseDialog.ui Widgets/IdleCorrectionDialog.ui Widgets/ActivityReportConfigurationDialog.ui Widgets/WeeklyTimesheetConfigurationDialog.ui Widgets/MonthlyTimesheetConfigurationDialog.ui Widgets/ReportPreviewWindow.ui Widgets/NotificationPopup.ui Widgets/FindAndReplaceEventsDialog.ui ) QT5_ADD_RESOURCES( Resources_SRCS CharmResources.qrc ) ADD_LIBRARY( CharmApplication STATIC ${CharmApplication_SRCS} ${UiGenerated_SRCS} ) kde_target_enable_exceptions( CharmApplication PUBLIC ) TARGET_LINK_LIBRARIES(CharmApplication ${CharmApplication_LIBS} Qt5::Core Qt5::Widgets Qt5::Xml Qt5::Network Qt5::Sql Qt5::PrintSupport qt5keychain) IF (WIN32) TARGET_LINK_LIBRARIES(CharmApplication Qt5::WinExtras Crypt32) ENDIF() IF(TARGET Qt5::DBus) TARGET_LINK_LIBRARIES(CharmApplication Qt5::DBus) ENDIF() IF (APPLE) FIND_LIBRARY(COREFOUNDATION_LIBRARY CoreFoundation) FIND_LIBRARY(APPKIT_LIBRARY AppKit) FIND_LIBRARY(SECURITY_LIBRARY Security) TARGET_LINK_LIBRARIES(CharmApplication Qt5::MacExtras ${APPKIT_LIBRARY} ${COREFOUNDATION_LIBRARY} ${SECURITY_LIBRARY}) ENDIF() TARGET_INCLUDE_DIRECTORIES(CharmApplication PRIVATE ${QTKEYCHAIN_INCLUDE_DIRS}) SET( Charm_SRCS Charm.cpp ) IF(WIN32) LIST(APPEND Resources_SRCS Charm.rc ) ENDIF() IF( APPLE ) IF( CHARM_MAC_HIGHRES_SUPPORT_ENABLED ) SET( MACOSX_BUNDLE_HIGHRESOLUTION_CAPABLE "true") ELSE() SET( MACOSX_BUNDLE_HIGHRESOLUTION_CAPABLE "false") ENDIF() SET( MACOSX_BUNDLE_INFO_STRING "Charm ${Charm_VERSION}" ) SET( MACOSX_BUNDLE_BUNDLE_VERSION "Charm ${Charm_VERSION}" ) SET( MACOSX_BUNDLE_LONG_VERSION_STRING "Charm ${Charm_VERSION}" ) SET( MACOSX_BUNDLE_SHORT_VERSION_STRING "${Charm_VERSION}" ) SET( MACOSX_BUNDLE_COPYRIGHT "2006-2014 KDAB" ) SET( MACOSX_BUNDLE_ICON_FILE "Charm.icns" ) SET( MACOSX_BUNDLE_GUI_IDENTIFIER "com.kdab" ) SET( MACOSX_BUNDLE_BUNDLE_NAME "Charm" ) SET( RESOURCES "${CMAKE_CURRENT_BINARY_DIR}/Charm.app/Contents/Resources" ) SET( ICON "${ICONS_DIR}/${MACOSX_BUNDLE_ICON_FILE}" ) FILE( MAKE_DIRECTORY ${RESOURCES} ) FILE( COPY ${ICON} DESTINATION ${RESOURCES} ) ENDIF() ADD_EXECUTABLE( ${Charm_EXECUTABLE} ${Charm_SRCS} ${Resources_SRCS} ) kde_target_enable_exceptions( ${Charm_EXECUTABLE} PUBLIC ) TARGET_LINK_LIBRARIES( ${Charm_EXECUTABLE} CharmApplication CharmCore) MESSAGE( STATUS "Charm will be installed to ${CMAKE_INSTALL_PREFIX}" ) IF( UNIX AND NOT APPLE ) INSTALL( FILES charmtimetracker.desktop DESTINATION ${CMAKE_INSTALL_APPDIR}) ecm_install_icons( ICONS Icons/16-apps-Charm.png Icons/32-apps-Charm.png Icons/48-apps-Charm.png Icons/64-apps-Charm.png Icons/128-apps-Charm.png Icons/256-apps-Charm.png DESTINATION ${CMAKE_INSTALL_ICONDIR}) ENDIF() INSTALL( TARGETS ${Charm_EXECUTABLE} ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) IF( APPLE ) SET( EXECUTABLE ${Charm_EXECUTABLE}.app ) set_target_properties( ${Charm_EXECUTABLE} PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/MacOSXBundleInfo.plist.in ) ELSE() SET( EXECUTABLE ${Charm_EXECUTABLE}${CMAKE_EXECUTABLE_SUFFIX} ) ENDIF() IF(NOT WIN32) EXECUTE_PROCESS( COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_BINARY_DIR}/${EXECUTABLE} ${Charm_BINARY_DIR}/${EXECUTABLE} ) ENDIF() Charm-1.12.0/Charm/Charm.cpp000066400000000000000000000145021331066577000154270ustar00rootroot00000000000000/* Charm.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Mike McQuaid Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include "ApplicationCore.h" #include "MacApplicationCore.h" #include "Core/CharmExceptions.h" #include "CharmCMake.h" struct StartupOptions { static std::shared_ptr createApplicationCore(TaskId startupTask, bool hideAtStart) { #ifdef Q_OS_OSX return std::make_shared(startupTask, hideAtStart); #else return std::make_shared(startupTask, hideAtStart); #endif } }; void showCriticalError(const QString &msg) { QMessageBox::critical(nullptr, QObject::tr("Application Error"), msg); using namespace std; cerr << qPrintable(msg) << endl; } int main(int argc, char **argv) { TaskId startupTask = -1; bool hideAtStart = false; #if QT_VERSION < QT_VERSION_CHECK(5, 2, 0) if (argc >= 2) { if (qstrcmp(argv[1], "--version") == 0) { using namespace std; cout << "Charm version " << qPrintable(CharmVersion()) << endl; return 0; } else if (argc == 3 && qstrcmp(argv[1], "--start-task") == 0) { bool ok = true; startupTask = QString::fromLocal8Bit(argv[2]).toInt(&ok); if (!ok || startupTask < 0) { std::cerr << "Invalid task id passed: " << argv[2]; return 1; } } else if (qstrcmp(argv[1], "--hide-at-start") == 0) { hideAtStart = true; } } #endif const QByteArray charmHomeEnv = qgetenv("CHARM_HOME"); if (!charmHomeEnv.isEmpty()) { const QString charmHome = QFile::decodeName(charmHomeEnv); const QString user = charmHome + QLatin1String("/userConfig"); const QString sys = charmHome + QLatin1String("/systemConfig"); QSettings::setPath(QSettings::NativeFormat, QSettings::UserScope, user); QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, user); QSettings::setPath(QSettings::NativeFormat, QSettings::SystemScope, sys); QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, sys); #ifdef Q_OS_WIN // Use ini for storing settings as the registry path is not affected by CHARM_HOME. QSettings::setDefaultFormat(QSettings::IniFormat); #endif } try { #ifdef Q_OS_WIN // High DPI support #if QT_VERSION >= QT_VERSION_CHECK(5, 1, 0) QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); #endif #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) // High DPI support QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); #endif #endif // Q_OS_WIN QApplication app(argc, argv); #if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0) //Now we can use more command line arguments: //charmtimetracker --hide-at-start --start-task 8714 const QCommandLineOption startTaskOption(QLatin1String("start-task"), QLatin1String("Start up the task with "), QLatin1String("task-id")); const QCommandLineOption hideAtStartOption(QLatin1String("hide-at-start"), QLatin1String("Hide Timetracker window at start")); QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); parser.addOption(hideAtStartOption); parser.addOption(startTaskOption); parser.process(app); bool ok = true; if (parser.isSet(startTaskOption)) { const QString value = parser.value(startTaskOption); startupTask = value.toInt(&ok); if (!ok || startupTask < 0) { std::cerr << "Invalid task id passed: " << qPrintable(value) << std::endl; return 1; } } if (parser.isSet(hideAtStartOption)) hideAtStart = true; #endif const std::shared_ptr core(StartupOptions::createApplicationCore(startupTask, hideAtStart)); QObject::connect(&app, &QGuiApplication::commitDataRequest, core.get(), &ApplicationCore::commitData); QObject::connect(&app, &QGuiApplication::saveStateRequest, core.get(), &ApplicationCore::saveState); return app.exec(); } catch (const AlreadyRunningException &) { using namespace std; cout << "Charm already running, exiting..." << endl; return 0; } catch (const CharmException &e) { const QString msg(QObject::tr("An application exception has occurred. Charm will be terminated. The error message was:\n" "%1\n" "Please report this as a bug at https://quality.kdab.com/browse/CHM.").arg( e.what())); showCriticalError(msg); return 1; } catch (...) { const QString msg(QObject::tr("The application terminated with an unexpected exception.\n" "No other information is available to debug this problem.\n" "Please report this as a bug at https://quality.kdab.com/browse/CHM.")); showCriticalError(msg); return 1; } return 0; } Charm-1.12.0/Charm/Charm.rc000066400000000000000000000000761331066577000152520ustar00rootroot00000000000000IDI_ICON1 ICON DISCARDABLE "Icons/Charm.ico" Charm-1.12.0/Charm/CharmResources.qrc000066400000000000000000000027661331066577000173360ustar00rootroot00000000000000 bill.jpg Icons/configure.png Widgets/report_stylesheet.sty Icons/arrow-right.png Icons/document-encrypt.png Icons/document-new.png Icons/document-properties.png Icons/application-exit.png Icons/favorites.png Icons/256-apps-Charm.png Icons/document-close.png Icons/document-edit.png Icons/media-playback-start.png Icons/media-playback-stop.png Icons/128-apps-Charm.png Icons/tray/charmtray_mac.png Icons/tray/charmtrayactive_mac.png Icons/tray/charmtray22.png Icons/tray/charmtrayactive22.png Icons/document-export.png Icons/edit-find-replace.png Charm-1.12.0/Charm/Commands/000077500000000000000000000000001331066577000154305ustar00rootroot00000000000000Charm-1.12.0/Charm/Commands/CommandAddTask.cpp000066400000000000000000000027131331066577000207510ustar00rootroot00000000000000/* CommandAddTask.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandAddTask.h" #include "Core/Controller.h" #include "Core/CommandEmitterInterface.h" CommandAddTask::CommandAddTask(const Task &task, QObject *parent) : CharmCommand(tr("Add Task"), parent) , m_task(task) { } CommandAddTask::~CommandAddTask() { } bool CommandAddTask::prepare() { return true; } bool CommandAddTask::execute(Controller *controller) { m_success = controller->addTask(m_task); return m_success; } bool CommandAddTask::finalize() { if (!m_success) showInformation(tr("Unable to add task"), tr("Adding the task failed.")); return m_success; } Charm-1.12.0/Charm/Commands/CommandAddTask.h000066400000000000000000000025251331066577000204170ustar00rootroot00000000000000/* CommandAddTask.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDADDTASK_H #define COMMANDADDTASK_H #include #include class CommandAddTask : public CharmCommand { Q_OBJECT public: explicit CommandAddTask(const Task &, QObject *parent = nullptr); ~CommandAddTask() override; bool prepare() override; bool execute(Controller *) override; bool finalize() override; private: Task m_task; bool m_success = false; }; #endif Charm-1.12.0/Charm/Commands/CommandDeleteEvent.cpp000066400000000000000000000033301331066577000216360ustar00rootroot00000000000000/* CommandDeleteEvent.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandDeleteEvent.h" #include "Core/Controller.h" CommandDeleteEvent::CommandDeleteEvent(const Event &event, QObject *parent) : CharmCommand(tr("Delete Event"), parent) , m_event(event) { } CommandDeleteEvent::~CommandDeleteEvent() { } bool CommandDeleteEvent::prepare() { return true; } bool CommandDeleteEvent::execute(Controller *controller) { return controller->deleteEvent(m_event); } bool CommandDeleteEvent::rollback(Controller *controller) { int oldId = m_event.id(); m_event = controller->cloneEvent(m_event); int newId = m_event.id(); if (oldId != newId) Q_EMIT emitSlotEventIdChanged(oldId, newId); return m_event.isValid(); } bool CommandDeleteEvent::finalize() { return true; } void CommandDeleteEvent::eventIdChanged(int oid, int nid) { if (m_event.id() == oid) m_event.setId(nid); } Charm-1.12.0/Charm/Commands/CommandDeleteEvent.h000066400000000000000000000026741331066577000213150ustar00rootroot00000000000000/* CommandDeleteEvent.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDDELETEEVENT_H #define COMMANDDELETEEVENT_H #include #include class CommandDeleteEvent : public CharmCommand { Q_OBJECT public: explicit CommandDeleteEvent(const Event &, QObject *parent = nullptr); ~CommandDeleteEvent() override; bool prepare() override; bool execute(Controller *) override; bool rollback(Controller *) override; bool finalize() override; public Q_SLOTS: void eventIdChanged(int, int) override; private: Event m_event; }; #endif Charm-1.12.0/Charm/Commands/CommandDeleteTask.cpp000066400000000000000000000030651331066577000214640ustar00rootroot00000000000000/* CommandDeleteTask.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandDeleteTask.h" #include "ViewHelpers.h" #include "Core/CharmConstants.h" #include "Core/Controller.h" CommandDeleteTask::CommandDeleteTask(const Task &task, QObject *parent) : CharmCommand(tr("Delete Task"), parent) , m_task(task) { } CommandDeleteTask::~CommandDeleteTask() { } bool CommandDeleteTask::prepare() { return true; } bool CommandDeleteTask::execute(Controller *controller) { m_success = controller->deleteTask(m_task); return m_success; } bool CommandDeleteTask::finalize() { if (!m_success) showInformation(tr("Unable to delete task"), tr("Deleting the task failed")); return m_success; } Charm-1.12.0/Charm/Commands/CommandDeleteTask.h000066400000000000000000000025471331066577000211350ustar00rootroot00000000000000/* CommandDeleteTask.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDDELETETASK_H #define COMMANDDELETETASK_H #include #include class CommandDeleteTask : public CharmCommand { Q_OBJECT public: explicit CommandDeleteTask(const Task &, QObject *parent = nullptr); ~CommandDeleteTask() override; bool prepare() override; bool execute(Controller *) override; bool finalize() override; private: Task m_task; bool m_success = false; }; #endif Charm-1.12.0/Charm/Commands/CommandExportToXml.cpp000066400000000000000000000043241331066577000217030ustar00rootroot00000000000000/* CommandExportToXml.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandExportToXml.h" #include "Core/CharmExceptions.h" #include "Core/Controller.h" #include #include #include CommandExportToXml::CommandExportToXml(QString filename, QObject *parent) : CharmCommand(tr("Export to XML"), parent) , m_filename(filename) { } CommandExportToXml::~CommandExportToXml() { } bool CommandExportToXml::prepare() { return true; } bool CommandExportToXml::execute(Controller *controller) { try { QDomDocument document = controller->exportDatabasetoXml(); QFile file(m_filename); if (file.open(QIODevice::WriteOnly)) { QTextStream stream(&file); stream << document.toString(4); } else { m_error = true; m_errorString = tr("Could not open %1 for writing: %2").arg(m_filename, file.errorString()); } } catch (const XmlSerializationException &e) { m_error = true; m_errorString = e.what(); } return true; } bool CommandExportToXml::finalize() { // any errors? if (m_error) showCritical(tr("Error exporting Database to XML"), tr("The database could not be exported:\n%1").arg(m_errorString)); return !m_error; } Charm-1.12.0/Charm/Commands/CommandExportToXml.h000066400000000000000000000025121331066577000213450ustar00rootroot00000000000000/* CommandExportToXml.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDEXPORTTOXML_H #define COMMANDEXPORTTOXML_H #include class QObject; class CommandExportToXml : public CharmCommand { Q_OBJECT public: explicit CommandExportToXml(QString filename, QObject *parent); ~CommandExportToXml() override; bool prepare() override; bool execute(Controller *) override; bool finalize() override; private: bool m_error = false; QString m_errorString; QString m_filename; }; #endif Charm-1.12.0/Charm/Commands/CommandImportFromXml.cpp000066400000000000000000000051131331066577000222120ustar00rootroot00000000000000/* CommandImportFromXml.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandImportFromXml.h" #include "Core/Controller.h" #include #include CommandImportFromXml::CommandImportFromXml(QString filename, QObject *parent) : CharmCommand(tr("Import from XML"), parent) , m_filename(filename) { } CommandImportFromXml::~CommandImportFromXml() { } bool CommandImportFromXml::prepare() { return true; } bool CommandImportFromXml::execute(Controller *controller) { QFile file(m_filename); if (file.open(QIODevice::ReadOnly)) { QDomDocument document; QString errorMessage; int errorLine = 0; int errorColumn = 0; if (document.setContent(&file, &errorMessage, &errorLine, &errorColumn)) { m_error = controller->importDatabaseFromXml(document); } else { m_error = tr("Cannot read the XML syntax of the specified file: [%1:%2] %3").arg(QString::number( errorLine), QString::number( errorColumn), errorMessage); } } else { m_error = tr("Cannot open the specified file: %1").arg(file.errorString()); } return true; } bool CommandImportFromXml::finalize() { // any errors? if (!m_error.isEmpty()) showCritical(tr("Error importing the Database"), tr("An error has occurred:\n%1").arg(m_error)); return true; } Charm-1.12.0/Charm/Commands/CommandImportFromXml.h000066400000000000000000000024661331066577000216670ustar00rootroot00000000000000/* CommandImportFromXml.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDIMPORTFROMXML_H #define COMMANDIMPORTFROMXML_H #include class QObject; class CommandImportFromXml : public CharmCommand { Q_OBJECT public: explicit CommandImportFromXml(QString filename, QObject *parent); ~CommandImportFromXml() override; bool prepare() override; bool execute(Controller *) override; bool finalize() override; private: QString m_error; QString m_filename; }; #endif Charm-1.12.0/Charm/Commands/CommandMakeAndActivateEvent.cpp000066400000000000000000000040041331066577000234140ustar00rootroot00000000000000/* CommandMakeAndActivateEvent.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandMakeAndActivateEvent.h" #include "ModelConnector.h" #include "Core/CharmDataModel.h" #include "Core/CommandEmitterInterface.h" #include "Core/Controller.h" #include CommandMakeAndActivateEvent::CommandMakeAndActivateEvent(const Task &task, QObject *parent) : CharmCommand(tr("Create Event"), parent) , m_task(task) { } CommandMakeAndActivateEvent::~CommandMakeAndActivateEvent() { } bool CommandMakeAndActivateEvent::prepare() { return true; } bool CommandMakeAndActivateEvent::execute(Controller *controller) { m_event = controller->makeEvent(m_task); if (m_event.isValid()) { m_event.setTaskId(m_task.id()); m_event.setStartDateTime(QDateTime::currentDateTime()); return controller->modifyEvent(m_event); } else { return false; } } bool CommandMakeAndActivateEvent::finalize() { if (m_event.isValid()) { ModelConnector *model = dynamic_cast(owner()); Q_ASSERT(model); // this command is "owned" by the model model->charmDataModel()->activateEvent(m_event); return true; } else { return false; } } Charm-1.12.0/Charm/Commands/CommandMakeAndActivateEvent.h000066400000000000000000000027211331066577000230650ustar00rootroot00000000000000/* CommandMakeAndActivateEvent.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDMAKEANDACTIVATEEVENT_H #define COMMANDMAKEANDACTIVATEEVENT_H #include #include #include class QObject; class CommandMakeAndActivateEvent : public CharmCommand { Q_OBJECT public: explicit CommandMakeAndActivateEvent(const Task &, QObject *parent); ~CommandMakeAndActivateEvent() override; bool prepare() override; bool execute(Controller *) override; bool finalize() override; private: Task m_task; // the task we send to the controller Event m_event; // the event the controller has created }; #endif Charm-1.12.0/Charm/Commands/CommandMakeEvent.cpp000066400000000000000000000060311331066577000213120ustar00rootroot00000000000000/* CommandMakeEvent.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandMakeEvent.h" #include "Core/Controller.h" #include "Widgets/EventView.h" #include CommandMakeEvent::CommandMakeEvent(const Task &task, QObject *parent) : CharmCommand(tr("Create Event"), parent) , m_task(task) { } CommandMakeEvent::CommandMakeEvent(const Event &event, QObject *parent) : CharmCommand(tr("Create Event"), parent) , m_event(event) { } CommandMakeEvent::~CommandMakeEvent() { } bool CommandMakeEvent::prepare() { return true; } bool CommandMakeEvent::execute(Controller *controller) { m_rollback = false; if (m_event.id()) { //if it already has an id, this is a redo operation int oid = m_event.id(); m_event = controller->cloneEvent(m_event); int nid = m_event.id(); if (oid != nid) emit emitSlotEventIdChanged(oid, nid); return m_event.isValid(); } Event event = controller->makeEvent(m_task); if (!event.isValid()) return false; QDateTime start(QDateTime::currentDateTime()); event.setStartDateTime(start); event.setEndDateTime(start); if (m_event.startDateTime().isValid()) event.setStartDateTime(m_event.startDateTime()); if (m_event.endDateTime().isValid()) event.setEndDateTime(m_event.endDateTime()); if (!m_event.comment().isEmpty()) event.setComment(m_event.comment()); if (m_event.taskId() != 0) event.setTaskId(m_event.taskId()); if (controller->modifyEvent(event)) { m_event = event; return true; } else { return false; } } bool CommandMakeEvent::rollback(Controller *controller) { m_rollback = true; return controller->deleteEvent(m_event); } bool CommandMakeEvent::finalize() { if (m_rollback) return false; if (m_event.isValid()) { EventView *view = dynamic_cast(owner()); if (view) view->makeVisibleAndCurrent(m_event); emit finishedOk(m_event); return true; } else { return false; } } void CommandMakeEvent::eventIdChanged(int oid, int nid) { if (m_event.id() == oid) m_event.setId(nid); } Charm-1.12.0/Charm/Commands/CommandMakeEvent.h000066400000000000000000000033131331066577000207570ustar00rootroot00000000000000/* CommandMakeEvent.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDMAKEEVENT_H #define COMMANDMAKEEVENT_H #include #include #include class QObject; class CommandMakeEvent : public CharmCommand { Q_OBJECT public: explicit CommandMakeEvent(const Task &task, QObject *parent); explicit CommandMakeEvent(const Event &event, QObject *parent); ~CommandMakeEvent() override; bool prepare() override; bool execute(Controller *) override; bool rollback(Controller *) override; bool finalize() override; public Q_SLOTS: void eventIdChanged(int, int) override; Q_SIGNALS: void finishedOk(const Event &); private: bool m_rollback = false; //don't show the event in finalize Task m_task; // the task the new event should be assigned to Event m_event; // the result, only valid after the event has been created }; #endif Charm-1.12.0/Charm/Commands/CommandModifyEvent.cpp000066400000000000000000000032511331066577000216650ustar00rootroot00000000000000/* CommandModifyEvent.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandModifyEvent.h" #include "Core/Controller.h" #include "Core/SqlStorage.h" CommandModifyEvent::CommandModifyEvent(const Event &event, const Event &oldEvent, QObject *parent) : CharmCommand(tr("Modify Event"), parent) , m_event(event) , m_oldEvent(oldEvent) { } CommandModifyEvent::~CommandModifyEvent() { } bool CommandModifyEvent::prepare() { return true; } bool CommandModifyEvent::execute(Controller *controller) { return controller->modifyEvent(m_event); } bool CommandModifyEvent::rollback(Controller *controller) { return controller->modifyEvent(m_oldEvent); } bool CommandModifyEvent::finalize() { return true; } void CommandModifyEvent::eventIdChanged(int oid, int nid) { if (m_event.id() == oid) { m_event.setId(nid); m_oldEvent.setId(nid); } } Charm-1.12.0/Charm/Commands/CommandModifyEvent.h000066400000000000000000000027411331066577000213350ustar00rootroot00000000000000/* CommandModifyEvent.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDMODIFYEVENT_H #define COMMANDMODIFYEVENT_H #include #include class CommandModifyEvent : public CharmCommand { Q_OBJECT public: explicit CommandModifyEvent(const Event &, const Event &, QObject *parent = nullptr); ~CommandModifyEvent() override; bool prepare() override; bool execute(Controller *) override; bool rollback(Controller *) override; bool finalize() override; public Q_SLOTS: void eventIdChanged(int, int) override; private: Event m_event; Event m_oldEvent; }; #endif Charm-1.12.0/Charm/Commands/CommandModifyTask.cpp000066400000000000000000000030731331066577000215100ustar00rootroot00000000000000/* CommandModifyTask.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandModifyTask.h" #include "Core/Controller.h" CommandModifyTask::CommandModifyTask(const Task &task, QObject *parent) : CharmCommand(tr("Edit Task"), parent) , m_task(task) { } CommandModifyTask::~CommandModifyTask() { } bool CommandModifyTask::prepare() { return true; } bool CommandModifyTask::execute(Controller *controller) { m_success = controller->modifyTask(m_task); return m_success; } bool CommandModifyTask::finalize() { if (!m_success) { // this might be slightly to little informative: showInformation(tr("Unable to modify task"), tr("Modifying the task failed.")); } return m_success; } Charm-1.12.0/Charm/Commands/CommandModifyTask.h000066400000000000000000000025471331066577000211620ustar00rootroot00000000000000/* CommandModifyTask.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDMODIFYTASK_H #define COMMANDMODIFYTASK_H #include #include class CommandModifyTask : public CharmCommand { Q_OBJECT public: explicit CommandModifyTask(const Task &, QObject *parent = nullptr); ~CommandModifyTask() override; bool prepare() override; bool execute(Controller *) override; bool finalize() override; private: Task m_task; bool m_success = false; }; #endif Charm-1.12.0/Charm/Commands/CommandRelayCommand.cpp000066400000000000000000000037421331066577000220140ustar00rootroot00000000000000/* CommandRelayCommand.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandRelayCommand.h" #include "Core/CommandEmitterInterface.h" #include CommandRelayCommand::CommandRelayCommand(QObject *parent) : CharmCommand(tr("Relay"), parent) { // as long as Charm is single-threaded, this does not do anything, // because there will be no repaint QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); } CommandRelayCommand::~CommandRelayCommand() { QApplication::restoreOverrideCursor(); } void CommandRelayCommand::setCommand(CharmCommand *command) { m_payload = command; } bool CommandRelayCommand::prepare() { Q_ASSERT_X(false, Q_FUNC_INFO, "Prepare should have been called by the owner instead."); return true; } bool CommandRelayCommand::execute(Controller *controller) { return m_payload->execute(controller); } bool CommandRelayCommand::rollback(Controller *controller) { return m_payload->rollback(controller); } bool CommandRelayCommand::finalize() { QApplication::restoreOverrideCursor(); m_payload->owner()->commitCommand(m_payload); return true; } Charm-1.12.0/Charm/Commands/CommandRelayCommand.h000066400000000000000000000030621331066577000214540ustar00rootroot00000000000000/* CommandRelayCommand.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDRELAYCOMMAND_H #define COMMANDRELAYCOMMAND_H #include /** CommandRelayCommand is a decorator class that is used to wrap all commands send by the view. ATM, CommandRelayCommand sets the hour glass cursor on the view and resets it when it is deleted. */ class CommandRelayCommand : public CharmCommand { Q_OBJECT public: explicit CommandRelayCommand(QObject *parent); ~CommandRelayCommand() override; void setCommand(CharmCommand *command); bool prepare() override; bool execute(Controller *) override; bool rollback(Controller *) override; bool finalize() override; private: CharmCommand *m_payload = nullptr; }; #endif Charm-1.12.0/Charm/Commands/CommandSetAllTasks.cpp000066400000000000000000000026431331066577000216320ustar00rootroot00000000000000/* CommandSetAllTasks.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandSetAllTasks.h" #include "Core/Controller.h" CommandSetAllTasks::CommandSetAllTasks(const TaskList &tasks, QObject *parent) : CharmCommand(tr("Import Tasks"), parent) , m_tasks(tasks) { } CommandSetAllTasks::~CommandSetAllTasks() { } bool CommandSetAllTasks::prepare() { return true; } bool CommandSetAllTasks::execute(Controller *controller) { m_success = controller->setAllTasks(m_tasks); return m_success; } bool CommandSetAllTasks::finalize() { return m_success; } Charm-1.12.0/Charm/Commands/CommandSetAllTasks.h000066400000000000000000000024671331066577000213030ustar00rootroot00000000000000/* CommandSetAllTasks.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDSETALLTASKS_H #define COMMANDSETALLTASKS_H #include #include class CommandSetAllTasks : public CharmCommand { Q_OBJECT public: explicit CommandSetAllTasks(const TaskList &, QObject *parent); ~CommandSetAllTasks() override; bool prepare() override; bool execute(Controller *) override; bool finalize() override; private: TaskList m_tasks; bool m_success = false; }; #endif Charm-1.12.0/Charm/Data.cpp000066400000000000000000000132431331066577000152470ustar00rootroot00000000000000/* Data.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: Mike McQuaid Author: David Faure Author: Mike Arthur Author: Allen Winter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Data.h" #include #include const QIcon &Data::charmIcon() { static QIcon icon(QStringLiteral(":/Charm/charmicon.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QIcon &Data::charmTrayIcon() { // Mac and Windows: the system tray uses 16x16. // TODO: different icons though: white background on Windows // On X11: pure-Qt apps get 22x22 from QSystemTrayIcon. // KDE apps seem to get 24x24 in KSystemTrayIcon via KIconLoader, which is actually better. #ifdef Q_OS_OSX static const QString iconPath = QStringLiteral(":/Charm/charmtray_mac.png"); #else static const QString iconPath = QStringLiteral(":/Charm/charmtray22.png"); #endif Q_ASSERT_X(!QPixmap(iconPath).isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon = QPixmap(iconPath); return icon; } const QIcon &Data::charmTrayActiveIcon() { // Mac and Windows: the system tray uses 16x16. // TODO: different icons though: white background on Windows // On X11: pure-Qt apps get 22x22 from QSystemTrayIcon. // KDE apps seem to get 24x24 in KSystemTrayIcon via KIconLoader, which is actually better. #ifdef Q_OS_OSX static const QString iconPath = QStringLiteral(":/Charm/charmtrayactive_mac.png"); #else static const QString iconPath = QStringLiteral(":/Charm/charmtrayactive22.png"); #endif Q_ASSERT_X(!QPixmap(iconPath).isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon = QPixmap(iconPath); return icon; } const QIcon &Data::goIcon() { static QIcon icon(QStringLiteral(":/Charm/go.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QIcon &Data::stopIcon() { static QIcon icon(QStringLiteral(":/Charm/stop.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QIcon &Data::newTaskIcon() { static QIcon icon(QStringLiteral(":/Charm/newtask.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QIcon &Data::newSubtaskIcon() { static QIcon icon(QStringLiteral(":/Charm/newsubtask.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QIcon &Data::editTaskIcon() { // FIXME same as edit-event icon static QIcon icon(QStringLiteral(":/Charm/edit.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QIcon &Data::deleteTaskIcon() { static QIcon icon(QStringLiteral(":/Charm/deletetask.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QIcon &Data::searchIcon() { static QIcon icon(QStringLiteral(":/Charm/search.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QIcon &Data::editEventIcon() { static QIcon icon(QStringLiteral(":/Charm/edit.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QIcon &Data::createReportIcon() { static QIcon icon(QStringLiteral(":/Charm/createreport.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QPixmap &Data::activePixmap() { static QPixmap pixmap(QStringLiteral(":/Charm/active.png")); Q_ASSERT_X(!pixmap.isNull(), Q_FUNC_INFO, "Required resource not available"); return pixmap; } const QIcon &Data::quitCharmIcon() { static QIcon icon(QStringLiteral(":/Charm/quitcharm.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QIcon &Data::configureIcon() { static QIcon icon(QStringLiteral(":/Charm/configure.png")); Q_ASSERT_X(!icon.isNull(), Q_FUNC_INFO, "Required resource not available"); return icon; } const QPixmap &Data::editorLockedPixmap() { static QPixmap pixmap(QStringLiteral(":/Charm/editor_locked.png")); Q_ASSERT_X(!pixmap.isNull(), Q_FUNC_INFO, "Required resource not available"); return pixmap; } const QPixmap &Data::editorDirtyPixmap() { static QPixmap pixmap(QStringLiteral(":/Charm/editor_dirty.png")); Q_ASSERT_X(!pixmap.isNull(), Q_FUNC_INFO, "Required resource not available"); return pixmap; } Charm-1.12.0/Charm/Data.h000066400000000000000000000036461331066577000147220ustar00rootroot00000000000000/* Data.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Mike McQuaid This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef DATA_H #define DATA_H class QIcon; class QPixmap; class Data { public: static const QIcon &charmIcon(); static const QIcon &charmTrayIcon(); static const QIcon &charmTrayActiveIcon(); static const QIcon &goIcon(); static const QIcon &stopIcon(); static const QIcon &newTaskIcon(); static const QIcon &newSubtaskIcon(); static const QIcon &editTaskIcon(); static const QIcon &deleteTaskIcon(); static const QIcon &editEventIcon(); static const QIcon &searchIcon(); static const QIcon &previousEventIcon(); static const QIcon &nextEventIcon(); static const QIcon &createReportIcon(); static const QPixmap &checkIcon(); static const QPixmap &activePixmap(); static const QIcon &quitCharmIcon(); static const QIcon &clearFilterIcon(); static const QIcon &configureIcon(); static const QPixmap &editorLockedPixmap(); static const QPixmap &editorDirtyPixmap(); static const QPixmap &recorderStopIcon(); static const QPixmap &recorderGoIcon(); }; #endif Charm-1.12.0/Charm/EventModelAdapter.cpp000066400000000000000000000101461331066577000177400ustar00rootroot00000000000000/* EventModelAdapter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "EventModelAdapter.h" #include "ApplicationCore.h" #include "Core/CharmCommand.h" #include "Core/CharmDataModel.h" EventModelAdapter::EventModelAdapter(CharmDataModel *parent) : QAbstractListModel(parent) , m_dataModel(parent) { m_dataModel->registerAdapter(this); } EventModelAdapter::~EventModelAdapter() { if (m_dataModel) m_dataModel->unregisterAdapter(this); } int EventModelAdapter::rowCount(const QModelIndex &) const { return m_events.size(); } QVariant EventModelAdapter::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() > m_events.size()) return QVariant(); // beware of stale persistent indexes switch (role) { case Qt::DisplayRole: { EventId eventId = m_events[index.row()]; const Event &event = m_dataModel->eventForId(eventId); QString text; QTextStream stream(&text); stream << event.taskId() << " - " << event.comment(); return text; break; } default: return QVariant(); } } void EventModelAdapter::resetEvents() { beginResetModel(); m_events.clear(); for (EventMap::const_iterator it = m_dataModel->eventMap().begin(); it != m_dataModel->eventMap().end(); ++it) m_events.append(it->first); endResetModel(); } void EventModelAdapter::eventAboutToBeAdded(EventId) { int position = m_events.size(); beginInsertRows(QModelIndex(), position, position); } void EventModelAdapter::eventAdded(EventId id) { m_events.append(id); endInsertRows(); } void EventModelAdapter::eventModified(EventId id, Event) { // nothing to do, except: int row = m_events.indexOf(id); Q_ASSERT(row != -1); // inconsistency between model and adapter emit(dataChanged(index(row), index(row))); } void EventModelAdapter::eventAboutToBeDeleted(EventId id) { int row = m_events.indexOf(id); Q_ASSERT(row != -1); // inconsistency between model and adapter beginRemoveRows(QModelIndex(), row, row); } void EventModelAdapter::eventDeleted(EventId id) { int position = m_events.indexOf(id); Q_ASSERT(position != -1); // inconsistency between model and adapter m_events.removeAt(position); Q_ASSERT(m_events.indexOf(id) == -1); // cannot be in there endRemoveRows(); } void EventModelAdapter::eventActivated(EventId id) { emit eventActivationNotice(id); } void EventModelAdapter::eventDeactivated(EventId id) { emit eventDeactivationNotice(id); } void EventModelAdapter::commitCommand(CharmCommand *command) { command->finalize(); } const Event &EventModelAdapter::eventForIndex(const QModelIndex &index) const { if (index.row() >= 0 && index.row() < m_events.size()) { EventId eventId = m_events.at(index.row()); const Event &event = m_dataModel->eventForId(eventId); return event; } else { static Event InvalidEvent; return InvalidEvent; } } QModelIndex EventModelAdapter::indexForEvent(const Event &event) const { int position = m_events.indexOf(event.id()); if (position >= 0 && position < m_events.size()) { return index(position); } else { return QModelIndex(); } } Charm-1.12.0/Charm/EventModelAdapter.h000066400000000000000000000056451331066577000174150ustar00rootroot00000000000000/* EventModelAdapter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef EVENTMODELADAPTER_H #define EVENTMODELADAPTER_H #include #include #include "Core/Event.h" #include "Core/EventModelInterface.h" #include "Core/CharmDataModelAdapterInterface.h" #include "Core/CommandEmitterInterface.h" class CharmDataModel; class EventModelAdapter : public QAbstractListModel, public CharmDataModelAdapterInterface, public CommandEmitterInterface, public EventModelInterface { Q_OBJECT public: explicit EventModelAdapter(CharmDataModel *parent); ~EventModelAdapter() override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; // reimplement CharmDataModelAdapterInterface: void resetTasks() override { } void taskAboutToBeAdded(TaskId, int) override { } void taskAdded(TaskId) override { } void taskModified(TaskId) override { } void taskParentChanged(TaskId, TaskId, TaskId) override { } void taskAboutToBeDeleted(TaskId) override { } void taskDeleted(TaskId) override { } void resetEvents() override; void eventAboutToBeAdded(EventId id) override; void eventAdded(EventId id) override; void eventModified(EventId id, Event) override; void eventAboutToBeDeleted(EventId id) override; void eventDeleted(EventId id) override; void eventActivated(EventId id) override; void eventDeactivated(EventId id) override; // reimplement EventModelInterface: const Event &eventForIndex(const QModelIndex &index) const override; QModelIndex indexForEvent(const Event &) const override; // reimplement CommandEmitterInterface: void commitCommand(CharmCommand *) override; Q_SIGNALS: void eventActivationNotice(EventId id); void eventDeactivationNotice(EventId id); private: // if this is slow, we may want to store pointers here: EventIdList m_events; QPointer m_dataModel; }; #endif Charm-1.12.0/Charm/EventModelFilter.cpp000066400000000000000000000101461331066577000176050ustar00rootroot00000000000000/* EventModelFilter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "EventModelFilter.h" EventModelFilter::EventModelFilter(CharmDataModel *model, QObject *parent) : QSortFilterProxyModel(parent) , m_model(model) { setSourceModel(&m_model); setDynamicSortFilter(true); sort(0, Qt::AscendingOrder); connect(&m_model, SIGNAL(eventActivationNotice(EventId)), SIGNAL(eventActivationNotice(EventId))); connect(&m_model, SIGNAL(eventDeactivationNotice(EventId)), SIGNAL(eventDeactivationNotice(EventId))); } EventModelFilter::~EventModelFilter() { } void EventModelFilter::commitCommand(CharmCommand *command) { m_model.commitCommand(command); } bool EventModelFilter::lessThan(const QModelIndex &left, const QModelIndex &right) const { if (left.column() == 0 && right.column() == 0) { const Event &leftEvent = m_model.eventForIndex(left); const Event &rightEvent = m_model.eventForIndex(right); // date comparison in UTC is much faster and just as correct return leftEvent.startDateTime(Qt::UTC) < rightEvent.startDateTime(Qt::UTC); } else { return QSortFilterProxyModel::lessThan(left, right); } } const Event &EventModelFilter::eventForIndex(const QModelIndex &index) const { return m_model.eventForIndex(mapToSource(index)); } QModelIndex EventModelFilter::indexForEvent(const Event &event) const { const QModelIndex &sourceIndex = m_model.indexForEvent(event); const QModelIndex &proxyIndex(mapFromSource(sourceIndex)); // bool valid = proxyIndex.isValid(); return proxyIndex; } bool EventModelFilter::filterAcceptsRow(int srow, const QModelIndex &sparent) const { if (QSortFilterProxyModel::filterAcceptsRow(srow, sparent) == false) return false; const Event &event = m_model.eventForIndex(m_model.index(srow, 0, sparent)); if (m_filterId != TaskId() && event.taskId() != m_filterId) return false; const auto startDate = event.startDateTime().date(); /* * event.endDateTime().date() < m_start * Show also Events that end within the time span. */ if (m_start.isValid() && (startDate < m_start) && (event.endDateTime().date() < m_start)) return false; if (m_end.isValid() && startDate >= m_end) return false; return true; } void EventModelFilter::setFilterStartDate(const QDate &date) { if (m_start == date) return; m_start = date; invalidateFilter(); } void EventModelFilter::setFilterEndDate(const QDate &date) { if (m_end == date) return; m_end = date; invalidateFilter(); } void EventModelFilter::setFilterTaskId(TaskId id) { if (m_filterId == id) return; m_filterId = id; invalidateFilter(); } int EventModelFilter::totalDuration() const { int total = 0; for (int i = 0; i < rowCount(); ++i) { QModelIndex current = index(i, 0, QModelIndex()); const Event &event = eventForIndex(current); total += event.duration(); } return total; } QList EventModelFilter::events() const { QList events; for (int i = 0; i < rowCount(); ++i) { QModelIndex hit = index(i, 0, QModelIndex()); const Event &event = eventForIndex(hit); events << event; } return events; } Charm-1.12.0/Charm/EventModelFilter.h000066400000000000000000000045221331066577000172530ustar00rootroot00000000000000/* EventModelFilter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef EVENTMODELFILTER_H #define EVENTMODELFILTER_H #include #include #include #include #include "EventModelAdapter.h" class CharmDataModel; class EventModelFilter : public QSortFilterProxyModel, public CommandEmitterInterface, public EventModelInterface { Q_OBJECT public: explicit EventModelFilter(CharmDataModel *, QObject *parent = nullptr); ~EventModelFilter() override; /** Returns the total number of seconds of all events in the model. */ int totalDuration() const; // implement EventModelInterface: const Event &eventForIndex(const QModelIndex &) const override; QModelIndex indexForEvent(const Event &) const override; bool filterAcceptsRow(int srow, const QModelIndex &sparent) const override; void setFilterStartDate(const QDate &date); void setFilterEndDate(const QDate &date); void setFilterTaskId(TaskId id); // implement CommandEmitterInterface: void commitCommand(CharmCommand *) override; // implement to sort by event start datetime bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; QList events() const; Q_SIGNALS: void eventActivationNotice(EventId id); void eventDeactivationNotice(EventId id); private: EventModelAdapter m_model; QDate m_start; QDate m_end; TaskId m_filterId = {}; }; #endif Charm-1.12.0/Charm/GUIState.cpp000066400000000000000000000057161331066577000160310ustar00rootroot00000000000000/* GUIState.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "GUIState.h" #include "Core/CharmConstants.h" #include "Core/Task.h" #include #include GUIState::GUIState() { } const TaskIdList &GUIState::expandedTasks() const { return m_expandedTasks; } TaskId GUIState::selectedTask() const { return m_selectedTask; } bool GUIState::showExpired() const { return m_showExpired; } bool GUIState::showCurrents() const { return m_showCurrents; } void GUIState::setSelectedTask(TaskId task) { m_selectedTask = task; } void GUIState::setExpandedTasks(const TaskIdList &tasks) { m_expandedTasks = tasks; } void GUIState::setShowExpired(bool show) { m_showExpired = show; } void GUIState::setShowCurrents(bool show) { m_showCurrents = show; } void GUIState::saveTo(QSettings &settings) { settings.setValue(MetaKey_MainWindowGUIStateSelectedTask, selectedTask()); // workaround for not getting QVariant serialization of TaskIdLists to work: QList variants; Q_FOREACH (TaskId v, expandedTasks()) variants << v; settings.setValue(MetaKey_MainWindowGUIStateExpandedTasks, variants); settings.setValue(MetaKey_MainWindowGUIStateShowExpiredTasks, showExpired()); settings.setValue(MetaKey_MainWindowGUIStateShowCurrentTasks, showCurrents()); } void GUIState::loadFrom(const QSettings &settings) { if (settings.contains(MetaKey_MainWindowGUIStateSelectedTask)) setSelectedTask(settings.value(MetaKey_MainWindowGUIStateSelectedTask).value()); if (settings.contains(MetaKey_MainWindowGUIStateExpandedTasks)) { // workaround for not getting QVariant serialization of TaskIdLists to work: QList values(settings.value( MetaKey_MainWindowGUIStateExpandedTasks).value >()); TaskIdList ids; Q_FOREACH (const QVariant &variant, values) ids << variant.value(); setExpandedTasks(ids); setShowExpired(settings.value(MetaKey_MainWindowGUIStateShowExpiredTasks).toBool()); setShowCurrents(settings.value(MetaKey_MainWindowGUIStateShowCurrentTasks).toBool()); } } Charm-1.12.0/Charm/GUIState.h000066400000000000000000000033451331066577000154720ustar00rootroot00000000000000/* GUIState.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef GUISTATE_H #define GUISTATE_H #include "Core/Task.h" class QSettings; // I am unsure if this is a good idea (making a class for this at // all), depends on how it turns out in the future. If there are no // more options than that, it can be merged back into View. class GUIState { public: GUIState(); const TaskIdList &expandedTasks() const; TaskId selectedTask() const; bool showExpired() const; bool showCurrents() const; void setSelectedTask(TaskId); void setExpandedTasks(const TaskIdList &); void setShowExpired(bool show); void setShowCurrents(bool show); void saveTo(QSettings &settings); void loadFrom(const QSettings &settings); private: TaskIdList m_expandedTasks; TaskId m_selectedTask = {}; bool m_showExpired = false; // show also expired tasks bool m_showCurrents = false; // show only selected tasks }; #endif Charm-1.12.0/Charm/HttpClient/000077500000000000000000000000001331066577000157455ustar00rootroot00000000000000Charm-1.12.0/Charm/HttpClient/CheckForUpdatesJob.cpp000066400000000000000000000077621331066577000221320ustar00rootroot00000000000000/* CheckForUpdatesJob.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Michel Boyer de la Giroday This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CheckForUpdatesJob.h" #include #include #include #include #include #include #include bool Charm::versionLessThan(const QString &lhs, const QString &rhs) { const QStringList lhsSplit = lhs.split(QLatin1Char('.')); const QStringList rhsSplit = rhs.split(QLatin1Char('.')); for (int i = 0; i < lhsSplit.count() && i < rhsSplit.count(); ++i) { const int diff = rhsSplit[i].toInt() - lhsSplit[i].toInt(); if (diff != 0) return diff > 0; } for (int i = lhsSplit.size(); i < rhsSplit.size(); ++i) { if (rhsSplit[i].toInt() > 0) return true; } return false; } CheckForUpdatesJob::CheckForUpdatesJob(QObject *parent) : QObject(parent) { } CheckForUpdatesJob::~CheckForUpdatesJob() { } void CheckForUpdatesJob::start() { Q_ASSERT(!m_url.toString().isEmpty()); QNetworkAccessManager *manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, &CheckForUpdatesJob::jobFinished); manager->get(QNetworkRequest(m_url)); } void CheckForUpdatesJob::jobFinished(QNetworkReply *reply) { if (reply->error()) { const QString errorString = tr("Could not download update information from %1: %2").arg( m_url.toString(), reply->errorString()); m_jobData.errorString = errorString; m_jobData.error = reply->error(); } else { QByteArray data = reply->readAll(); parseXmlData(data); } reply->deleteLater(); emit finished(m_jobData); deleteLater(); } void CheckForUpdatesJob::setVerbose(bool verbose) { m_jobData.verbose = verbose; } void CheckForUpdatesJob::parseXmlData(const QByteArray &data) { QBuffer buffer; buffer.setData(data); buffer.open(QIODevice::ReadOnly); QDomDocument document; QString errorMessage; int errorLine = 0; int errorColumn = 0; if (!document.setContent(&buffer, &errorMessage, &errorLine, &errorColumn)) { m_jobData.errorString = tr("Invalid XML: [%1:%2] %3").arg(QString::number( errorLine), QString::number( errorColumn), errorMessage); m_jobData.error = 999; // this value is just to have an and does not mean anything - error != 0 return; } QDomElement element = document.documentElement(); QDomElement versionElement = element.firstChildElement(QStringLiteral("version")); QDomElement linkElement = versionElement.nextSiblingElement(QStringLiteral("link")); const QString releaseVersion = versionElement.text(); m_jobData.releaseVersion = releaseVersion; QUrl link(linkElement.text()); m_jobData.link = link; QString releaseInfoLink(linkElement.nextSiblingElement(QStringLiteral("releaseinfolink")).text()); m_jobData.releaseInformationLink = releaseInfoLink; } void CheckForUpdatesJob::setUrl(const QUrl &url) { m_url = url; } Charm-1.12.0/Charm/HttpClient/CheckForUpdatesJob.h000066400000000000000000000036221331066577000215660ustar00rootroot00000000000000/* CheckForUpdatesJob.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Michel Boyer de la Giroday This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHECKFORUPDATESJOB #define CHECKFORUPDATESJOB #include #include class QByteArray; class QNetworkReply; class QUrl; namespace Charm { bool versionLessThan(const QString &lhs, const QString &rhs); } class CheckForUpdatesJob : public QObject { Q_OBJECT public: struct JobData { QUrl link; QString releaseInformationLink; QString releaseVersion; QString errorString; int error = 0; // QNetworkReply::NetworkError or xml parsing error ( 999 ) bool verbose = false; // display error message or not ( default == not ) }; explicit CheckForUpdatesJob(QObject *parent = nullptr); ~CheckForUpdatesJob() override; void start(); void setUrl(const QUrl &url); void setVerbose(bool verbose); Q_SIGNALS: void finished(CheckForUpdatesJob::JobData data); private Q_SLOTS: void jobFinished(QNetworkReply *reply); private: void parseXmlData(const QByteArray &data); QUrl m_url; JobData m_jobData; }; #endif // CHECKFORUPDATESJOB Charm-1.12.0/Charm/HttpClient/GetProjectCodesJob.cpp000066400000000000000000000043241331066577000221330ustar00rootroot00000000000000/* GetProjectCodesJob.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "GetProjectCodesJob.h" #include #include #include #include GetProjectCodesJob::GetProjectCodesJob(QObject *parent) : HttpJob(parent) { } GetProjectCodesJob::~GetProjectCodesJob() { } QByteArray GetProjectCodesJob::payload() const { return m_payload; } void GetProjectCodesJob::executeRequest(QNetworkAccessManager *manager) { QNetworkRequest request(m_downloadUrl); QNetworkReply *reply = manager->get(request); connect(reply, &QNetworkReply::finished, this, &GetProjectCodesJob::handleResult); if (reply->error() != QNetworkReply::NoError) setErrorFromReplyAndEmitFinishedOrRestart(reply); } void GetProjectCodesJob::handleResult() { auto reply = qobject_cast(sender()); reply->deleteLater(); /* check for failure */ if (reply->error() != QNetworkReply::NoError) { setErrorFromReplyAndEmitFinishedOrRestart(reply); return; } m_payload = reply->readAll(); emitFinishedOrRestart(); } QUrl GetProjectCodesJob::downloadUrl() const { return m_downloadUrl; } void GetProjectCodesJob::setDownloadUrl(const QUrl &url) { m_downloadUrl = url; } void GetProjectCodesJob::setVerbose(bool verbose) { m_verbose = verbose; } bool GetProjectCodesJob::isVerbose() const { return m_verbose; } Charm-1.12.0/Charm/HttpClient/GetProjectCodesJob.h000066400000000000000000000027531331066577000216040ustar00rootroot00000000000000/* GetProjectCodesJob.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef GETPROJECTCODESJOB_H #define GETPROJECTCODESJOB_H #include "HttpJob.h" #include class GetProjectCodesJob : public HttpJob { Q_OBJECT public: explicit GetProjectCodesJob(QObject *parent = nullptr); ~GetProjectCodesJob() override; QByteArray payload() const; QUrl downloadUrl() const; void setDownloadUrl(const QUrl &url); void setVerbose(bool verbose); bool isVerbose() const; protected: void executeRequest(QNetworkAccessManager *) override; private: void handleResult(); private: QByteArray m_payload; QUrl m_downloadUrl; bool m_verbose = true; }; #endif Charm-1.12.0/Charm/HttpClient/HttpJob.cpp000066400000000000000000000122631331066577000200270ustar00rootroot00000000000000/* HttpJob.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld Author: Olivier JG This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "HttpJob.h" #include "CharmCMake.h" #include #include #include #include #include #include #include HttpJob::HttpJob(QObject *parent) : QObject(parent) , m_networkManager(new QNetworkAccessManager(this)) { connect(m_networkManager, &QNetworkAccessManager::authenticationRequired, this, &HttpJob::authenticationRequired); } HttpJob::~HttpJob() { } QString HttpJob::username() const { return m_username; } void HttpJob::setUsername(const QString &value) { m_username = value; } QString HttpJob::password() const { return m_password; } void HttpJob::setPassword(const QString &value) { m_password = value; } int HttpJob::error() const { return m_errorCode; } QString HttpJob::errorString() const { return m_errorString; } void HttpJob::start() { QMetaObject::invokeMethod(this, "doStart", Qt::QueuedConnection); } using namespace QKeychain; void HttpJob::doStart() { if (m_username.isEmpty()) { setErrorAndEmitFinishedOrRestart(NotConfigured, tr("lotsofcake login data not configured. Download and import the task list manually to configure them.")); return; } auto readJob = new ReadPasswordJob(QStringLiteral("Charm"), this); connect(readJob, &Job::finished, this, &HttpJob::passwordRead); readJob->setKey(QStringLiteral("lotsofcake")); readJob->start(); } void HttpJob::passwordRead(QKeychain::Job *j) { ReadPasswordJob *job = qobject_cast(j); Q_ASSERT(job); m_passwordReadError = job->error() != QKeychain::NoError && job->error() != QKeychain::EntryNotFound; const QString oldpass = job->error() ? QString() : job->textData(); if (oldpass.isEmpty() || m_lastAuthenticationFailed) { emit passwordRequested(oldpass.isEmpty() ? HttpJob::NoPasswordFound : HttpJob::PasswordIncorrect); return; } else { provideRequestedPassword(oldpass); } } void HttpJob::provideRequestedPassword(const QString &password) { const QString oldpass = m_password; m_password = password; if (oldpass != m_password && !m_passwordReadError) { auto writeJob = new WritePasswordJob(QStringLiteral("Charm"), this); connect(writeJob, &Job::finished, this, &HttpJob::passwordWritten); writeJob->setKey(QStringLiteral("lotsofcake")); writeJob->setTextData(m_password); writeJob->start(); } else { passwordWritten(); } } void HttpJob::passwordRequestCanceled() { setErrorAndEmitFinishedOrRestart(Canceled, tr("Canceled")); } void HttpJob::passwordWritten() { emit transferStarted(); executeRequest(m_networkManager); } void HttpJob::cancel() { QMetaObject::invokeMethod(this, "doCancel", Qt::QueuedConnection); } void HttpJob::doCancel() { setErrorAndEmitFinishedOrRestart(Canceled, tr("Canceled")); } void HttpJob::authenticationRequired(QNetworkReply *, QAuthenticator *authenticator) { if (!m_authenticationDoneAlready) { authenticator->setUser(m_username); authenticator->setPassword(m_password); m_authenticationDoneAlready = true; } } void HttpJob::emitFinishedOrRestart() { if (m_errorCode == AuthenticationFailed) { m_authenticationDoneAlready = false; m_lastAuthenticationFailed = true; m_errorCode = NoError; m_errorString.clear(); start(); return; } m_networkManager->disconnect(this); emit finished(this); deleteLater(); } void HttpJob::setErrorAndEmitFinishedOrRestart(int code, const QString &errorString) { m_errorCode = code; m_errorString = errorString; emitFinishedOrRestart(); } void HttpJob::setErrorFromReplyAndEmitFinishedOrRestart(QNetworkReply *reply) { switch (reply->error()) { case QNetworkReply::HostNotFoundError: setErrorAndEmitFinishedOrRestart(HostNotFound, reply->errorString()); break; case QNetworkReply::AuthenticationRequiredError: setErrorAndEmitFinishedOrRestart(AuthenticationFailed, reply->errorString()); break; default: setErrorAndEmitFinishedOrRestart(SomethingWentWrong, reply->errorString()); break; } } Charm-1.12.0/Charm/HttpClient/HttpJob.h000066400000000000000000000056141331066577000174760ustar00rootroot00000000000000/* HttpJob.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef HTTPJOB_H #define HTTPJOB_H #include #include namespace QKeychain { class Job; } class QNetworkAccessManager; class QNetworkReply; class QNetworkRequest; class QAuthenticator; class HttpJob : public QObject { Q_OBJECT public: enum Error { NoError = 0, Canceled, NotConfigured, AuthenticationFailed, SomethingWentWrong, HostNotFound }; enum PasswordRequestReason { NoPasswordFound, PasswordIncorrect }; explicit HttpJob(QObject *parent = nullptr); ~HttpJob() override; QString username() const; void setUsername(const QString &value); QString password() const; void setPassword(const QString &value); QString errorString() const; int error() const; void start(); void cancel(); void provideRequestedPassword(const QString &password); void passwordRequestCanceled(); Q_SIGNALS: void finished(HttpJob *); /** * the actual communication was started */ void transferStarted(); /** * The job requests the password from the user * * Must be replied to via provideRequestedPassword() or passwordRequestCanceled() to continue. */ void passwordRequested(PasswordRequestReason); protected Q_SLOTS: virtual void executeRequest(QNetworkAccessManager *) = 0; protected: void emitFinishedOrRestart(); void setErrorAndEmitFinishedOrRestart(int code, const QString &errorString); void setErrorFromReplyAndEmitFinishedOrRestart(QNetworkReply *reply); private Q_SLOTS: void doStart(); void doCancel(); void passwordRead(QKeychain::Job *); void passwordWritten(); void authenticationRequired(QNetworkReply *reply, QAuthenticator *authenticator); private: QNetworkAccessManager *m_networkManager; QString m_username; QString m_password; int m_errorCode = NoError; QString m_errorString; bool m_lastAuthenticationFailed = false; bool m_authenticationDoneAlready = false; bool m_passwordReadError = false; }; #endif Charm-1.12.0/Charm/HttpClient/RestJob.cpp000066400000000000000000000035361331066577000200300ustar00rootroot00000000000000/* RestJob.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Pál Tóth This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "RestJob.h" #include #include #include RestJob::RestJob(QObject *parent) : HttpJob(parent) { } RestJob::~RestJob() { } QByteArray RestJob::resultData() const { return m_resultData; } void RestJob::executeRequest(QNetworkAccessManager *manager) { QNetworkRequest request(m_url); QNetworkReply *reply = manager->get(request); connect(reply, &QNetworkReply::finished, this, &RestJob::handleResult); if (reply->error() != QNetworkReply::NoError) setErrorFromReplyAndEmitFinishedOrRestart(reply); } void RestJob::handleResult() { auto reply = qobject_cast(sender()); reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { setErrorFromReplyAndEmitFinishedOrRestart(reply); return; } m_resultData = reply->readAll(); emitFinishedOrRestart(); } QUrl RestJob::url() const { return m_url; } void RestJob::setUrl(const QUrl &url) { m_url = url; } Charm-1.12.0/Charm/HttpClient/RestJob.h000066400000000000000000000025531331066577000174730ustar00rootroot00000000000000/* RestJob.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Pál Tóth This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef RESTJOB_H #define RESTJOB_H #include "HttpJob.h" #include #include #include class RestJob : public HttpJob { Q_OBJECT public: explicit RestJob(QObject *parent = nullptr); ~RestJob() override; QByteArray resultData() const; QUrl url() const; void setUrl(const QUrl &url); public Q_SLOTS: void executeRequest(QNetworkAccessManager *manager) override; void handleResult(); private: QByteArray m_resultData; QUrl m_url; }; #endif // RESTJOB_H Charm-1.12.0/Charm/HttpClient/UploadTimesheetJob.cpp000066400000000000000000000101271331066577000222010ustar00rootroot00000000000000/* UploadTimesheetJob.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "UploadTimesheetJob.h" #include #include #include #include #include #include UploadTimesheetJob::UploadTimesheetJob(QObject *parent) : HttpJob(parent) , m_fileName(QStringLiteral("payload")) { } UploadTimesheetJob::~UploadTimesheetJob() { } QByteArray UploadTimesheetJob::payload() const { return m_payload; } void UploadTimesheetJob::setPayload(const QByteArray &_payload) { m_payload = _payload; } QString UploadTimesheetJob::fileName() const { return m_fileName; } void UploadTimesheetJob::setFileName(const QString &_fileName) { m_fileName = _fileName; } QUrl UploadTimesheetJob::uploadUrl() const { return m_uploadUrl; } void UploadTimesheetJob::setUploadUrl(const QUrl &url) { m_uploadUrl = url; } UploadTimesheetJob::Status UploadTimesheetJob::status() const { return m_status; } void UploadTimesheetJob::setStatus(Status status) { m_status = status; } void UploadTimesheetJob::executeRequest(QNetworkAccessManager *manager) { QByteArray uploadName; /* validate filename */ if (!m_fileName.contains(QRegExp(QStringLiteral("^WeeklyTimeSheet-\\d\\d\\d\\d-\\d\\d$")))) { qDebug("Invalid filename encountered, using default (\"payload\")."); uploadName = "payload"; } else { uploadName = m_fileName.toUtf8(); } QByteArray data; /* username */ data += "--KDAB\r\n" "Content-Disposition: form-data; name=\"user\"\r\n\r\n"; data += username().toUtf8(); data += "\r\n"; /* payload */ data += "--KDAB\r\n" "Content-Disposition: form-data; name=\"" + uploadName + "\"; filename=\"" +uploadName + "\"\r\nContent-Type: application/octet-stream\r\n\r\n"; data += m_payload; data += "\r\n"; if (m_status == Staged) { data += "--KDAB\r\n"; data += "Content-Disposition: form-data; name=\"status\"\r\n\r\n"; data += "STAGED\r\n"; } data += "--KDAB--\r\n"; QNetworkRequest request(m_uploadUrl); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("multipart/form-data; boundary=KDAB")); request.setHeader(QNetworkRequest::ContentLengthHeader, data.size()); QNetworkReply *reply = manager->post(request, data); connect(reply, &QNetworkReply::finished, this, &UploadTimesheetJob::handleResult); } void UploadTimesheetJob::handleResult() { auto reply = qobject_cast(sender()); reply->deleteLater(); if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) { const auto doc = QJsonDocument::fromJson(reply->readAll()); const auto errorMessage = doc.object().value(QLatin1String("message")).toString(); setErrorAndEmitFinishedOrRestart(SomethingWentWrong, !errorMessage.isEmpty() ? errorMessage : tr("An error occurred, could not extract details")); return; } if (reply->error() != QNetworkReply::NoError) { setErrorFromReplyAndEmitFinishedOrRestart(reply); return; } emitFinishedOrRestart(); } Charm-1.12.0/Charm/HttpClient/UploadTimesheetJob.h000066400000000000000000000032771331066577000216560ustar00rootroot00000000000000/* UploadTimesheetJob.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef UPLOADTIMESHEETJOB_H #define UPLOADTIMESHEETJOB_H #include "HttpJob.h" #include class UploadTimesheetJob : public HttpJob { Q_OBJECT public: enum Status { Unreviewed, Staged }; explicit UploadTimesheetJob(QObject *parent = nullptr); ~UploadTimesheetJob() override; QByteArray payload() const; void setPayload(const QByteArray &payload); QString fileName() const; void setFileName(const QString &fileName); QUrl uploadUrl() const; void setUploadUrl(const QUrl &url); Status status() const; void setStatus(Status status); public Q_SLOTS: void executeRequest(QNetworkAccessManager *manager) override; void handleResult(); private: Status m_status = Unreviewed; QByteArray m_payload; QString m_fileName; QUrl m_uploadUrl; }; #endif Charm-1.12.0/Charm/Icons/000077500000000000000000000000001331066577000147425ustar00rootroot00000000000000Charm-1.12.0/Charm/Icons/128-apps-Charm.png000066400000000000000000000360001331066577000177520ustar00rootroot00000000000000PNG  IHDR>asRGB pHYsnnޱtIME ((9]"bKGD;IDATx pՕ]!! U$@'d,ꚚLS,]2tBHgBP@!d0M!@!c˶lڗ'Kjx_j3?џגhx,UO N߇?]w߾E?s}ƂcY/l-WiiAϟꫯJOO_^v`Ν{nٳgݻW/ÇѣGOEw7O[0Ud&nٴiSP]ĕo>yr!9r |VKT zSOl"nD TW>##NK{{ 1x=> _PPaax=Jq¿)wm ~/,,clVR444HGG[w{)dr<;&ç\pAK. 2 oii5kքпu֙&q?%QI3Ys~.&Q8>J}x+?gهS#G4f5/L?~BȐ{T:wˈz: ޶moW{+]9,ANU8BŖQ*#"i=gn08tuuB(s7g1510xWsa?6A`z! hJ{掣F Fda稬3l0<ޕ aSta ʜ9Om}]Ru\ B|0@q=ľI8LNHQtDN$wM0YV^J+B<xwIwRSNoޮEK{\[6wqٛNO?Ty寈<&w#:d|To2drldePx=0>'rDȊ#|τ "gr 7v|B&.+?Il2160*uLAZc@蜐C `Uz\q`}Rݚ6;e3{a<\`FZV_ `{{⁓{GF5wuȄFi`golZF{1m8kڔ0ĕ~2r{:Q}e\śGD\Z,+m>G8s@.Dc FD&f#oo<0qQDr'k]Yq}OKxf#=DuvGIƊcϽ"=]spw˒ c"c#ǝLX1W `0yya߫º-A1 ||BwTvpoX(qiyL\^MwЧ9t5]˥ U%̓=3ܹӛmrc.\8͘ToCFRwuOC6wnth\vkTPO}Gy}G亊ҷO#G&A0^wl >=p,\\^cc7ȯ:Ǥ{A )Ȯe>?9VKKreY5̆DU5RT(+z]%s|WWT9.:XR8P;P^RYB˪d&ԕk3YTzsuxey^|cnyskɞ^ _kțZ^_uڶvZ%2[XWCtr %W :_̾̾Lx>qN|SOIcSVFimi&ini>o7OȠʏTRȃ0Noww3dk{Y;^= ^jJ*\:^eܗ_^' _m뛐 ۧI]o:t&pGT* hll `zq ˁjc> 9m9AY"Y9٩e5rwt¸E=}|YaMѥB~:}T\\ck:Gea} 8nÒL&(M|%'M]] !r,G odYMm @)YЁ9h`H݅1Dtq <3"*f1Yi4>=vOs/Hx%U P__O*P#h@ʌEwG⚱Uz c3F [r?R^d?4X?`i>PF۵0`n N"D,^8{*۬ݯ[U:#V߯ۤ琷O@?DM 4djdx vm-]G8ZIJk}':\s3;({\Lp2IK>GJ#}L>^S./)MþWpJEzHj5RE~#at3ެt,8FD ZuQ::&ʫ:پUS&)\Ga&s=#Xhq l{X~YWdlJ862$?*# hĸ~ۤ nkcB<~ lZ]{ZU0=016,O$_ddx淁D!yy=j0enVs$H ۜO&F$wB'vJv9tCxz4ߐmZEc@z&ALΌEFOgaٹtVdśL uutSEkȆuo;q;HDMB#@ e#!smTG}'.p KM7>I?kI@n>{6utq[kK+_3ۇ)Khbݼ0L  Ah}nٴuPzm@༶om͌7wY]DyL=V gꐫ %{Dv4][0yPLB|V Zc<k[k[kxLF7ogcj[瑄fOމ/C0wZdqwKqql2Ĩp61KS(D戗n,%[gKg 2 I U"e;)awBI?RgiFylUOpJe9AcП29ğs: "*{RRRkVKDO <]ټ#޹)-%[t424$fC(`E b 0 ީDV fF*-$ (YY7V@y`xs<<8Nn[rq,_jO-wUM~E #%ԙ. ѝ9n]}Ff0p-p,ɘظfJ1g뿖,rŗ_,y%\ErEQ.oN[T4+pρ}^? 'SsBpQehqs(ڢ"n+Av[xdSXW&#˟duiBro=mC{չf :5o ׇ_/kA1k#rpiux[n?kҵ9I_u~r|%V.?S iLkj%ILU(Y9ZXI8_\һFkGAᄣ&Ɇtp&e ђT>`TVVJEE;_ B]yy%״uyJץ}W{;NRǩ>\FƧz2qqS oS)uu) |4%ϯ&dDŒvAM{QZ)=)^ɧ3Yɿ 6)6l@$dCw+ɰ%[)Cy[.ZiGK'Q[ocQNzև(aV2m޾SA߯7/}h%0rо}նum.5vq*w$Cklh-GW_[98J646of#CA78|#c-ZnMm۶{=}}FYB ϯ' 2֩:x:]shurmDy |vfvlϖ3&<Zʟ~,o~ls1z&۽{F0aD j~CR赖)X]hk=p=M=8N3Ώ?o8{ӱ}krȕ  '\3x>٨o&ݯbA];w٩ikO=cxenF?ٲ%Dn+;D o/)PPSzmGNc`E%oY+OWyJڷjAQ8=dƳ.|oWpwzr^=X >/Fe]@ϛ1!\cScH\h~ uQeREx~tγA2{=~: ee18\3;/]c<Aɘ:c.6>qbt)܄G_?rqX}V^i&&cy O[ui<\him/%K+یfԘ`j<;uD8@ Zhb F|/k%|^D:ڸ6>]L|ځ?{MmFMmm,)}&,\&8\hs2@f= t4cH;@g(u Y3l ˷-l9FQYBo LQAlyF?\hGKBƂ˱4XRΗ`</,^qKY~9/CCxR>K=DhyCM/ce5Ờ#фNkYg?Y$fUA5@)vO;4ZFJOM dg}]+M˵ ^g  <5Z:McÉQ%FaWi2$7,/yN>)+gY7x݁s'9Ug1!ōY#ǸȎ&wP/ft0Ȇ+%kPSu]R{ث#I)T2 >~&S-m-HZ'Sʏ('̖+)O%a>DLxX,zyY$X5,?1 f:ېmBx.x)#gpL֥A_tǃC7Oɉ$sR,,5dL.C!^\SFh` WA)odJVWn;ߑ җ ^?k=zXsu{8Y|wd9m ,㎼9gkG"%/'VRr[ߖzk!):!o*#JsN Y57HȚF&eRmmG(!⹌AI˩r OEu)S/\CUOהs赍CB8/1.h=:m㠇*_=l?nl` }4nC5ЄKAn тkSg/0MKL'= JCY(`P_Rn`f(rbl]f m|Bc݈?qZ:VqcbB }w3~g< +c3ш{e |{W<];N K1w.h%p"~w|[ߒG{̝ y6%\@zME8 a+pO8\)VVj2(6 1= C,=1R2!:Sh"oCύAq?CQ)$d?:< =xز'`t\i'0vVCly @lHl>& 9U Roʈ6SDaufH*rc 1-X3(~F 3V8mwݽo(S qO15d@Ѫ"N4eCSN5w#|U03/}hʹOt)0tvlĊ"XZZ!$.իW'Rz6O2elcH©\Ѽ6Q kM9nL( :l'r}113! cFk<1f9ii vXycamKF)rat?cfcQN|t馛4 sD+moy,&cCMR@x8b`" 0KŐ Zʷ:.]XfaC)k36kQ05*?n?}iS|:o駟fΏQd/3x=M/Q^!3uYDv89 ³P2q 6f ZkJSA{Ơ&cp  ^Xsqq ]]\ L(yJp0\-aP)_҅,~ BF%_qֱm7C*1<7JO@ü 10ڼ S*<0 G VV`\A.F@= F]@oD@dxSaū m@?<\mW!$&Οd#JShlҁ6/O##MOWT]3-6Ex7-#2t.8QýOsaA)l W!b˵EPgM)+bni"=Wb|q`xХmD3Mr1`HKȵ8kHO͡oY6> ;x Uz 6Py hU+y L1-!JQTx{mԯXʣit$Ɵ-(%bAV>+|7@Ig[vm V@xRGc=py @d>K7(Rˊ<+)6Β ₲DQ>(nE]F ]<\G QxȎ7%u xX8]eF3px%f$^Lx.9/ "»/'wNx@x^B5lYl `I]NZ5!{)Q< !#JM!T:~y!uxP¦ڔQFߌ2,_`<+KDpd)~^&q U|^Ò%K8,%Ie5}Ǣ7wXMҦřzex1_փ`^m'x?3g(,꼝x74 \iygq o x(&7 q#սk}W+=~rM-9Ü뷳1n%^0N^q_ͩk~1!z' R# JLع"L~4G ~o{-!J8Kr;S{o奥!@^Z+|hC| 0l`e_&w4|-SOa>,HY{nTYR%ss%oѯ߬u Pq}"e.~IӹR?=ưq-?C8yi"% N#BG)3cmUtJw)#`Ɖq@cߕ^xI7IxP*JNҠ8 d0@h `PKϤ+Q`C( TrJ}ʋ~zy/|{Bb;__UA6/nD5MO_tQu'(NҥNy<o&9Y&Z~H3>P*NGqy/s~:Ӧcb79GgN\|lRmIVTqWEA#+#E; ףhE/.RX-B1|Q1,07.UEwn/vUgJB!͌d2I&T Iʤ2FM-}H@iQ奅$㜜3眹1&`O(>(>~kl3 < ko}[^{:["_qG3?ЎCszx=ioĺssWP 忁S7yg ǀLNk-79Y\ZtUȃi_\/KW{KZVڝ 55hHv{ 9 еWj]:RPǹWaT@EeXi`:E'^qc0^"x> GS5$e}j1 2 6~ֆh2`,PBl6ϫB/x'bNÆ_z>zӜ#/@FV\4BXO8ަky. > wfxo'M> M5xKwXqn1n80t+4ZSVV۝#~[aWtmo0viみjySZuqU( :X8 W"\P2y&z[=[UAcvq)߭`>,2gj âMπtSU@YrGE9`)ӿPRES _ҧ \~{3Ilnp!G &/pFQ=u3Wim?e?ٱo_c GI}X:+jTNVWkua@!#Ա8Q3Miݣi^/G:dЪiP.YV}QCSjR+wFX޿ CZt)1L$O+YСqgW 0`J!Ml]Z\Б߼'Tqmm.ۖ;_ؖP {w{b}.OxL(#\`\o}tÈ<\Sdoc97)' &YIfX4;ypҧ5CCdTa-m4sH]+Eg:aK #8`0sy=K Et.?w.^W9--ghNfdqHcTm._?,m\dA_nL;8$|Ʋۇf2-߹1nyM,O}044 :0Q0.yAQ"NJ֗豮5bHc=%_\Tx8U8-=JHť\o('!pEI :x'49D( ~(_M =S=[8KzBxxTxKd_n瓇}<ɓԼZ7}xq_"9cN+;,G?Vt*~ &Ʒ3#4p{wJ2]uc`x50pkW)CemG 6#5\A)+eawԚUpMCIJ2b/v[@IENDB`Charm-1.12.0/Charm/Icons/16-apps-Charm.png000066400000000000000000000013751331066577000176750ustar00rootroot00000000000000PNG  IHDRasRGBbKGD pHYsnnޱtIMES}IDAT8˥͏TUu}_kf!Ai4"Ҹa!č(*Rwq:6qLw. D MTΩ:Ik\L&{ZWRhOڶBkM/)EK 3꿠1eI#V0^YuOA.O"W[x*kGHw{1:띪j˛g5:E-'7=o_p|ÝFcTCo\DT2KbZ;f?~9RJ~wSքON X]}l4-Y:<#r>N$I:f^-x2}D.r`) bQCnά62~8fee4 ~p4DK`DB ϝXR$ MlksUW mimK]4%1Ҷ<բ77u&шEN($kuGWW_;!޿χ.ٟL8:f6sE~; h{K}dgg-F{#f{a2IOCA) I {6x1`9",̿AL%<.IENDB`Charm-1.12.0/Charm/Icons/256-apps-Charm.png000066400000000000000000001427551331066577000177730ustar00rootroot00000000000000PNG  IHDR\rfsRGB pHYsnnޱtIME 1KbKGDmIDATx[lTUK@!D"A@+M_|7'QB4&<1A!!&@J)ZR(+FgΜa?ɞ$HdqfB!B!B!B!B!B!B!B!B!B!B!aʕ%ׯ/ݻwoٶmۖnٲeݻ/hlݺuxÆ JƍK+**koj?3^xGc{YPZBd|rÇfCBW۩_[vZVB e .THvzzZzzzΝ;r=Aa/FFFdttTebb 5yLNNԔ:3332;;+?9/c#K2>^=tP !%6mZ%ŋ?5 ---)wc C2c߿bڵDիW/?qG(ё7~8׭[G27oF#ԩSXQg/_,ׯ_30#~[@G@4"1}_kkk?CV:+WH]]ܺu_4T@?)&XH:T*`ϝ;W^200f`3 @)RuLEɓ'^RSS@@a63c69_w(!t41giC#~D?3;)n"N/ӣwY~3S ݽ{cB?"tIJ-8ٻEƇؑq4e?2?JT "ٿ|JIp1G@숫W∬c@?1_9_a/od`JlFC)T!nLajbGxmmqr˕>oY(m/v3g>cyه؍!1>7m~ٟ+,osX+$#Ŏ;a QEE'k!tt8Lנ1?~?ǦDCs 1%3xL]Q dLA( p9d}tbKWs?wryAן ÇQla#X֋~ۻK3G~hwa\ 5֬YHVAQyc\#p@7/ ۺWTO>J=1G|ˋ/iAܖsN|k>X;>7`wj#*Kŗ3DP~?:4Ӈf6af0@q@sB"ą >ٻ/so? l ;1^ះx!\|>|KtQ1wԷ?C!C`8m+`1v`11"Ğ[>;J~݈߶@ ?;@Աzlo~p1=>V tS 7<6q>؍:fW:&X ;v۳?GBϵ'" I*){4b5m1l⏄u$V,}3):Jg{ " >XK`&4[s54gHJRBO'f] hKFz&IySP= ~ӏ }51ɜT|MrWzD:[}A^5Dw&y (r[omgY?f\9_Ҏ֝[*z_ߛN˧Òqx[plf ?1{$]c熧r$ҿǟ2b)#5v`0b>Oo?9HI4ȉ{_0 Cxޫz(-m-j4RnM_H z-zYǕkq662OoM{7{g\uye'Ϥ2q*?]d*͸ʖd,G0Sd*UyIȱ-kJdIBӤ,JB܇ HP\p?;0@>Wu~>}΋>%)Qߔ74ڏ~= ,u׼OѧDV?K׊S*W+>0XZ^D=ş S'e>ťnr6N7dl>:I/j '')~D}JVz*~ [ ۮV.j6s7Mӵv8:]@ 2%\/&Dbi7*UE[ܶW#>,?K_aU*À3x%\{E}"(Ne??3 }v(wkVX*dy/[l$p[f.OMc<zs%xٲQ ,-6u6?Ѻ#[Gn|{#Ճ+[B|tߞIQ@<x~RWpϋ~8O?_WQ{y\}TBڭg2"Aʑ ax;7S/z&>.ul|糦kͲW>\7W:>~m>Z%+OR1> [dYӉ{E}"(9N%ᏻ3yF|dKy_yB)`a=f^{ DP //5d?|3}2f<%ȵvqVſ,$Cǀ x">g?g]>D6M0)Y/>wG} E}n,fq~4MO{\Th6]QȻ/n? xJEYs;bikWKQݒ_($=_vN{&Wǎ/tB`Z O2Շ[`J~YKժN]Od%bQ$fQI4|ש՞j4?_~^xϔWVpwMݺ{oˎAn&iml) ~i[q瞠lN`˞}]{[ڊJiSlV 6^.oz|d`-u3}EҒWI}A~=t18(=b9wz*S}g;6(U V,i[aGeMB oY-w<~}KݕcOÏ{l+kĽN(r("[|-Qn:->:| 8H>Q*'x Bd~,,zw=S(:hε񋬍?OCW\q<V\㚛]KKkjj+*.x"XkX,bD K_康h`+ע@4L2( \߫AVFכvT74MȸiTQS4@@ ;xpD@S.zt-D;CW.Pc R[qoO3^f5)75c(b/޽{&H?&@qJS)5hY"ʭʊG 28>wSr=׏",c%ޮz \="`o^1Ohjiz*=iP|,3^J>#^6y{o r $)/H,t֭[BbmVۜ)7z';[h?t E xUޭֿAVZ[dߡL&O*X ٕ)P@@e_!Q܂MQjp:Ϋk 0l^:![;V?ֿ+^U^Y-(⧼dKe"f2R0(q}XrES)?ڨ[N1_;oY!J8$WA Zڎt]qW6J:\Q&櫁Ux˥I;Hl~h'"y,욬නg+mPh5&oa0Gܑ׫^ѣmlmYvW0K0QW\@.X0M4^9ED z,=n #9O[?W oymYEIa&S/.j_ H$?JCdKFӲU9G!MEv3z*4Pdgܢ!g8rF,zfa@ɥ x.5,X]Aӏ`1ॗ^rF& l1DȰb]1StRdˠg!>(,|/(B(-Bne?ލwC|8)٧L61PlB> /ܩ_t<u~  .KnUm{.yl(~ )7rVVMqvQ gA=m;v:To ⷴiJC-u2@1twXm y1/dւ"x<dד:b ՁX"je[Z˪jIM1Ѽe7-Iy8Z;5X{-ĵ yU=ђ <,"j;;]xKKnE +ް.@ zݏçYEu ^b s Qb Bu(Юk{z,.{H}`#,csZϻ[@3&|>5#Xת\My71"X"xg܇Z m<߾ ӡwi sv5@( `CZwˈq=D[-PihXrJf6Mb {(9AP@%x(*hXnVORhLΔ;`o2 A]{Y蹋g{O@x虋Ѳ,#}c55Ք/+PP9z9fsʠƫ'jw&{=[}]k \x>~DvҁGR-tP\:OcHcR\(!C=^8gM)1K;w hg\c_߆72nO{ ,G@Mu ՀZfi@\sȕw} OH>IPVa-UȷfrV;`w@Hy7tihn^Bw>g37ȍK a8M 1ˊk )iG\6҉g?#`4jVGݿl?ʺ "7L!7A~!ߧ<8N*r=bsCc_ m* /tH[eKtv7LP$,~:0c #˓˯rslCJkXxZ~@:5U7^}V:{lD/]ߛ^?߮ȋ/;snm9%/6~%,.sKu(=tEy]yy,@(>aX eмߢ\qZ[jVe[}64'{Fv\ K'%I:p}_ a@(\| T(u`և(hQ/\/RC/J,7u]k`2Ƈy:i<6Nmq2r^sΝ+΍m;+|4{d"xsuN>*_鴨nK>0={ӁWm۶ Tں:(u#4[9dm0~)27x0Y`|{s~?H"XS@*HmZ[϶1{H?5TQE xPA'r"U%+nzzڝ>}ڍސΞu;u9!< 6[4.q؝?´n+>SN9۞<@A[T+/}0SO0HΧ:>OǓ}ϑdAڦOut*j UՇHl|74A dÊ xaY@BRN. ^RwqWr1i?5ɖ7MI]~aZl~?T:v옒=zTe㩧r۷o'9'}PnN"#ȡFkzK^B8˶f-6@*hg:U54.?(n"UBG l(xkR@ |{B/Ơ6ʯc^G!;zmwo(Ǯ-ÇބT|IqF(a(]ת@2[,C~1p  K3fCxhQD{~L@(/wϊJ)hi5;[@(M%HӨk R m/Dw&s\І붵8AwuIu؍\= `[*EqfoHe'pe60`1rϖrw\d;eA-pA9F:1F;bf\>ǑeTs^G_{Z/d)~ 67O&?oe {hF~t, |t^\o`S!iþ"M o&o/ر= Xd{ݔ gF؟/|<Äܶ5"!?ٶu۵z aLP@ e:,ZbذoؔT7640Ƴ'X,Ɇ51:u;︿to`3r͹L*ڮ{,]o4̢"G2@Y C&2*AAe+CquW%CȺ:U_@d$$iCK}+&Meh ^ u/V4A/kf,o\ò8To44o9@sojA(ᚇ Zǵ7U `&mۤ>˵a73ZBkʗݗ{ܽä]wcw?#+}N\y睲|g7^]{ ?۟Ƀs35]Nvλ?Gx}go ynB٤fw` !(ŵLy^ ,zcyMX<Ɩb8ﺧ P7{t,y=Kxe¾HP; K+Ȅ6o<033L0OD%.$K$,FcaYB2n饺nսUZ% RKhCB@ 09?]tT̍}'O<ɓrvG l ($NSlEJ*,ɹG1wBтF{=h8Yɜw䡑tV¡Y5\p2`>4;3  +*󞄳URl&wi8K94˒LW:-8I>pN5\­I,S(9=Iy', >uU>U#U8z~b'J, |`A>t]hk||Ǵ:mN(>P^pOyw&i 2 x:EsnA|Ov>6G |GAM2' ,t2YrB&(S٘`9o\Abah0s||ZOڛz7 v9VF*8h} {;ä6Yquyq4$xYVIyuHDYuU{ F@0:^KA~p#?*/@օ-ٸ c00a(  8)k!Vx0?0BcYR7QajL\7Fxj@5_ԍB-Bpgap0rڿe46^Dq/.`ȇCЌa(0Oyn'%T+'c Wh\D_Lv,?gnV su r.+!u#y6y9lޯkY"2:G;2r;da á7:tQvN+ d yvO lbٱS4"'!yjhx_n,{49<2$8 wNҐ& YkLRa$sF@eTf'jڰ*!dQ0.pe|]5 G$џXQQ ewơ̏Ƃ}"ܹVEC fʺXhԳ&ܫ[kܬCA^Cs)|{feeG๭lE~k9(<رlIӂ);(NܗJ)rlړq8!Ɓ/ls`?Nh `fK*%{9YuHflw*@Z(>3A7^8G1sF'bI0ULyUI"Obo>xAxZ=WۚirzM<R\ &b>MfGb$.H^-9)ʺ)GM)6z遘G;d8C՛{'ug:xw'pױ.YJ9k%XI횦&t}I1<(kw̶]#}<ϔӵt1O{OBF{>\Jpܹjo{ (uz @@xY|\, H+ TIq;AxGwP;%<)Fۭ;K~ OО|ء_~DBˑʂ/Iɂ8Xq-"Qj{6ZB))ӫ[ }|? $4 Q@g|LH?{]3zH]QGpNM4hX `]Dq>Ӏgv+RW`Bg)f{Lx {hLhǯfL]4kw%%z^|E)x +8T/`OHV?F1tS"UAn2+VR"?_=\ԗ*Ocrz:U ';J*tk%߁]%*-q_ujSg>? 2 yҞK^3]>NR}թS7{} ծԓr]:QZ<.vm1OM=]J>RB`K혔SWhuҟ@0U[Y$1o65s劽_)VB&@xkWA+}:ϭ\Ѣ]rQD)/?Yk'MݵCWB'e$ >ݔ|\A)K=pkJKKc}5~c>mv08RwG[`ut'p|劲BCA ?Z׍`B">/*4L6ڟkRB9˚P~=CKVT )ڷ[y\G>\_Vpx^-uLg/Po[O=5}#fr=eE[4C.'_8l.ݠEޥ xwɴN٤88?I^d:@[㹆q~yg-nLty)Anw`^ u#27zL! yyGBCq9 jN`ʱȽ : OW}r.pR<\s= îSՆN=2Ky>J n? IAeu$+i'۝OP}J~[ 8IvʘuV>̦3C+}硁 xּ+i-47N餞k ccmމU+ϻG->`C .?3G%GEW$c ~`W uԷJC>YA*ƒ.0V^v{b̆.«j;6< ޕ|x*q6BC.(:[;O3Ϟ)D4څ:K/&`[,Bh&Bp?G{siyt{ "afh lYl61wfڐ\ٱYQN@%M=L*8()|頣R/ۅC9-3ҪS%JL{Vg1H"'1)Lѓx :<0(OOvyHa棭ag! rJ*nC{srhCȺ m)uwk-ۍÃ&xށ'RfA+]|(e15 ѥtbkMrYr11<E9BC~6hMPV!4'MoH%LlQU^6)9mg-&p5Y"Q?ۗٳv#pxN`P/2qhfþ* JaPo¸%%u54K0L& L9֝)~ZmUI:Aܢ#B51`e':BCN .FF(' 9I x3}apf#|G6~hf37 aa^9̈C#`2*?@tYchb,D!iUϸ:D&E(`U&V;8\ϔ:Op/ܑZ;# ;^WN܁ˢ^Z7k-<6>%R3X1A|Nh43ڭc]D/ʂA=ǘ@c,&ĕӱY rbQ|U p>N@UV|D`QcLBmahDݯQzicݮ%I[(VP(4|l8ܲ CNFU ĒVa2"Fi&}p͕trΧA E8gYc,ʈ@FhFZFgWhtu,K5A#ڸ% F d3pU|*,(dapcJyKۿ:ur nv}x"3pڎ?["|j/|/x#8a8`RL;H!Z{vf=R# 3ubzSyꙜό)x&>F.flr2=sX"!t9oؔD_gI,l[Ѳsh'908cs W2$+%A+dHjK(*=KTR$P+x <%bu ?ɃxXߣ 6о Dљ s|G'@`(Wp 3"s3qz[.A{w~=ۿz%x_e]+V75WrIgh7;j{ۦߟ=WYiLꔙvuvf{W63ɏRZgI,)12aоY;nL:ق!ꀕI6W5JGfPd28.d m-ry0b,  ye eNA;t!pub\1wy0],O>ӇEE*rx0#0b:6%p})jKD_.v?yY풋/lf?XkG?v7WVN|[YJbmܝN n|g.Җ:M,{* ;oZ fԖEo9{|f1 PƳY-.;-mۻ݆^^9μam^io`gη"b8ϴw/S.xsfQ*@Q9vM >dn^Ld 3ycb.&dn226?8 uDekvW=A2P Lqh0\DYQq@2"ńȮD+^WZ`$Q=Di+=-rH/@?xC+eQb'R̃@fp}~sK(@ꋄj ʌ4Qf(BuN~(Pm/fsQJJ(/YIXo߃Cc1yG_@{;Ń(F} /ϗW믿tMOۂXW/۷ok_u]68sۦR$.*_ח_aZ'<1։>Fq0X'yK1-y%;ɇ0/u$v_fg l0מÇmmo?֯3.ub49߶Qm̵w6.yԶM9f)q$w,oonS̹v.٩faYmkDn}[Fܞ20kǢC{"vL2'1-sU:gK'QG6 4t|G @BUveis3LقSM'ZEG|98MrGaCB)4 < WMU֔(ukй袋˜?m.\?ԾRh>’*9a9mc9~) F}u^N+aӦr8꺴M) >db .:Zz W, (< e 2Ub ޘ T CE7y<Ӆ-m ׆_HKhk껇mˮa15y"i(?}omݵضW~pqGAL<ˑ;vA4!˂Iz%3<\YmraL{2Wx$:,vt A+-V6$~ ^6Ӱx6## ia5SI@h .PGw%dodG>GCR u0PռsxhkYs\ǼgN뭷ͩ;vr\q^k?ɃOo~%8MBs%b_3(™X ؁8ǔmĊF`vSҷ<l$! K/>ڰ(S'h(*Kt*L-+Kqd̔j} c3hs:f'GQ_.󳒙+!jWӗz/_JA͇܃;픰C;*'NgrB#ؚBC"ی0 y3`$!F/1bCJDP5eMIݔ.Ku횧V<<~!$~h'؅45\(Z&_BI?wӟc ĆG,́S`qc -㼴쬥cbz^;҈XɈ )!D'#t@7 xV U+L7(Y\<o?_"UN4uX\&!h1dzOiht4 $0TN? * `Rf)ʜRW~0pᆮ#ism |_d=4(oɉo7_<ů)o_0rGoy{Hwrô8˕W^ANď};5e?gcX />F ww RJ:ɴ 3z}#q^>`GDַ5=9AYK^*Ao_d+1Ӷs}{@ a!g-<:/s3wa9[ul5£@W8hxLY1w;=s`F'l>jj /#B#EHt/0܃KF>x'sŎ Ȕ5[^JY8kyh#6:P壜, B0%IJZ,45ψf pcՊJiɋl}؈Np:Oeҏ3}쇾GwW]u]A$<~'L__"RI5޸+/U6x9*³\$$X&F@>m۶;N@L=OjT#ڌo Qi#H'y̜TF>$qVqOe{0zFkm&bh_/S強6ꌃ1N{S:L3%3 eA;$c 3/dĶUs:4h1d}|Xi(i5锦8:bIЊ|3jDredGPwILM%G<`p4Lvl@1#8_إP :jYLfɔg@8=0 !EϒH"2=3cM ֶ "RV_Mh%Sa6aQns鼏?Hc:sOޑFI)nh&x# 3,ߩSV>mʿ﫭;K #kDЏx@plRbĦLp ЏO^mA% 1'i.Q>&biCwS/9ngÏOf JKKЁvoQyg|Gv?OY!FS<˖VY (_zRȴI8v2@, $޶=$&>:%wV5s.S,M߂(,h0:Ԧ6ϼSOsY>dF1,d@ fA#hi Sdq(d\lW|~n딓̟F=ܻc0֞\! [ k~bydZB62s`dQw[n F>,%腓p@3  :ѿX=(T(MjwzA*/m쩿KxϦPҺӀXמI^.(~\`y#y^>æ! /6 }I< 1xb}7+l8H|Udzb3Z1,kG|/F*MףL#rh:ʄ2GRvE7ˏB`0GoYV8eN*e35&,T$x Kh2/lBs?kG!ʈLp$6uA/:Ve~ n]z3Ya o ?Re={N|sX8(E|E|Tf~A;Xˮ c 6`F(hb mH$W]m!6v#rplq<̝;{3Ɉ&M` IP"ZhO_ݭs{%g4Kܽ^{{]σqa! Lx3&al1`@%3弍IHu1xo5LS Ȍ 13=]rFf>&`jHnWZw L + J\k33rl3c{/oOa !e46;3 3ٙ0&F޼3zT6$rBkVGrߘ Ě}Q8 (^ت|#ꦘ;OՆ@H^A[Mvc, 8d?N~ks-\WL m/r)j!gڂ*)Dp ,\6M[2 ;5&v$QM@?Fm{)/8|* p)c9@)P(ױ%iOfJ7Hƀ Ae@j)pٟ/O?B 8>oz=U2D/_r>jѠ5,x_=~vvZ8f?|PS?\]g.,+xD $a8D6 $y{prD3uOM1G8o=p%z@}ƞu qftI`CFca\P?p{G`kP$PR7m 8Gc~ [sb"hVc*GL %H:qm S>k)! _oXX$4\uw]whUwm,TX}qODc 3I)AR59ҒK5 d,1^M6?cYN$e HZ=U" NGg-_pI=-oFg/mΤǟ@_\]MxR_h07)G lA%:d J=I[L֘\DRp % RyL_mjGo("I~+ϧ-0T;àpHg#09H_~[?~&X00U8Ah߮‚0 %}o5AB#i02z9 "~b34"ֳ}nNScpNLΉ0 6 m U׶n$UZ.T^pNؚWROI-TĻl^x[jX .тY)P1GNGǒYװPb>\RmyhGc+ij!'<i!9n` #sS QXG%I__9L\"GTY4ż.fK/ߨ-?%9GYHGZpp_JsB v9]y_BEEh:kfSS}H|k#TkyK5J*Vz@;p"^jpc T\NUc$HIRG`*ZTukKgYp b^@zM"!$p  _L F8:v%()0?99sN̘v7σ){U'^c,Ԇ `'`fވ0aA ]ioT֭B}-&RZQh?(b4D8dcL-ҿ+n%IIw C[`BkImԞE8?MU#I  ba 8@Dm=肈uՁ$It@%g;~;\jvè |$d2-&E.J.F>m6ܘIIԃ%`?D}:T읐 Ta5}bs@[lצh\i7]XK럶f]L6+L*}4Szs8 RbĆ\Jn6[A3m y̓5GhJ|pqq>KK]< @N#4ƹIJ)Bm)Н?U%*8eF⛑Iiڙn6!( ]4/'\R1s#YmcKOY¦ʽ:.隀#dK2Jre6s̘==!T^_lhxw͍[ /1~k+6}\<mB}J90G3DwcRD&4BQQźnL jo9UZCzk)8Ye~`~CYcfL@y3€Oq) Td+tR GvbCᗐcڲ{# 2&bF1yAYm2lc#ƌYhAPlr ,p]i^"L09o#)YM!+9KӔ8wmV8X@N6b T#CD9c g^E9“?M Lv*N=9S%c0o3TY~gෙ@"‚C0d}v69X0 ;705>hC"[v pNxw6uގ&>aP d7`Ł'N$"4 u9 3RP6m i%$VkJR#4W$ ' ڨN'#/6Rc2Fx *B1<4'Y@1XB]h'g bL v`,r(b~Q'cXy1ɏ`ιZ?@?W LY)˶:6)vdUG[Yn<ހ MhCp(S /򗷷Fi*Tvh$3澉}!/<\hH|h7SoAd8+nD%/,8^ul| g&{5RI69\HrILǕр%O*بQeW 08r@t_#i:85c9xl228Z(s@܏UQh8a,q`B}TL~3cD h qq2G-͜Bz>m%)46vu|*G?@ÙH۟~h!)m3 р{خD"2+̉~S`չ>BWڪkz'0R!alfqFRRBv0&[hՙHV?Cy ?l\з ^ }^دv[>B6~ .m?; |;Ch0~|ٙ=Apľ}@{ hxl~slE!r;(P/&dFc_[-BE@Sp 5ƩAroR̀1t2@KEˈLI_*e| 7/&ǣCraB.$ !l*Ht:Sh0#Ϣ*T|3 C7u\vCZ{*ٮ+;:5#=ϋ0Lıa .ih`dazzǡqz}«_aZ_qm6dO6m+% cZէϷ=Hp#.X}0X Gf?_lJ0P߭;ПBGUcij-tӨH2ǫjE4i!" N:*rovS_JM HRyVؒ:fJ wR3Q/,`>˳O%%@},M&ag/dùo#Of`P~JۚNs?O/E8fwL= Ħ:5GAuSZŸ"F prAP~~C0 -3uF@J!3Lu?u Ƚa[D@y-T] ! K%i ULH$̛X(&_I:ςH 3Øp/ #!'mVT!>㊝u+$}0!Ɓf_;Ub*LMMg .96)$d 1dj~12DHL 0iN 7[g~w'szH~ S= G9IǼ gNo=EIf{bb߄#ÈNTjތ7+"|liksg" ZۦI7908@i #UvL *%yG{< JLU#8zn7 eO]/c閬L Iƕv0"@Ka.h`Kv6@avq0,)7@2 CJsv5Uգe0rKrC- 8[M~b@P '6, 2:@YwhVy6)?f˼T"Ds2FӟL )𻹛LAptGsfT`QwFd.TȌfb,qeN.3ϤU!|IRahȟ{O%JKmxSp$&݋~>OAʻ5˯6r 74 -[0 s\1gYKR|F )GS0=>[ČQ/`؇<,-/zw'&E<1lXMsP@u^S).B21+Օz#tbcx;"yNJϷ"0'z0-BHǀ(~1HfL:/~w>3#JR[F*@J39C2҄$߉p#AYF[WewϽz7Ο3(Mߜ# CEn~89SI-?ՇO]rI!\{| :=3}O>9]sƯ uf/NNŊ|2m8qBڐ*zʉf yt9$ .Π z(KV@VCBa[R&g%?_~ =Fq>LmaI+ ~"3@a9a$Xy>0[L;F20uMd e qDh ZA[Xs{q{)^;P7 V+SҿpT2s \Ud{S9'g?N\#6 8WUd#$D@,SEuxh8$Dݎ Mɩ#iB#S0S@Rme 8f1l1')j m.܈ƐV(́mf9BP&+(7}1o> A @JHB\~Ax[sg[7Kjg@dB{d]*LջECײEx&o+zZj^Io QE=zM(XSbcBXKẉ+@JI@y&00:GsSCҜDz8DK,.ώzgw@8Bjʌu㗈#s|2K1/t G1a_A&Dk^=UԄ8̄QIkldB-|LD{p HA cS_3Q)ǫoX&vo$ HE4#i(#zƮ0(k}KD@z !u MD`dv]LR"IH)N2[$-D+$3qu\ٛ7=%N0$2YkM(N6s`Zͧ.Aᇠ~b bOǙF >j'0 ͠@Xgb2 a*Ym>W" V}' 1 RK X1NF`)a)MfM Z͘ЇHQmBڥ͸Jfp8ah%,1wjmgq N^8 `l;>dFiėsl.񮸎#S.#8G?ܿ馛&?qP@`da[Lh޺裏^N|zʞtd&Fw8+]yI#u܋$lfix𓳯 7cFJ;rdQ`DRMTj̢9Pݩj/c/>(,_33#I0un̈Ӳ\@e\3μ8'K;Ši,\Ӽ2wA+s9H  ?@  e,a|P`G6Cn`5hP@8`6nLDBILqC-[JGF\ulAWqU0W q"r-H<#O"nֽtEQ 1up6; x&#[\D|9sUqO`D1[;: ZZsXNr#UH&yP[qpcqΑ% v . ~-'ǵϱ !?>4U(54B&Ef+4גnl ~'59ZID`umʠD !g 53CZmN<9ا,BDh"uƢ9A]G#P$i) Ny^ܡ5a1v)'-%/@.Ôx Ĕˢ(~L@~ro)ؾ};;߮1L[??6$oG9*ߖR?sͬI'αlݳATqVey*΃tBo#ނcRK0!HdGI*mroSD C%Q BxLGl2;%Vz$~q7'cO=忑D~IlϜD81kNb8~X0N! [zgu-Lb:cm4ASsÏw>Mz 5Qc5LK83};&SV'R,," l9 `G? p=[w2e; .B{kSZaW|.5R n*.9?Y9`0^ÐjrCdD𲟑Nf{`κH)>TaiYub4qnbL.魶q sI (D"UcӼz $xa=:r~3oD\gŨyXw=Cqy_cbm:=YŴG .ZM.׵h!?:wR1|h?>qnX8 z?e-ɧvIH2H,pֆ@1L%S,81@~t>Yl@l؄NjpMX|Mo2ueh}xI?R~]/bY0φ=H/"ocZapprI!`Qocіz]1Xah ٽVofzv ~jɷ>vo"@6#aֱ1]0*P @}G m`1ZZ H}P2ާZ9anK־L:_&$˹ͻ=c{^q`|d A0|.t"^j$MlN*x7 `E#}bRpD"\k6?iH! /B"FjEsLxɶu sS.kXMRX-O„" .yls=y's'ǘl6X^`;XoZp2 g`7&i Ao%?0]ಆug>{K7vy1ǀh<`07BC<= A]%`w ϽH\Kua,v"ɚlzN_&8:\k6 ^1={u9-?³_We 9KƢĐ)H4Vqr*C 璉g$c`>8o; `TrdBe3 k\{&|l|Ly(;C$p樂 |cBU_V믿fC. @z I4GAhIfqfg?plɀꄖ3b3F}I'=>0^ը0BvwDJ'aDvpB9.U;0rE Ivݵ}{)C?>tC Jfl 7K5{ϖ 3R_]|O9xUr3!3#g`$dP 4(tZ R?N1* =ylvƲ$>_  y"E.'U9 [D}/}o$9)ne9緋ϟܪ{\O2üaa M;>;W1@31b!h??bx:eеߣ?y @VA#G4cY9|[o9q ?Ǝ=H)MN*rYԃfۣ|s_Sܨg8Wi'AwLx4&qɯ*,c̚jgAԧzvNIUNݲToKrO *;b͘ a'%URF_OrM_*ok[YaGfHa`hƉg)M=,Z077mؙf#P:b.bZBiu9~7ۀر]Tuc#2G#8Tt d&.9LԾj ֍b g~=9߾xJZjSB)!5Q?0SגC%D]UױYNrU^?=RFd]:F>0f.М` umx?T ܝ Ooֵ.`\՗ߪsNU1O7 yE] "y Aj<[^0*A৬[OTM@\Ƣ=chh}&9 3$]kcGlk'W .s/\\ʖ3_[~ j猺9eҬX0 '.'U9%M?\S 9/}i_uNR}=A./x \ 3}YDQo|#/H뭷MP*׶s)<|]wdaE"%8rd oC(lm ?Bֲ DFwo… p o{QWI˦Ї([; 5BM вxc9F}8k \w$x6J6Oڟ’x+سc O@h?ʮuezR'qum|~г^x d $?fL¸ܯ6(j'ɼ*gxzKǓz/|wZ^&sr$|FC_!niKR CعSO ud+)fs@ tjjͽm FCH l!>=Ml5pALɉGRd}a+"Z<#!}KQE>%W@alRyJW7){߮{Y>/fqe]\^岺wWi+){ ڤϥKX&h?F*$ Jryj<#ӺvE_[._*MX.ՅWL IO2<+<[/kIZEcv~}[`x51v 6aGkJ9&lC{1Y1h0iT~"s]y q\X:^? r8.q}/̂lVF+\zNTvJ6Àqر}rI1y? G#f0>Q!FCZڹ/){#)18d|A}H7ujF^Ku\ؿ_|1>Ϧp||zB ._x ?^(qk_S},o~sw1j|[_#*}TihYSd–l8Y; ~7?_&Q~$exUvڒ?˼7):PhrwiӦDl;ꎋH Y MlAaZҚTUAEjK(ZJ )4*!oY<3flό'6NځnHYҲ(-Te!˯O3NT{}}{3b0vv= ƙX ◦I aX4.ٹtdyFwb62E3`c Z\`Cc&z3Da -a>/Dvz^o@q`Pqi֍,iJbezr T,* ro6u b9+1X'z)U hd5aBz s 혫l M: .30!`QC~aGמhDz8 =ٛ ǰˎFh~*fb LO:0 Z [Dpy&γ8}yzGxYYTWy@@@DQFs/pC_.|}Ym|;_L|/-.uŊ1W\Z[% 7+%-fWS0JN^J5𣬂ܟ+4gS|+4`}c%!0yF?omF 'Dd`L"}O6,DR3)氞)C8mhXfأ A#gVEIQqseQ;΂"\,fw"8/Px[~rp滫W @ wL }c<'\Ҭ/\UzH,)7ĀuU1SL.@3]pur}C%2n{D+KwMMcMC`1j= T Дa8!&C{ofԵ@~{ 4ߟ+K+ydOς;e V(jdB%tS~48ca |<=00{ؑF6sRuGif].g^p]m*̌ eX0{ɺ8;)G3:AZƓЄf2K˽G@y&E/H8\3`kx.e%ջVz,[&mZ*zɥ:TZ8YayW;#oPwjрxVQҽJ/Dp³6.2 2 7noՒ^&pihA+huʂey{KHLLh^KC535O}j+mK2FP0x/o0 Z\{>;vzǬ?T$c{Q>,R@Ƭ.*=dF_5S8\o~[#F18}kM̱13?a8GS4&i3[ ' 8|#A[JN{WNJ޿DL5 UNmZ TxҎ-mfVMR y9QsҦXc >s B1=8?'ܿo#yXhpY)- _w?-1fʚL)$puȿ| kmBȩuλ'_5*?C34nZS(.BRSp1{S=+ #L &}=v*a}`h:soȷM}\?[3:oTp3y1nN7'3_̖88QN@ܷq \U*i$Vm؄ohoaƾ7XNόW !9<wp'$e2P_q^x6`ҝ4Z*ݏo 陹͸W nLպ_ O|),Ͻ ]״Pgbr2N`ʄ&+G}M=OHt0pl0?>|˦4/9uB(81?CwOR/O͂qļ8P`ʠtI<3BhL̀A'02 H=oh04;& 11뙘埯%Sĝy^r*2 8l "ʔwh-;<w63%Xިo/dQ >iJ\K>XUvf/e Y=c& T]92u8-xڐB'mӧٛ"iw33]jvg21͗>4z1Kung;3yl6ӓyeGp`)/8ӧ|[gDOBVhknLwu2Rhi&whq>T5)W?B cu~&^L0;@|9JD(f-0wE 3.тCSOi3%n#+ֈׯ_ݦ}YZ@F}Pކ=qz/Z2e"Lx4_YhͲJޥUץ\mԷIAJc= WƓ:`)V"]9:4x<:xHSҦ0I6fgs`6F ]ܗ[2"M&"O4<{@x;E R4HsJ8]׌-}yzlr1AKb⥏3")E/uLL_}f^kqUaW5_Rվ^]XT9E\ GRv;^Yriv_.a; 'NɖuPāWLo{]ny\JU<,:j@ Ok9O_;@# z=~.Atاg̖0)Z ag . Wy74H4IgH_`ɆL!=⻧w0= ΌنnhmX >-V S01V 7` wMFFX6lP230RDh:nPS9G|kosFԡLW*o~v4&}L%&5sތBxt:g˚$ӗόĬ4E2 z?DBB̊4yH -~nŗp¹HŠv$=4ܸG?cE]Qx Au xlU }b (mA۽FbC"懁m?.ص, BS#ͻINIwǏISIm0 fňݗ3&<0q]87 B 8ޅό('tTi5%O ʞ c< ]x`qI#~LLR']'7`xxE.Äpć@1+Ak(FYM5QvNGOc&T:­Ex$N ~vBFҦ#:-3,6}1Mep;  ΐ7( nzu>cb#& 4HS%0fqqL{]^R^P1}gY5ݫM csx4Hگ Nw/ Bo  R9[o|'-ߓ>wCEX%D5O`6h{iY4[LrE{yp]QTOlčifGFG4(?q:Qr>Zt]QҢ )f0f?-|Fi{uk&QU6};b,ӆLY+O?z XpTօ23 JG31qB+0tfq "0ӷ7xF㑦Gۡԥi!j8lѠe7׋j|;,vfhh$- f#ژwO#cɯMEB/ħƨhM8J{{ޟ ̂GLˤ`&JFHd pC;h&kKJ"}D΢JeT-OH^4 CDSp&{=ꢶE_7EÛQ|ig0>,7#DFZ L6kb"āHKj934ӕ,}}5OR0X9ܓ՜0'YL>h,BES?SbM"፣ܜħΫlJ+xN6v9X0WGRaEݤNgs+8ot)F{"$_Ұ((|"~Q͞4 xM@B+`)$#9- 1eGBl U Y ~M?k)2JTT軈8@2zGXk5Zqoӷ$MH cPpp21g؁a'ɋ~(k)H6f=3br^"{2f :HՍ/c[N~.XgC[CZ-JAm2&ͫ(}wvC{piU><~pR9,OhiKꕐdy'jCMvʙA}{Sک7My[3RBu?!R^H84L. Wh"+aw"HiҮhG$bTyhTf+1-Pi||CpAx'IFT 0Nc *oOBph&5C*_<(H:|_> # I,lzD!^a%r 4ffFSGjOvg9^xWǬˣ~11 iN( -\A[>H@b\(B36O @$ߒNב6);ݻp6b]p}.dI^T_+6G@/fx8`V6BOBv.WYOe,FkaѽBcPNsWǡC3R6w^ZJmLWiÈȇvw jS<iBh0e-h;h ܈ rB4/) tQh׽w[yYOgx=̊ N=}7|m۾nB{?5}tyf-[. Yߖ -m޲eIdi¡za%Kۇ/ m[>2qG|SvSi&'ii#vLjm:۪pS)-?[i8S_wahG-n#n(eL7OlA^eӦFGN)@W<}{~,Ysk`\\$R=H'ܠ}_nrf Cۚ t\@8@Wц :8sYP ojMh$qYKRV]rX^+9Jtՠ6]e2_,^ߍ 6 >,x ?+)yZ @.*xm!dp!t$Q!u^3;蠃5Q<⹧ O# b<@Ah"֦F6 ޳)R8GiVjZ4sBrB﹯SX)/_fll Ϟ>OD!" WoU햙Bpm0t: \.3t(G }\AAEaTall {v&IΞ=g &O3`~%077G{ϙ3gxGH{F`ّreaq񏎳8K4uxWT>~nsK/F đ#\vf0fn[)r)/dY (:>ڭb)W*DCj(3 &{}Jh4*βo>:"ի`ۦ sRoIӔFN/Q&=(&(y?8C`m ʛ.|m={n' 1חSdJaDO=cQ9)_IoQР Ƭx0"^ohUū\0 E+Xk{ƘSNzng۩[$|٨IENDB`Charm-1.12.0/Charm/Icons/48-apps-Charm.png000066400000000000000000000070571331066577000177050ustar00rootroot00000000000000PNG  IHDR00WsRGBbKGD pHYsnnޱtIME u IDAThm]Y3sι/b{m;8iB$! ԪI ɪZZ-"BHP TC4* 68Ӻrص{33Ç9ݤEmtsΜy^ w>>,lr\ɡ?iycgs=>>?󧗗i4yNQXkj˷mVr߼[1Jϲ OrLi:3t~HUUONOO|ijϙYxy6]?$bW)d }1fɭ pm3P&ª/17d`bxk`<_Y yon K#|br|1Hg\WL[%Ti_l󖱝.l*KKZFye#7On IL o|o+K~V09'N`7K&s9 (F ""g!!oM8SdngS]~^z3GZx#)Qc@>*=)[ݷ sǗ/mb Y̳,cnnI!dsYDsFTi9_ׯ}7>п= vwMRQAD{"yw"TUW)O}c[GBUy>CyUUUUQUB&sI*}A$bml]w%Xu0JHWR%KK|<'+og`g;ɋ?&evvvL!s9AU0ƪj4<jJn#"jDc5!r^z(qKu|dۘ׫*B9ǏC@E:D ;p腽h7Xa`)c R[ndt,yGykƿ=1FAVU(;c̀콙`XA5'FZPA1 V=2cZgU1uѩhlۚأ&k3S(Gd99PwXB/Q$!GFlqbX4-4&c~ H_sXSw<J |Te>DYt2LU( 6sT1!9d9(X n750 ׵QC9s*RN׉*E^n+1R~K{/@˒Sɦfkk<|.9=5DBؘ"R'BD}b IꫯrA:ıc/}ELAek-IV(8X m)4bDBωt͢M"_ZZpsMbiiI<9r^M7DUUEN_ػom?ak=Q >B$FBTʠ|.RR(ĘDT\bzo*> ]{-.*nJ56o?9uTGljEBY }MXTtD"{ ƈJG4&9ػw/fhYYIx6^dZKAQ Ⱥ"Q7(AMʂ4CHSwkHtسg1F4F?S_Yc/ر^#1 yZ+>@ Z[=X(j%E C3(RԞB277]{Rrs#0cd##`,KڵJQAC{cLnNNSE!Ї )g /fQ$ +n7#\jyeYjs48QKD.rE콯bH\ Y*>M2 n5hHdddٳkZ 8(nw`Yΐy АRQMGU,0L.q(FZTKeq"/$0EQpYǨj&/Jĉ4{ KB$Y6ŵrf!DB8k "'##uP3kk#@Q,,,09990B'$e}W:B8J&QL=ʽދ;v{G_:ƽCf. ,wg>32PX2UH*.KԽnY\xc vx/E18瘙2ZdCLD|QUDx_TYTipp &DC ȭ4KKK̠xY\\ veVVVx8C^Y{ꩧclJlڼ !6o*!x6mj'Ec 5:tVV(FI 8UII;l\k7ۈz=s!ơN}37xΝ;_m5,ʲL4TUID$' o(u+O+B!*1Vu :DϝAg99 -j1ҥP6C[SESg̠6a}'`]n&iLEpvn\Ct3 ro]CE1{'KǏM+b3?? (IENDB`Charm-1.12.0/Charm/Icons/64-apps-Charm.png000066400000000000000000000116251331066577000176770ustar00rootroot00000000000000PNG  IHDR@@iqsRGB pHYsnnޱtIME $۰bKGDIDATx[mUy~{Ϲ|]\@B@G@cf45f1vZNf14ퟶGǟ MhtPkD^TPDD8W=v`33yw_p>OYS]]&g/%5/o͛78[gɛsgHI|3ܴaÆ_;v$IR)e+' |;2lݺ֕+WO.]N +g]/ G5q|*0LeT 9rL!86}: f,X^_VKIw+V(Ӄ3gHĺ3f hޮ.r;d Yy2saj''N4v4Јes#,F(%$>.R̬gjpt$])}`cKHe[X*:x`EBY}ŵwcY|+jGTnyߞ99|AT1zzVw14SZ'+1qOwe 츏 3ssUzd,:D!:5,,FSہǐ BLKX e`GC1ȷp (]z$qt,ᱜI{xT ~G׷ۺROp" 9Vg1զ<(? (?zٿƴfbZ)Ӟ7'GovdO&|`̇?j :^mC"ifIcy vA/fBW4 nm]H@@5G6=/-555T5S??9oDmo~p/._s v{KIW>2!^6eDOdo>z2=lJ+MM~N2DV8LJh8'묧Y{>Od*>H39R'Q S|Px/XL:g2|X,'$aT4xkb{#fc;)޹=x0t6UQcf1!h+@j37/u-x;vVFx:nm<Y~#CX5j沝g:mP~itZj#XMcC7|u`ɬ'} 5ZOBT@.7:N `YH\^2ÖWp>F_lPFxL(#L1D>X [m* ػoߴ|0[@YQd ۃ~<|a'Ow)[J>>8_qϧnORr:Cʹ|^ 0Be}bN_|yk\{bB޶ }ߊ{Devƅvu7E`DA1$(@f w' ːisXVz Vtަl 7?3neGَDDpQ;sb G딒^+R5R }vCBfEom 0eʔL#1;2<"ǣ@tN3!dJ>wƈj[gXXr)SXwIEL@_>@)znUђf^!0QdWa:Yҵ5fcd-TU$ۯ\I3: $?4T!Yӎ. k`'UhY*" &vnWx"R~%*$FnZǠȉ>__H02=u``8B} :!fB鄨8@ sfs;/5VA~#o+&"YuzG˜9- 92O?4n&y]Wk:u WGj?LvիjɆh! JIZ͒,'Fa/G^($i>kqRv5ޱcnZ.bq̽.dI#P!->5kָ 2CkhMdf9A] T?JMe=k38F!ŋ'Ӊ G'U=Dw[lI}Ok' 5? +`ā:ha$F wbSDAdPiHIP&Xix͟K,AdQWd*|bwIJUBDD0N<$ǀ+`4Gvcߣ<6;P C \%ȣ|{G`jE+1⭮Dwwi.vH3W`_OOB5Vl"C#6'tw Rd/Xn^ vt6zΛ7ccqH b:.#a#@;,`m_03B9(SsP /D-wƺ++QNՐ&%!09Ga, cyӦM=SG.N|zK` ի`|g+(Ȋ&Q)ܤ箲6,]~CS|&$>.=5;834 {/Α7e3e[644{Z]}X`2 a@(`D`b5 Yc>; 7JVR#}ΔK쏈ЩQcZs?]]G^M.iF 94)'p'ww\!Ω`qkj 81sNBJ3W;b(E m92 ;Q<;د~ G%@ ѣ:u*N824k*3+!v]] r s۠av焄1;<.e(-+ϑP:]_> ٷjkjaf_] Ĭ}7e8id,2֏>BNcc#:hP)(5!C$AlfgmF2$b]W|1fnw{w#DS$ $ >2hk1M^k=2hecD, _SM%l:;Nyv hm #9s#ޮ2 wK9B 3 8~L {/ pA Mb}cfϞ#fHWlFe6w:m|?m}[NG?kԫJsF}e7-uq;U[{F[" GHoqwn_K 6MwQa z~,ִS}[\y-X@vwD%!'g<6*|>OdBȷIENDB`Charm-1.12.0/Charm/Icons/Charm.icns000066400000000000000000002715611331066577000166660ustar00rootroot00000000000000icnssqis32 !+*++*, 0ܺ ?;܂ oUM5(΢ݹǿmtBƴСǽ˼ȪƹĭĒ˯ۼqƥƷ6TPOWLOMKRc`LHN> !+*++*, 0ܺ ?;܂ rWL6)ҩ䵒ǵpmbo͢Ⱦʻ¿ʧǹȫœ˯ۼqťƷ6TQOWKOMKQc`KHN> !+*++*, 0ܺ ?;܂ rXL6)ˆӭ䲎ɴsmZд͢ȾʺʦǹȬœ˯ۼqťƸ6TQOWLOMKRd`LHN>s8mk =FCDEBI,YmftWp|il32 7K:-Ѐ ѿZФ|y޳zңLJȍl ԲӞњԀ8մюϞϭ|~_˼Ðz{|Āǟ{CŞⶶoƿ¿ڹÚدԷҪ̼rq̪KKKôĥRRRnзRRS|ˬRRREèJKKʢͶιη绳  K:-Ѐ ϷտÂs ԸӞаԀ8պюϧϭ|~˾2Ðz{{ǀ8ǟ{ZƂlŞ؅a\ۻbÚدԷ~Ҫ̼q̪KKKĴĥRRRnзRRS|ˬRRRèJKKˢ͵Ϲ΀绳  K:-ЀԀĂu ֺӞԀ8ռюϨϭ|~ ˾!ˀ Ðz{|ǀ8ǟ{ZŞ؅a¿\ۻbÚدԷ~Ҫʼq̪KKKôĥRRRnзRRS|ˬRRRE©JKKʢ͵ϸη绳  l8mk JI$%5&5'5'5)5))? l|'it32sm&c^+O,!.^.z...x ρффуу {   մ  цՆՆoԁՃm  շ} }׆  vy| ӆՆՄ ҪӜՀ؆}xҋxކxӆՂiׁmՁԖҩzy{xx{|ěլуՀҵugԭzՃԑhтٍ߃|c{q緶y٨|dсֆҁՃՅՅ˲۩߅ɰլٍvζр ԪšӺ"էބccуՆՅՅĵէ̱рԥ~tg͹ҥҾĈҢwрcффуу ππǭÈ"Рq̺ ˁ͠lǁЀ̀Ѿрπ–擭$$`΄΄΄΃΃$ꄳŽ|ӊҀɆɆɆɅɅЀሴ½S(c؁ՆՆՆՀׁ$+QͿ-:5!#   ! #6:Ä؃ ڀׁڂ؀!ݼ˱E¼-ͽ ȀȀ^ɿÅXЀم߀1߀Ӆ   υ   ̅]ԀҀ ʅʀЀɀOɅŀZȅƀ:ſÁDžOȺȈ'o̓$uɷʾĿǀ$ʿɿȅwɽï"  ůĿȿĽƿÿo_WNQS_oü½ą¿DzpYk Z±GƽþĿ¿~_cëhY€ ýǾą-˿ľƾźżÿÿ€F»ƿÿWggŋV{ŅľĂAþ½¿¿¾¾Waaԍ\€…½NüÿŽ¿ź¿ùdgyĿÅGÿûĽ Žoy}q\¾þ½½tކqýоĀQĽ¸z}bmm º$(ffμϻ,º 䖇ɿ n lκѼ? 䚏θ?˼vvȼ*ʸヤ˻5ɺ ޺ɻ`溏ɷS·̉ƻĵ5ӧhςیĶRŹ%#KFYvdUEDEBDBCI%鏲ò7$#~"]'$䖱õDÿ/Мųñ7U忦Ro泬wNꤱ oԫ:¿·ƽƀS紾#*()/'\㨻#CC«CC  ̴_̳JJJI€3  (f{ {a2 ƵPٴƫSJJJJ ZF KC تzqٱ\JKJK !!!w8!! !l<  !! ڲéYKKKK "%$"3#[ga#)$ "!(% UYA#)$ ӹըSKKKKJUQNaIE8XWo&Թ ߰VKKKK#"" "##"6z! %"#߶殥wKKKK& !''$5"''($Mc#%''ʿ媧wKKKK-" "%)tosh),--!`bvQ#Fca ,-##ϼӧ/KKKK!/1*+**/CVUU+0D!"//FZZU+-\[YC/00!&&浥#B@ABN$%#"$$%$NN$$#""#$##"#$/M⺥)ѴҴѲгՀ0ΠҦūov&,g% 8p7y&c^+O,!.^.z... ρффуу̀    цՆՆҺсՄҼ     ӆՆՄ Ձ ӆՂѺւҺՁ ɽҀуՀ ˼պɽҀՃȺт̓ºÿ΁ĀةйтстՅՅͺ۩̹ππϼҀ Ԫ,էӀ ѰְуՆՅՅǼ Ӏէηрԥ;ӥȈрҢрՓффууɳLj"Ϡћ;"͠ǀ͂ˆ#ݽĖ她$Ɔ$y΄΄΄΃΃$ꄳÇż|әрɆɆɆɅɅЀሴć¾S({قՆՆՂրՀׁ$+Q†!;6!#   ! #6:žÄق ڀ؃ ؅؀!ݼ˵F-ο Ȁjɿąπˀ"ЀЀمwӅ# υ) ̅ՁPҀ ʅˀ΀,ˁ)Ʌhƀ ɅTſĀDž#˂#tǙpʘnʂ#wĀʾ3ƿĿʿȿȅ6Ƚ ZTXT <ſǿĽÿo`XNQR_nýÿąwk^ÇSDŽU}iXŽþĿ~^c«hYÿǿą0ʿûþĿĿCÿVggŋV|ąXĿ½ž¾¿ſ“Waaԍ\ÀÅY½½þŽÿü¿f fzĿÅ(üĽ Žoy}q=¿ýsJ߆qþ]пľ¶ý~z0}ѿ$:mlҿ Bf bfξ)ϼ% 䖇$ nu lκѼ2 䚏θ?˼v?vȻMʸ<ヤʻ ɺ? ɻ`¹m溏ɶSø ̉ǻĵ6>mςیĶ#Ź+%#W?[vdUEDEBDBCI%鐲ò$!$#g'$䖱õwÿ"НijñXU忦po泬wNꤱ(oԫT¿·žƿS紾#*()/'\㨻#C«C©CC ̴_̳,JJJIª     ƵPٴƫ/JJIJ      تzqٱ\JJJK !!!!!!! ! !! " # " !! ٲĩ`KJKK "%$&.'"#%.)$ "$$%+4$$ &)$ Թը@KKKKԹ߰VKKKK#"$$$"##"##$ %"$##""#߶殥wKKKK& !&)))(%&''') *('(( *''ʿ媧wKKKK-" !"--,--.!'.,-##Ͼҧ%KKKK!/1+.1-(''10 ""/1)%$%21$3*100!&&浥#B@B@N$%$#$%NN$3%$$%M⺥)ѴҴѲвՁ׀Οҧ ū\c&,g% p7y&c^+O,!.^.z... ρффуу   цՆՆρՅ     ӆՆՄ Ձ   ӆՄՂՁ πڅՁ πՃуĽżÖ¹٩тρՆՅՅμکͼժϾԪ>֧ӁрфՆՅՅԧκρԥοҤʈрπҢсффуу΀ʴˈϡ ΁πΠ͂ň#ܾĖ$ņπ ڀ΄΄΄΃΃$ꄳƇļ|ЀɆɆɆɅɅЀሴŇ¼S'ƃՆՆՂրՀׁ$+QĆ-;4!"   ! #6:Äف ڀ؃ڂ؀ݻ̀˵S¼̀ ʁ ŀǂÀ1ŀƅρ7рЀ؅aӅ #΅̅ՁNԁ ˅΁YɅŀŀDɅTſāȅ'˂#t̻˼ǿʂ#wƀĿʾ3ſȿȅRȾ Ŀľ ÿƿȿŽƿ!¾o`XNQS`nüÿÅ2k^ŽŽjXǾǀĀŀ ÿ~^d«hY ąwɿû»Ŀÿ¾}VggŋV|¿ŅF½Ŀþž¾¿ſ¾“WaaԌ\Ž Ľÿ*üÿùffyĿÅ!ľþĽoy }q#¿$ýý sJ߆qþ]ϿĽ¶ý¹~z0}ѿпElmҿ ºCf bhξϼ< 䖆$(nu lκSѼ 䚏θɼBw?vɻʸ<ㄤʻɺ# ɻ4¸(m滏ɶSù ˉǻĵK>mςیĶ0Ź%#W?[vdUEDEBDBCI%鐱ò;!$#g'$䖱÷D."МôóXU忥wo泬wNꤱ(oԫ3¶ľſS紾*()/'\㨻C¬CCC ̴_̴,JJJIª     ƵPٴƫ/JJIJ      תzqٲ\KJJJ !!!!!!! ! !! " # " !! ڲĩ`KJJJ "%$&.'"#%.)$ "$$%+4$$ &)$ Թը@KJKKԹ߰VKKKK#"$$$"##"##$ %"$#$""#߶篦wKKKK& !&)))(%&''') *('(( *''ʿ媧wKKKK-" !"--,--.!'.,-##ϾҨ%KKKK!/1+.1-(''10 ""/1)$%%21$3*100!&&浥#B@A@M$%$NN$$%$3%$$%MẦ]ѴҴѳвϠҧƪS'K&,g  p! yt8mk@  ())!)&)-)')))'))))))))))))))))))))))))))))))))))))) JWSm6MI_$\q#7p|yystkodi\dW^QUBK)0 ! 2WwwS0*h $"$}rmiPPn Qb(7pE ̺u?<l Ua2Ieu5E`/mH*´Ai8\I#({pw u>W37uiFH|[z#ZCA% 2O)ā;^&b`#!+̱ &vT-lZg9֩l?ԁ+P$dƂH6c3<WmF>jk(: `țܼ= EUFYYDp8G^3*;>|}CAUҰT6JdL-OX,AJ,wMhAMBz~uA0ul &jAr$@[<7CM a|`I@@UՊ!y>½=֞8+F J_.G5~ꚢ3ʿ/i5z* @w`RVS78QP}V |}U?3~Ѧ Vjtx]BNOL%DJV%j6;Hg͈CqDgHJ5{_} `u6 P舱Ri~>A|OBxUr{:M^}OrR,qD MTGZ+eּ :ݦ{=ؖ+\lJ{ Dp1!. -sB lx숌6ǏZI(w_ϻN>r{4ϑZ$qZ-=pH1d[w}ym/%%& Gf~T\m[ ě4 oc$l =T!íyEKfJsS^rS8PyH=u64Q amdF  W5XxNA1\߃V,02v\yݦ8h&Z$,?HAYj>-x9zc^U؃3}+V?ǮRE%.;XAH)f)Q~ߜ֕Dû!ػ^w-A,A/ \8;.d5LU:K7D)u@C:ESt@]糧H=;_\8Jc%RL3kyY)ᤈ<Hܵ65ּh `kWbV e!9<|چUݮ9ЗrEx&)SQE4K$!ڙnt>HMXFU 8@AdڷurcI]1(&^,>kj$ϐGغ__P0-CZ?H~1 PVNS\=b4{RBSrYV<~f>uG5ܹϾ7a'\*m[ ~eŇpn켄74T({ZcE65P1R=7Gd1[v %N0 Evy~'4@mhmzɦeas{[=q @CgIsdWzf毰`-/oXQdJL~$ch- =xoii5?y!MX)ʌ*Y.!0⭌ޚJt#ɚQ">F52X!1/zϗ4b3nY'@U5z0d0 -J94XZހiqUO֥eF->^JB+#u<\,܋sols@z&Vaw4s^+i9oCHԽ(|[w|\7[oR^c_2^dt((\ϡ&s-ܡ0kY6C5'(Vwk:Ҕ^V@waǩx\X&a&C|w!G8D >cP?>ҡEEX>Onyk%ese3DUfviEsnD fW7P0@rdLQAŲ mQb1wuTuTgqmk1ie_%5޲0&#LA|k(aOHn9+GU>i.`S0dLY^C\,/'l~(x-y\Ga$cP'z ;,<5 !7P߀mUON(<~+=AAƤe϶fO Ks g o*,4Jșlo[A(ldQc+9u9Y٢[}1:IyjR+{2gKNH|B_v @ɢvB!h APD<ǽ[t>+B|j5)[6x̉nU4-Rb7&=s4 EtҲPf)GP5r`h8gADG0r;~l[)}P"E5?[OehAtOyc{]xE#Tnڱc ^0_=M 闯Mi}՟دkOM; 0NVV@}8D/7h>DCMELqh-ritq-J30mFb6Q-K[ss>[@t j|2b/A#rdEV>fGP]ӒIkmI7<6ŴrO6A5x- Jo"l#m䥈ޤTT'`ADC4L͖ĻSYhnN+tק h[l:+?Dz> I)&؞" ev gJԔR( g,5cU-ɼk-hNmC7BD`0E U=z &1NPVx:iV3HqR:|KOv\VuB.p4Q di@\ p8y9Rᵹq!Htyg  uh|.X˹U^"? ID jvLvV#/MڠH7JFcZhmb6 H| r V7MJurjacR'YYoE6- 9UD ey'>^n|(od+qθO=.8NQYǭ=NrP&a \:=3@QD Ra1kP@>;4 iDE3du ?k|=YZ3<<ܪ7wҹMWCseL2_X ) 0SL*ުJ 쁁1mI+|@}99vܟlJ0Q)<_G kP衾V5b$1LԠݷ" %`ufƈ@*iF7/,#G33:)rF2B@ ze^Ö&@d(La*2C5ih<~ &Qa Z9qdHWtTS7u(3Q7unR=':eVshun` wm$r;r "fŢЦ=F4Ylck/=k~Ӟ_m+n'p䫰3qf粛h4tP„̣M*±oXR`mԪYb,O1FSǑև4bj ~7b %ƋZ ww)j.u$2W6" b%#sQMMT9مZ}xhOiGHM}OLr-j1䇮G;v|Nx M?1Hi|"QgS: QI :@*\gOw`*x]}hvYOQSغq7 ,JI{=RMJo)s}Q$װO1lE<$چd(*;CQa wkO 3i-jn~LNwR InP[Aw1et@fK6( tO+vO?py4'!D1~[K_b)+qo_in2de?Zo*O]3!TfM|X F{ͼL4G(R [M\"m[ }d~=q3rہݬ;@ycjCCERU/W*VU*Q334xpV9o#C{0UGjs H+YHA1%镞dM\]qGGPOS/Ƴ%+N0GſIkEs)͏@*n͇5j$bo> J`nscfkؽRG]ʞ,FBҵ-9'q}j`w6eusأ%-H hc𲜋 w,o>;OCqvƮC*.3ϖ #׏7[93!iU8'I`eE74nzjON~0lwSN;gȾl(TGR;Wām&~k !XυH5s>ZܴBiq\D2|Й{='7Yz ')}6A.&2ҡrMK4lMf4]L 'H>Dg8[ƫ4 \E8vri!F3h,Κ09CS(X_14ttB%i(Bb 3Zcfн??`j5Sam2dD5Ɂq ].GR ]K'PWA xb]>"2K@jbǿZƢ@["uyUYڗXhsg֍ĦcáLQ4TY$q];T$Fw,fsLjI 9z0]{~XeF;X2kwu"On۾JEt"̆7;TZ|9Mwa5A(k-/* k}7~۽DwPS MЄy2eH_bPB~ȃxx07+B "m*r*֌O-8_3y2Z ^ GzT:Q 9 B_zv'/g2Dq}FaAU ɡ~r&LG{OPQ=% $8ˡ&xR38`O?aC[\- {~|M?K}g(2TBI:""w4CWw1`J4INE1QXGͿ3XZ3'h<;MKc<2]0u$xtHr.@HMI36|uԪtu>kx q"`ws9"ĄiQKmݾtڦߍ[*&avof*BPh0LftE2.;kU2Uu8$%tp r _Ԟ@_Yདྷn#Ax-V3ؘDzxgƓ;jgAbQY Gэ`28n4A#5 m`P*.* Kױ(5UxK[ĸ,ܡEUL4>=CXc^8*2gi>@2;;-8j'ߊ>q/84'5j)G5SG{!߀NAP 1PM\^F9CGfL-Ep0oz%`}Q;#+BN x4΢&g`2@ S0{\J891ɔY 8:#GV#=qUc^-3N:l}M5s{ 8 :Hl[ԜBv!Hqw ao3:{zOr)U?Mfm R21t9٩Iq'vɦRg&@Rf,U1gx])?>lVi(cA*5b wf{b3gCȖܪl>up#|)Ծ!ʰ_xlj$⽤ZW df56^\a"s%ŅQkS;TKwl)gZ t 2=rm#"9TYY+-D-I:u`K&06WPu c2i{Z_A~s%BCt`:? qZeud ;.#eq,n,DT<͙&K9W{-tt"8MMZѥb cbBƟ84LLا)F%^ :nd6˩%)#>M˞:mZ۹DLp{0,L. y%KΖ|F9^"V{QaoTFZPmȓt=`U#^jt~Ksm714kB-P[\` ̕('c:AS׺+v1 }r%b`%Wy69_3#(?Drqu7p} rZGOeZF? 8PW4R-Ӛ$WA$_hb5 pB~5]|T.+=|ye\4fʖ}ަ˛9x-'eTDu$bJm;oU3d`>w2>.Hz3Vf {?I4ͳcI>0jqnlM!o>o\po 1@HQ8C PeXl9[WfpTaP%qLH9Q;ɄVҺ}Vf;" Zy͒d#LHX~G;NR f9-%^y$3}ĸm)z"K| ʝD7'H-iռ zLΞBObqM[?lE˙]@Ӥ@T,*jPiq1|4 . !/8h,MEp# u^ - `u%8>Arb\&D:5>"5xE ˴[enu1dY4xiUnqb mJzRg3bdyl` m33o'gܧ5k JzvtvB.{tgФ>ޢm8rTC! ZNgS,VW x( vOjk 4ɇSW39E YG/mܪnn̞@:KiwLzAʋ ,u6G2#D#3X(CoScԬP"Ǧ{'u^X f*-1%ݙsI<aV$`rNgHFE$@U#=@+-7Fhw84uvguf N騶5RQY s8'{tko]K5Hʖ'JSr/2]P"[ =ܓx hV=ՐOҿ$]-g<7ZvՔ8Yx,+'اt53+;sAG+kI@&=̰l^00 .v y:\򺠓Ns"IJt,UvMն%ZVzJ&oCR挹[8;)r+"?K)oaAAc' 0KR>,@ GYvOg~{ĝaռ6")S蒧:L 0avm5Vҷq2+Gߝ]E"ϋW"`TJP;n`e1_#?o&aKkUtXRTv!k9ڌ9dm2,*~RPn) ‡ĔUΆ&ƱBos~4㊲jJ 0;Q:&W'(Q.&(-psm3+}y/p#7;1y?ێ'VeުS$ؗܡ#Nz~ 6TSeOA`?^E:yq&|6>D`f b*2k mV 4ڗ0G fz©$ P2Qa_.ga:4gU7!'ǃB.o;? !SB ^!f'ο`1"/$hrYA1id,qJ^0笆Ro~JQ~=̬?|nA#-F@ogeɦlW~ޤ=F|kD'K雏nY9S*b>}k8%#dʐRȼ``n>%ؘ,$-05moE*3 ;]_i{Ku>i]Cf-;*ceH+(x$a@R+P/~H|ښjo?Kk\6gהXJ?pp!,S@e*'\Usu3Y+Zʷ'60X)E#Uʋ[Ir$&ڣJ*]E5+E.㜴VɲxWL-M:cQߎ =H?Yic4K=_SЉB9Do\Yx>z90 dž#(C;QYQL6G1t^+3kAZӕXdH$tZUc wRg|Tj$+'{( Cʙ '(Lտn^xIk(׾Wg %9^? ݥF< t }K`61o?-bya"/?ؽ{bi(`%)o꼤^_zV25Ɔf|_p|Ic}I#ޖOZm5-CBf>愙z'џke]g3ŀF8(RfI䊏?1T]7xVtn3]E9DhǷInYv.e?@l)bLx#Hlrt󺰧A+Gw;'A;+ }* R״.7Kd$Pmץ$=ڂО8?j44 Z֮ B0-%VsWc#e|Ϳx4u@٫}^kؿN1+q-@{ZP0 7fEӮ׮x=J6-~i);qJO'$,9UuJT4r YneCb_ =|v -^BRlB8ry'dR otrbN>33i ʈ.2L2ilk2u4T`OKFk>ss,5TFإbs CiӑС/UIv@2i 4.lL8(⳧>19?(qO+4(y5B} HG6EHF( X-@0(`^Cc@_:$huY(ȗCOLm$1Kg] SClf榈Q&˳OA+VD4}ʜdFSl?,%^ܓ?.7Q#U(kk߸PHMd_V#"9/0UqiHO zSK6ǡ0X2%L%zٰl<·uF4G@S4>yMEez$QHnNm0V+N16_ߜKӶpMd4EY$0GWjɮj:\Aۖ:3߆i~P180~aj _c2t_ ,~͇ <8xU!2RS"E'T3mo)6yrO&9zTQ_k`urB-4Uqd@QLIΔ;eMBvq[q2F1ttMgyJ 4{Ao樇Q_^Fiumw'MzJ=mr +Gk,DN۰l[-U6yKkL G?wwZh?'vP}Y^ _rB_*`Ї)0$\lY"j)ǂ(!}@[hcUyz!bødWպ;(Xt9< ac|V{Qd:\NWٓuϜE5 %@OK6SI|?{"e dcu3nQ骓j!8j#ckRw}WPq!ze "t5mҸ`^|ҒDq|\ V-Bq0Ebj0iԾE,Kf;̈́'딷'-GR%"FF{gOW1QヶP+|cs{ݍ[_9u`.>Ti [I|?}(+=Pٶ^,3]9 dwmVa+8mq>ȻOv[}a칟Џw}GϷ<}pt7}mIׅOl ~%^6j !hun(i'Ť0䄢I-&p`:G6uX |zpb7g$hQ$FkItJխEkPL x*)@PmoD6.k@6({'ZOAPm *g2نq Wz*{0 :!*Z Kt  \߽ ƟJxc :"RɚHlؗvV}U + &hoޮ}tF#ÿ}ɣy>e^ݞJ/T_ y1̧vϨ|'yjlW3ӎk1Q"=pxudYyNUhbmB^qUڊ[ُ*ʼn̨޾tM^Ca.upt6l1b6W7;،$gY\.ŎkǛCwZu2YVhY @Q[ iy.0P`dS3kʹJ#(zJSow v%.G{YmZ:gf;9 i:-*q{MS?9&P]L<ZK3!>`JgEW_#ʗVP(q? BOYU [;ׁ:OA=xK%8Gڞk^/Rq}e5*'p^[g}u縖P6,*h9/alj_:/Wy[Q0|*aL8#d'mI7AqX 1$E9tH HTNHbOTbd1rus^pbw`(heX,ew_Vfw Fk_c{$mGS#@lFR-N^DBCWѐd}q",CUjYT0ΏÜeZD釨r-]5&20}jvtd5ã72\fu\?Q 9t&9[,DR >bu5`lJK);r GALIQDi-(BM$Zg/B]`"n4mū{.ޓk D<?<_΅l Id 82Vz{`I5, F6fc߰P},8tcն,"W)Θ/Nԕ}Z_$F?%˜TOR\tCQҥmN}Ux~4rf{uqMw*@yr]a;[W=v_W8V̔"]OeW/BM;L߳L˜#"pnϖt7R-5[x{7C?Xr ÒDE 55GyаY>ExؕHꦓ j T[k0uj}HRcTcE=zVC\d;8o~Kq50Xf!neN4ĕK* eِ.:k3f˭uH̢H/p9[jt,b1!h%1B?5v;-f inQ3-6j)ϔezBE`1/2/yt޲'(4Xڎ@<)F@SZqt6d|E֟Rg(b7~b)R{ '|*,tRD۫mib~vb$RᲝs>I8-8ѫRᐑXkmR4"$󹆊' 5ȵ;ń!e-L:.XG!rI>eȈ8Kw퐆Qv:~8w'e'MgA5 FW{ɨTt_$~.NЈPF0h m[r4.k,qY͋$sf:A{# ~YuSvfO Uu-3O,Oeq3\&b|.)<y9SzAF0kNMF=U-o*ϙ!"=wonV#jhNm&Dz:窃?#hJ#ۿKP\JCHPD[; ?MCW˴Ƌ(V\EeDedڛKfc:Jؠ܏տ\NR#{t}Ӂɐ^_gR3PKX4ejcMvݴ@?4wę>∲gqcY>^ZX H)}4ܲ ЭN ;u;WgRc58^9S3:vX@I۞_c~\jb}D3l  ʽ$-zB2YjYYa5ڇ~3]z3/?ՠ=JbB/EeA}zczNddj)ae_8b=d;֕=TY,zm{GgӡU6 ն͏QBʷ`())rԌ_Eg*oߌ6?Ӽ3dڈ :VеTWui#M Nɡb.+::o7Tofnߥ}Ƕ>cf7t#rYX{ufb&9C-|*WJyA1$eָ3IO|SsqrTᓙ>Zf!gKAm-ZLpP*wԔzf` UW) AyşpH[L?l&Gj2H2 44XѢ엚Oa+6Ӳϒ]&X| :cB2և"vՕ SQ bP݅1KS p6pccO/ӫrvl?OM``ꞔq5[$_5>XM1N_0+[d80!`ZT0ѱſ.WԸ%Pξ3;ySMj2Og'mX-焚O1*u ~@J9xb  bS \_l#E&HdJD(OzΒٕFZJt ~A63*˭R*GL eiT:Kw7p6<#=Qy@0$.1I.IbБʓͻ*x}DǙ,+w3.^+wLtT9l)+Ȟ4"aABRp/*$*Tjխs( vkPώJ鱄 /=;%Y'xM4\ZIPr Iɩ{.~Ed^UV>pܐjDcF3EjVL ~n4Gx/R,BGcM%-ҪsM9v )/ ,NHd5=\x^uLFh:եJG{Q*z%M/4@] .&OjR}胵dWpHA'QXvѠ8 k5>*Z}bD(I5.qmƺ/.QTL\-|}XM㠍D S5]wXMnq"oxTЉlEJnt-&Gt$~^Q=JV꺤~d+C)؆M֖*rYN^Y?TQyS3io@ǐ* I b{tE}]csBMwITAz;E$| ~ ;`[Uv.n^]Rqibv~ @+/|&jWq1Kܮ3rwį*aK# xsARb@KǕjԢbdϋYckJFuP* 0Z]~z3"Ti]T6mW^Ls9=:A;֘5."Sqjfh-+Pz_IM'xoW[HM4'UY:9']d?Jۊ {$ְm/Ӈ#}eJUlgPIHFPǍ[@SpgbɅĦ#k7K[N3wfOK/Pnp'oapMZ>MZ4M>Fq4[̸ꄂ}wFԖ9u*y&+$,Lp&:-E`sll2KroAxA*Axh4㙡}sTDi-\VTD*84@n_Crg0-<Rx1c*MCݢlNq4=VnsgԀJ؜I10%{ ыO惌- ɪ_D͜p;UTyhmH9M* RxP< ų(Њt&([)C!kF$PuDgvWǪEP5v 7~j~;g(!Hc' hB IH^A-( 0̐'j`|LUYЩ?Ul/SyTn)* 4=pSTgd!ж iꋶulC5zZ9jO.x807VqU`[Ow16Ϝn*FyPRw y̟Fl T{jS&3BZi EčAz2J;-ܒ%~ km9 ) 9AxCH)fzXJ8LcPEcs7BWZ1JyJ4A!z'4MU P)*&"iK` u ct;e{غ In>DL#8Jm1޻J Y ZwVCE'11b+N`$sTu#m>(&83u$ (9O{._ѐ~c+$m1WJ'bBP8q̫̪2-W:b['fVUB'Cz:Z!Xد'2$%ϯ)+]D|I4jӌb e;vX[$ayw @^(`G˲  Q0+HND'3myr#=ümܬ6 uBD V$~1Vn[wәWzk,Z{pu`eU X' 'OJ F̊u ׽jM8f)'xt6D*V'gNOZi--/%{yC Td5 hl*cri؜C!\TY<%'")BAa/7:EbLk+SV/{{/Kۯ!1ő( yY Jtx6K5U8u9t>8+Pk֩zle=bXymu&p)gV8INpb"uG< !̨06"BpÉ.0[;$v-n wNvT2[o:CɲY﷝K[յ7e LLQ(6׿Ags줅*Ɉީ|Q752gz M}|?\n89n+DL6nUvQ=3zhZS6] `#o>RMF$Q Hñ | pj"kze Eې=2ncPc~ڽQj`?wBGZ*4T HD7'`Aj<Ƈ5{YyddO_Zɔ8Y]6:u>T\Z(j7[N9 g:2^2*Yz R:!ࣘȻGPQ ,:uFj%{r'dSbɉzQ }}Q*_,IzZ9^o#woRWC,KJ-uЕzkӍ֝ҳfA呄3 ȃg6& hٗQL)w  $z'v/V"V'l5FX^^~8Lznf+ +daKn&QĽ]V3;gm/dȎ/*B[조*5SLe2~8jаcK/tjlZuTżw=@tf4FUp㡁s1A0`:,gꌑy };8"H9­D )s7v붫?ퟠ}k|NwaaPD쮅]AI7ul^wd_09{~ޕ.ڏ#7ie{} F*'JESWNiB9\,dl;bURJvn a:8yh@KmU/xW \=i-As't^~/4 2bsa7gw4 c'bV{}9DJ1x6T9rT4 ߒRˮ-r9ŵsFTj֥<[Wz%(~'!ê</YD 91 q(?#oGZ!v@Ms\޷>txf*@MN|֑jHFSTaPPΓߎ 6Q府.;É>w?ۂч][s=t𭹹^'.bPc,d ss 㬏?}[(xy_|0}RD<]"AиYwG@Mtt1؆z"O.DRH@xT NCt+wX|@gtđ*iȯ0Kp)!s }EC sB0sũL#%߫meXCS7S5T/vD+&,߹g@!2>pJR44$Du8F696+j/E6]MJ _|l<zꗡ Z֌h. Pܘ94&So;|sQsbo}b!m˭UI䪉FAQ"r[/%{ID&4 (q7m$g#|}ٶpD{k}9^vL^=NzM$AtUgn8P1mG=Y HfyďeV7<%yŹqvݢW4FHH,be}s*$AʐU"(Ж iW4:m}-UMJXstτ4.~JC??J2tCoa viTЇ*"{ƟSn ll~{u=_lP՗ςm+&&u{ Lߐkx/^BPKc|R0y`,re#\HV|H2JVV\mJ[yv#ˣi,/r3QC "0FH#%9H2#Ww4lߦ7'(R$RxOkx2B* *{{z)DjN v0FFyYqzuBԒT1C|RNKFp"@d/"HfVH͆+gEEX}]kF®a}tv^ߗA %C:Ôb$o> :uIb|}AD%A ]I giv5fsF҅Ps3o="!1Y-Rjl'nxM @/D'z YW;([-N6Ho[Ek͓cXV}K9jOctc֭D)G&2b9%3-I,,b||0y|1 i11lJ.V)kxXGTy(4^9*%nj|&=H$>' sfe* 4ipfvy H ut$(V$iѯ?gɅmU3NVBC2%y?9WQzp m0V0Ntf%[^xq9x0؆-"V(=JY$&=@/zkF3X$bIj`&e 7?5 &r1),c*ICߦH[0t~xѯx(Huj$UrcEP=NӀQq ƒ fjbY0x&ZSǾ(YЃLq8?xg{-{%*$Tqp>A2-c.㾎qUKDm1>>Ao.$B'aنJrSr\u1R~Y 1 OˬMD[So^+w-k`Y\g1֞­WB- \S~,{5EVkbY*g7@pKL0<!~S)gL$b,9gX%Ϊʎ 0õ I%J&:1ib0 61ϡeA(%){ŊRlRI$vgcWE!)d9!{`_a'yZLdj7޿j3ff)tr5 frb8E P7h*  mP; Q6qbVoSxǮ,o1E]ͼq=y'U%q9xqWBrnI-8#> <ֆbmH-mAբ? 16gڝg_T0T|B'&%yG s4dZ`tU>#ҩWkbH/h )8z6GS6p`iG%y O*6 "w6slίfd^PdfZw.FI0)|RCFբ <%0,NBd3휧Nʦf-ze"tTstWl@x[ Jh{)DOBbcL1#}^kO&oG*>t3VrAF1^%UK%>Ŋ9gswv[Z@}F4 *2mIu*tn [6ݯ{&9?2AR>x(}_~򌅪J;7kZMiqf_'mi:+},P 0To5Mf; ؉\pڮog&`KzqnNRPaz8;9uƚ=!l?fp{*W l By0(q6HOBVQl(H衦0 `Ct-s$?d[vs;o^Nz‡ǭ<'vO *':Wަ .WXc]bH$ԆL| 2pi?M{MHfQ&P;`Mh Uia`z5Q+_*:k;u6l\ DBd^i~pמ^y W M7Q Yr^y~u^:.dzC^S6RAZ~K c2aFX??L>%B[AU(BNCȹ֖_^37ydm|a7P0"[IsFr;|(.}î \nԧdx+h7=%3 |(qW ~} Ոˢ)K^c:q#EďMdn_cu.4WڧTזq!䔞sulSgkJꢖ2,pv27QE~NOtf8Rq|2*XT%GDo#d~md^nIJ8I*[Q.?߷@drʈ~Mnηj{gTĨZj 7aZP&$$ӫ, 0ӹ6}%MqU/WUq >=ׇ:u~Y#lJ!uPn/yyΝCPEÁ6Q%}iYg\F&u?W fBp؍L{A(Y9_G`:~%fe2}E,<-!vKy>XR *:Un\ *=Mjw>mZ>Nr~ZLLew|VC|:fҚK[tMd͵.{J<7 >K)ӇX~*pUbxNO fއZw.NsWs׌H/ѰJ)ڌE!Vߚ4Ȟ2rVXڤn^akf-r,4$Cxzᤱiԗdžd?.ul#0W|}H5f8`0 ز@9l-sѭb7[tDyyC==ڬ)m8tPQ?A݁ɞ& 1߲MiZ&r~ӷZ^ ;=1e ̀zْPzhL>TjHپ[ f\)V 'bp ^uo݋MەÏ\FkaucU),9@ڕń@߸jրgݦMmm %9d,aȬGZJZ3HYNqe${8U+-k0p% GlvHRK[)g/^ZBqP:H6O4LԇT(iB '0. (|XE_݇yu3T m,zv3G;CqI29O!H RFwAn7KwɶPMT:xxf8vkhklWE8и`B%\r:gR]u& ærrtk;p1bƬ@?b)9NC>ZBstEb 7Z(yc=hlԓ*9,؈ݛ2,$hZ^aE8z`ؽ6ra 'C) V']2! oLfEB;F-䜳(!m"r## ]5 #e 5w}ȚY  La=ı 4@Ԇ4JNgӈ؉p[,@ [4.mdOrg(rEy D%I&Yvz@V-.\R¾اLs5 C %O0 @wESd}JV7<>j,[hxK({(qf?uJRr@ˣ*?^DMbet7|Tj;bVZ2 -IL-y @fyNp/jUhrG.Rvj0tPF솸40/) N(\tY}P+z຾wR64Ihf[e}9qP8Sס2_M{'@8{%` ڸ ]E;)hC=TwL%U>}s8,3 e%W4dU,ZV∝%L & AA8Wk *g,1C[ĕV!b>gqEy{ ecjׅI^#tq)r+33agr"IЗw||8T L6MnΔ @@S;K EIxl9AԒ6ptYWjQpNIY-Q3"#+2 9ȣl[cwY7N,JۊNH7ѭeqwՑco}s¦%tdqިmՐ@YoE0uGxa?>h ZBmk@KBoT_Eؔ2YH|"s^)AvZTAvhB&Nvqv} 8ݖgi~a6L}?7](.:.g..!S4 i0,9@k"2|jyI_VWè)$/:~NRٕ< 6Jf\IE%K<63t}c|_ Ls?0$`5u uMwy\zĠuI fǠ٘,}g'[= WlGj:(E9r1F\/Ug Y?zn+>F_!߅Wv}Zǐ/4GD0e )ox* ;O JOH- |m #W`jzpK˃,# l[+~il֊y4}ږ7^+hpb* ;({\;$*Ѡhlf%A ?IE>}[X/S]Oۑ#IֈTN$9*Çy"gA!,iCo'^io6q5lOEqۼ]C{l22x[S~Yӹ 0A^|ɸmχF0P\ l44$ًG!8>^v{,G0 lڵ&HT$ Mű-5jz^Ѝi,ڻ"\LDZס5*ɳ`nAKY~K aѼy==m6zʳR5M%r e? [Y_.iLӋ>@?JplpXd(ȆtV~vlE~RB}m*1{sfbp128;&NEm29>v7814|C< l_Z؆\zY"xxɐƥVkrJL_|d m}GrȲ QQ ˭S>\A Bp !r~8س""h\,^%jP@;] 1CFyyW< re" G#=vګ4;!oP?[CNRض]G*M3qvIkŸO=m4HKZ0mV*bwZ(j} %W X;Q2wc eyE_ѵlSg`" u Xh),ҍ[-4V5VzX÷5'՝a~/{%UU:nAShB bl<-f͉=#kcGɈ+Cbq%u919cCGsz.odVO~xDh$Z-/J k~2L]TyQ/(Fdn2dF5Ցղ85V;j%y2tzT<v;Z;zRU fXQ}/WwJ:!"ϾšpW^NeOKPyғr ?NT<'&TvG4f1 ~. 4!@p v™zXsqG#bmNZ/FpG=5O&Ee ДzFQ:[%,範ĖKvS 8G1х3q[NW?WK9yE{b9!7Ϙ2pyL/v3,4'Ķ! X0m @?PYEݶitxa E/kbDӁ4WP%E95,ni7kqKq3\CO-cznaE$Hk!>{ɹ(H"YaY{u# .?Hݝ}tL y:(JW.uj'/]D˜IqJЬer'v1_=/5AY[{xBr|;$'BLuPlw1]?E%to8}vz.+b$?|:IFۚ/VDuMm;`L U cA7}B 4|?#C.FͳchڡsjJ|M?+!!-(;b$4!~3%D@(20I@FN4 ؞9l̤hT˸;5H`Y݅cRoaCxApɎh,!BzFȓg7IeoWvWɯgsSW ̐UZh#l[j.FD$M8K˜Q+A3e.^+v"15hQtQVO.<ԜO6I=`2c}߭NzN֠%+\ wMŚڈM+Wdqg0grr!ir`Cd{36{{NRo/m R}ta? 3fcgKx[HFQF&+U ̨A,Lz~B!j]PK!>a8΅䷹AT? _pS' +p#[ `l֕z`8C~ز.M5ªW=$2.&q/%X\pUmɱ 7NA*PCpPB #S#WD#$vID/10je=Z1AYZ'pi|ք h6EJh}gZhx2 AtEr-z;;5RrǴfR^="NBt9!zEpXW4CF'.gNK#\ա㤧QiDv$[?n%4יg:[y}Cd.ĺ>W+>Y+bGg]e - U Y \"9.@:| ."8%Xс6EH<ذZ1]7[{54th/St,)⪳g8J+DkhƬSұH8nH× 4;kKU:n\=|nrPWP)`k^ >OkEBK3D Kb)yzSr-py0k|zfy x DCu paީ{[uީJ2Pm^6ym5&)R mW҇ Ǵ.~TW(joH\eXer71zJuzuҍ9jsuH S3[u^8 PN׼5$mZ܊OB۾eK+3 e8T0=To>pvP9RsKcAb{ -E|V`eaMLѝAJl}'8I=둥H~KE‚lGejU+nd ՝B0*}i GYS:|fPaK 18jvGK\m5t{*Z@mT&EJKm͐\wB}o )gR/9 fz zjdv9Mˠ@,EXkpC;' *#U ]cplZlOY?/I!5SE2J`p0<`a}y\Qcϔ'$Wڡ^˄ #T[D@ 7R$@5ہ!IGvA]QYd+cF: tH27Em`qUY׵D &U u-7Z |MQIHxI72x>Hj˂J4NBZ`&1QV}Sy8ApZdi9D?Qtɦ$@ˤoS*;]m*^xV_-ujץ8fF.D?; [g{8ߩc}}JNb'VvYls^wl8dRb {0wu6?xҍl s6DW¾VeʉKlWD[:~I%ATc侨_$OmA)ji7Laa#,\z}wЅ#r> 21iC(¸Y-J'jD։) 2\oZ=RV *U'\[`GmZrRa^%&%&FeD IYFAD8+[E3A/C.uV%3ej!8&*Hu^ˢ>,:o-tt1}FH(VUg?]LKKO8)0FAKN}{SGqEsAÆ>Hr*9X7Ԏ[ʱّ>qRYnf(}:51MAnPriXe?CvmB#j%Bm)O6n!|Zr *Ͼ}745խUԲumb=@S߳?_}cߠ(`&fc%@8&~ֶrh2,m{Dx,o3bmP fN_𔨱#?NEXw&)!*\Ŀo~VprE]5Mi'-rE?gPH—PN@ U&ɏ"\X=0K @H L%UMꡩCE["VRP\25,>2"O!X͸rnr!!51X%%)~y4ʪxg_M | %U̥{F|p/߶x SZ<vfB,Ĝ-âchǂF1ykcTpÍ__FAM,#QT'QCG5_D6j]`7|h#Xm2Ԝp+QU SaE@JV׉f!/)ۍY8Ebyw{y*_ .O'.)SIp,V[C|6$[UIT^AA4KX^B#Wom&tiu 1",BNM~ vEɷڎEp߻ɚKg].Z&Bp>XB}(ccfҐ6s.o4w1 B(Q;g:;JIOQhi||SX|I%K H6:%rM ةo,qOkvY4XnDŽc]VX|缾sqwkuq׈0lT?;bK}>.3f,c!bl,%bO$rk;A6?mAhYo}4jłxGkY+ʩ,ɡ$;[^k>B_[JIΚf|7KiP[ Dz1 #*Ogdl5;Ș[ѼaMPuHx)3@(|ޓ9oQSߌ61+G.Jԝ1}׀vx_ pQTx9{ gLT4%siN(&Y%ӞrgbGQdȦj(.CZVj6X$t&3*+8Kj%c"<@'`^;Nxe,'jTZ u.:U}v̸ꄂ̦EJH>ţ%d֩)ze:Lg5ewǕsV!uRN5ђW;l@C@Jl({Ku;jR٦pD + XΜ~wXTdĨ6/` oqO8lP҄48VL p) qr;՜޵#ěUkdbْ?EZBBGQk}%Yy{G:JExRW7x$[, ̌N6oBHt'ur@yKQ6HޝGPy>7_7P˅gy!nXf%~S*N}'1v߳lna s$K 2JgClQQNjtZ流}˻ DFL+_MdŒ_qJJCтϩkϣXQ3XicnV BCharm-1.12.0/Charm/Icons/Charm.ico000066400000000000000000000727261331066577000165060ustar00rootroot00000000000000 hf h  6  00 %*00 %.P(  @{{| ~~}vvv~~~ w??C;0eeb78146?? dc_E14T==7]a~~~nnn4""-I !K bb_A7a""B <<<_e{{{|||||{iiiUUU'##"bhelgp__jsmxpzr|tv¿ƿĿxqqc'tE\^^@@{uu܇+sE``@@}hm-qeeeシ2͟èɯ⩀ȴ²DҪ輊Oú̩ÑÖz½ηև۟}Д.,)˪Ŏʪő{ʬǠ ҉ݽܡ ՟ ̬ɬˆɤȪ ڜݫ‚ ԋ ݭ W۪ \ U L(0` %333f33333333f33333333fff333ff333333f333ff333fff333f333ff3333333f333333f333fff333ff3333f333333̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙fffffff̙fff3333333333̙fff3333333333333̙f̙f̙f̙fff333333f3fff3333333f333̙ffffff333333333f̙fff33333f33333333̙̙ff̙̙f̙fff3fff33333̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙fff̙̙̙̙̙̙̙̙̙̙̙̙̙̙ff̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙ff̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙ff3̙f3f3f3̙ff33̙̙̙̙̙̙̙̙ff̙3f3̙fff33̙̙̙̙̙̙̙̙fff̙ffffffffffff̙̙f̙̙̙̙̙f̙̙̙̙̙f̙̙̙̙̙̙ff̙f̙̙̙̙̙̙̙̙̙̙̙̙̙̙̙$D@ UU_Charm-1.12.0/Charm/Icons/Charm.svgz000066400000000000000000004221601331066577000167140ustar00rootroot00000000000000xזX{ezN]ճ%AK/x=~RIUѴd&pq2kUsYSyQ4aV'{0^zeSGw޿zol6~rƷ%A_'ram~rJkm1|a2_~/C.S_4}Q!/zY=#o(h7z},˂)! @0 ]գk( .mZ2?&2^vfieh>2zm7{%/Nk/ fe-d/ԝl,Y8JQ9Yy0 MY~y7gCF?Hfpu4wO!(pCDR`6luz}{4hτ~{2 J*ڬ!fݒ~{SC] }Wݮaw _nIs^ާ~z( T|8?~Nʹy {V>?C:F/\._Uj]Ƭ,GOXF~fS2MR|}Cw1Fͯlh57+W4o3uCo,{ؓ"!x+:z,?8^xߛQ6شJ9[S~w ][j!OM덄Ow5ـ~b~$=0Xz`Vz?~_.MoAgïic_amcȯHH~ds$HzoCЯ1Ce@1&d@wPjs='k? `0eO5Q8F}MvJ!T㯿8}1> I{7}}((_``?G#GqCa0|B f~zhaA ‰CCCzwz@'s~6V^m_L&^b "xq#1߾GDQ Gk4DШC||}h&^ao{_"*ah>ھ0 >>)KT Gk5 z>N_oAӘ5ڞÛ( 19u)9a9:DžYЅYV׾ַ*oοٷ9_X|`,YMw*pmŲlrKͱΐ ]/]5{E8r86% {WY<Ѿ bnImg$LUw{!YBh[|U2VH{EqɎ:(s! jb h&*_ )η<2)mB.{A3lnja$i1խa5Fn;( -$. -bөϓ>;%4.fv[(pǛi3)Ö'lX֡ )3J(%qD9!i+0Vt1n;F`VeUO'H&QPvXSAj,K1 KL0g36*ydBhi'I⩤r4xfFC - i˥) ^'7$灆(eex:Ru0u:N z\3p Qp5ݓTlΠo[tfA'9cA,xhqH{7pvZbP:>#S)qC:8i֐^%+ky9f^m[\<>ˡܴZ^yO+{IO(uӨ߻c,YMK 0ŕCɌ(7OSOW BȂ C,Yy*2 K-kZW\XzM#K~H(͢!uWMj݋6{$trsHkߐ^8ku}B%%-qnAq=h0{q ꍌJn {>yȽ+^ĝn#.\haqSO-{rZewqLSk(}е")xs7hLe 0~]"*VaDJԪMFKYW,:'J#>׶5eItZw}YEbvm >Ƈu0ݢqFqP*$a NcKÉoLs01=%"e6IӃu[E`mz.ѼPG9r-KB=pi(>C-CrEh/üBQrGiz{p#_U%!.pCt!!,k (uIo#xZf8 zXܳ8QǥP*-KSW+a-hTꞦKf{CY^&Lf>WFؘd/u14†Drز^Af]ĨsHUK9&}hl6^&Kun}c6@ Oq9&).m7'ovA[8w](q4l VuK aZ&$^6γw-}sgPۑΦvRNpnhA@X)DpbNx1! 88cKy/F]f?Pm/32"WХ)1NjA57pJ3)ƞXpnkFȂf|_]l&OV4#?lZ$TJd9=b)%)dtiaEphޤBF!᜕MΊcYw|Qn. H-(rGha|Y{lf,"MbA"tδ>Pr.IzI lQN}/f{4DأpE.뷇Z%-43[J_JA9F'#!ᝲ]y;aFכA^qLDGO 1̔6N78iC<B(3٘gw\2 x"alrpQp 7j"iaq+z&sp_T9G,ʲ1ͥ?S'7 RPv]q\bq) ,+2sPYr GRe8bU,"ݢZ7ceJ::5 CnҐMnZ*cP:L1S5ꡫym /@0&,rk[3j;UǡQC*+Ms'My'Q苽 8媇NQLeb}HΑOG3Z:C}I(\ .cE|Q}݆xV.)@3(^''m=+B9]L@MWdw#Z$[ s[`&q~eUZrO%21[E'Og" bXsqXeuZ F Hv ee@Ju9'Ā [qL%#F唒m$ըsijK=f]kAӬIA zM]t&MH $9?v&Q|+4?(S.0$jUdAsJs9ZżH08^aO8y̤S suxdJT:gXQgԟQdƤV8Jaj#zq@ M><<@t#Y0dlX6E>!V\ 8h$<)2n](0_VJ7Qw~dZ$ONf GeųB!MuЪL.% G͗f+! +&_nGeyfu@0saVӆn /8-ta!M¥#n4Q&{q75`EbH*;1ٳ8b{Xa8E1:^t3OQv{p6mr4.QzEeR*ѽVR<%:զ:@ZH25>s$fc+*]g0sO RSL2}lܱ̊R1?g3\?iDٚWbqυKIBWȷqs" &8bI,㰸 Ҹf^3xtLZݚvށ-Ca3vѬC{5jGO 2%JF)`Fscs)Òi*9qDlA=tj{w(oDj㹬`)%<8bh֔OGPEцK]7҂CN5i\zef% bE'lUuYŐL`lel/ʽJ۱{&N([Ԍ's9z`4f0RcA`w[?\bgNhL8eX-ڼn4&KOn|y%47$ź0YU\ IJ\4EuW"\B4%-n.ξn|h5ӱnfA6F@OiNJOc)hkf(! A seL2$&|LH^J/@&ŭ4Bo]Tc ؄9[z]Y';ݝvz, t =y9>2f67w&<<=/.J>lݯMG?$T̐Ub:6TC|;0D\D2̊#19SjˠA*18@zIzEݰk'~?Oi$.f7ϧkNOMa@6B_ чQB=b4!{@Z -QOYM'X&0~uK.vU kzckB< SӆQz+Ttv.@X^\iL*xê9iz$X Z)zn҉%hVFv\iɡD?x5\ ^Yg̥彔ғH e<<$ok˴ȁ ) R!eP"BEkaQ-"'g EUwê"$'w9qa Z awɔ2FyI٠z Q )xph~[G2`DNX#/tQz 3@ Sq/nא'9Lƅ n3Nվߺ~;W4*mٍ(EQdz|eL*ܦ5">ƽ5$ԋ曱Ȣxc+6#[U֒oBDݘr.賛 tAzn ڄ(C8h9ԣ,Osf|A| 'pqr]qR: P^K>ЦS } Z0y42H-^Ge6p嵖87J&<]V,9нP"H>4bI6O qjL 8b+[+%"{I1o ZgRX-ƽclo09}g"^#cn2$&D7#&h-< xQn#}UKkPd̬ZWӂ]F G84kiu0 =n%t\'yJ(|۲VwΑ{&%cIܿ٥! yn}G_;^n(f3C #RޓרƲV-Z!ڄK9?ZO6e0n%N&:S;݉L3|oyLi lO`_8P)/[?Vק.@+kHRDx4ʹ7fI"|!j}qJ({Ggu8 jLb_M'*<MhZs ~-C<#hB?P"|PgC%զ3}x%݆&RQa?[8w_܋3mw9V_@ऩB+e.a~?i1+_Տzms4ѝC`6G-cwA{^TS{q Oyi͹0.ʱx) \F8@!E^m}MtJ8-m6ket[H%ą @?[Vj禟!k-4+ޫwr8K6 VA]Ԫ3U~3=O)'x>s5^êO yL쩸F;*.Y{рe Qؓ>?c9u\ƍ -MhX=S@~eNYAr(ٖ%)@˛oRR5Q%t8\1HO.fOy;dgT5mY$WD瑫i*S-}-ЄaM6tb 4JuV L{r B1?>gۜGAڝqW4蒜\#r`$k5Vnm= KOǢ~\[l{7ED+Y:p(<ݵ"%=?;M5;vLb;mMY us#ZsyM lAȚ!شOB42Ǝeܓ2BMi!{D]O]QY;+8 {k{)M=_Rf}sj'"" C%8-nBWdaFz&** B 3o62t T]^s>'XP4ȲfV5[ K':B.ַ2v[\ |B@a&oݤ?L(GOJȥ!kF;:v=7Axw|Y,Z  J?tjaAlW8t %255hq @Cܺ*Z:v^˜kEҔLG4:3ecV Hx+.p3qb-4 !m6P#JGޤ:3[8 I,QTi?T>z 2NEj`՟懅b{?UhI7Sުq|s SSdp>6w3wr ɾ ]_ <}KyoNiEGTed?A\+i2 ;GⷃBw@zΗO?JD@$vFo኿dwX5Uޟ9ܷiڏ2[h>ަ_y GsI !4i  Q' t9)sb: aDsK2BCj f4Q3y Y84;--t`'ߚT,HE˘C:KZEXvHRqN}-P s} Hq/Ξs=~HJ6HdCߋ˽ Ps^*ל]b)Nٗ|VnY}5&/L;v=x#&Q-:pM]P0*x"}q;RhەqF4{i*=}~E.*͙ID*dȵOGKPt٦N䨢:C/SUlUbȠ5uo;?^vfd OS֯fN1;y8 "ӛ' m|^ȽM⣽z) yyFv˒a7qH{C-7HR ]߮Y!|iw;Ŀ5Fr8c[l[5afaGHYGOק@EQJXY $3uѹK^+\uIpjVHq,A;Ms~T~}D`sGЂ)K{ ,WH:8H]I\U=ew@sLybJt])mא_+|}`Ȱ]f /͢GJoIpcRZ ҶtGQz_.Drλ;ܫVb @29aXwQB_UaE4]$4wȃBCoMh՗,w^X+pFbf(SK^afB^۠i|2p[å9WI3>KBI5:qw{_Wd7,a^ 2abR`@n2X94u;f OBE6S`D$>$H_@㭐ߌ8Bia*g(: `r,1i͂N(3:+Nq~e<-+k Rpa0ٺ&Cq3/l'=NS@,s?CgF㈛ىp2K|R5nL~3Eei}z]& %-yh>FUl%T<,Ae3xE\uߦs0+C5a(F9 ^/97|auU
  • ˞$Sc}CmrG?zfgu>I'rI.rSe)wNu)мmoGu$LĎN /λX MuZ &%ЛYx`] $ɥȨhe,$Lg!]@J X%b?A8x=u:ϊO$X 82wܑɭEyn#?L:Cpgj~L1`E(e(ͨQ+KB }9>Wnm{Wז̙v'3zN>b|GCX̞[1{\S)^HbjAq$Xg>q% ":yQ7Lz$!xܔo;CUb`|q>s OrfpRP'[b2.o `r5km8?(Ǔ{T_i9}?-e(e~{p 1O_SGGnH{46~ vV bygd98gT?[YV诐x5!{oHޫɓ"7A}Cs[#ʹmR'K*Gş*0ƔS㦁u;I%&>jRzvY1B֔_UpPby|/1C@w6'%4éi17\dg&ěj|RŸ*F<4 =˧Sv ;|rZ/#Ww!k`6~0B/a"١3 yotTNld0!>s>wyhU(uV Ҫd.u6c;MizfI#yGT5Fϲ}5Hu`ǫ#$Pj2-m# # iʽ#@g}.&nʵ=Vcέ3y*EW[滩{mW\؊@*Z E[zw#fiSsZ _K+IQAq>EJW)|pN8޹n?ʸ {ڲ9@I~t궁祾w;Vϐ}]Aʜ*O a!8nM9P9E 6j:6J.P^[mA08[7̚GȞX"P",)TdWӏc\Rbb2ȰLKch$S+f$%C' U O["c!y% 84ZBr~jf˓"#6fI-ғ"^٧"I6802_k}@~);لHB;3uZ JܲȽTY_sj;V#bEү-ڌ[~>c L0n`9kz#]l3OQ 0* ,5K1aK&cQh,Xq .M_u}ΤSJf;,t;Б*+C0Ej87T7]i`qOCv5tPpnC<[Rl]o?ŔT+(n72.80[H4MOC0oﰷI$$RL Aw$ L(1b~ٷK)Va,ՆJ{K$ د㮫 Ìk+kzoBc^E[z(SNE&&g$[:2WM?ϬqىiXXnAgs:t̿ z)0)BX7")|"|-m6|톮*PM:vׄb#X!F!˂߬K#;HIok1ڻ02/'È^ҿh݀'xթ> Uj29pDbBr_M{SKģʒt`;WheR<ė ? o˴ 7PcW2˲= #`b@W6^VNG_h:\#fvlg 4'yH(hB/_!q%6eǣ IoY} 6]>J_ XI *'7|GFcU.AA.|tډj`LBH' *nJ[&4LMgpT9ޘfngƁ>@?cwC;Ip,$g hp< `Nz8q nDo8v6I<;<rҜ!FƉi$QjbNPG[fm~*Ûz?uoa|j&@WtW@բWpiķ%9zne?PD`J\$:xu?T([R!'-d| FY ƛqjW[GOe>K5(oCpCCf+3h+IRR%f0-7+HMqvd|S3E966 xp9WUSO+"j2[W1ܑ'\DsSRl{Pp7;7xW"* 2m*{@,- l IaqjfG P4aش_va[G|3k((Icz楷cN$|8ܩ*zw"8ؔ<_^)-hRT(bx%ہR:G+mcMXqxSc%1:TkH싁MB+ޔOݳ%)/f}RCĞE:OnUT4=(Y'fT{/1q8~Im,Ն($hSMiv=`8/oc4;qLo1KoK[id^' !6 OI<|FQa U9ȃU}a;fq{`\[qhg,tA{gݍ;KSF|0ږIS./ޛrkM0c&M2;1S, j{P5h?qÑUnl.'+k \\̗n'oXknB?*oE7l` 9\K+C4$9J٧L0tfG$P4RdŃUź.ԩPc Pe%\^dXHM51jg%#[ *TQ`MTYo"ξfs^69s@'m5*Vj'3oX c4 pwtfk~b0!VP`ryj#ETw9Wfi(b1]{eXr<`&ug3>d5Npl?EkKi }$~Y6}2GJ"@Ck!dIIܙ<ǘ@(~|i8vKYbkGcD>PcFQ KB$tȊ;BM,ZD07mϖR>慴>;5Kަ@푆\e7[qOc mV6 9x 5yv?2]vlJѩZx<U Yc {6ҿ g)M() l5,g&n2רv5]=S ޳H zZ*(8^Ab98vI,m>5pj*:C;3'5L ͌ݱo9d|iMSV.4 O C*!}f2~ҽxQU$XG U"Oi^Tߋk"qIcj_;:ӶJq,!%zӃ+GH֟\ާӰl+`n|lJ.Nd!$b9PdU#y}w_<ҟP3܏VUkf'FXǟ3b r>,bY4H, mw +W@eʇzDAX=P@BZwB(y!WWl~v*FNڶ' /n?rEfZt|L7#D5ʊ_1ɐo#8Qo0 WVFL)JB"+VgkG4H5D/:A3_8U8-MQx*O,-bF"vٟ g'V9 |,*>&~g[FKaIPX vbq;D4+`酶e14lUMXŸ|ll uK\cCk\~CkiB/GZ[ FաȎ]4O;Sbv jb(BjsGn vDZ KR?79iYSZo'ӼWƠaK:|Α 2~<#݉(Qp\ˮ >7GՋz2ْ Co54ǃL' ݯm4̾ӨY(3^rp2ǝxeeOv8O#Oqm?pE:hZhYF:rMj~"=mFdx,7:?ʻZC%PTkฃ']]Os %n f ,dLւ=&d"~-#ێc ȕ*K8TBO_%n}܈5e $s8_WѐZ OG8i/e=2Yg &r+$5eؚfc{5wKJu卌TdNKkFH߾c,mg|)YV($O9r@i{ve#jlIPhz{)zL?BETGȅ_bß84wxQPG[2[}VehtT5gE^ ŴDIBB+:.]ư/C4G HY-XT2,rm;(?bM',r"w_Ȳ @iDy x68.}c~Q# Ht m^m~b4LJnGMjg' |p3c9ej{yڂ Q}vfb.y>1t",nV,]rt)^+և ps/ቝOd]ͤMTDrBW8) wn~Rq8E+z`m'{{>bFˏ(h+J1)? s0bYwY=j /@a)InSG ]>@&bFdp+x)~d{jLL.,W]lHDĪ zӒj35|yJgcyɕ7 Ew`P1gm/*IvXHcJTCɨ~`C &a,Wq)ʀ.>}!m^.UW3EH6M*/`IU`ģ΂eM/pX7ch:fk{+ۇq5GCڕ?ڤ3>ԥqmſ=ѹţ:2FՈs7OwW<Q aQ/YB <6f]'GV?04QeyZ.v6CA*Fe(~f?.XJ[LagaFronP\>ևadX;_i ; ^]\ojQ;.3`)*;3JhEMi2S꣐e6!!oYs:cMeرvWa| 3Su/ڮ! ctHtm*EKH9s;. i$F]jhQ~v/6KqQ1)Xò1wb`Sޝ9 dɷB+W!0v{NDM{e[ߡxbe yU&WHe: LϽc#Wz K0a1sG3GGNOyn{<ݚFNTyy끠I? J#zAhsB/I+ J]$}ٕw ;CL 9^5 C^\L" x ZGqNeWNKL(G`n"+ d!&79iLDUOwwJ.{Ճ\S.}KDoBNVØ@=W1ya-]h6 oJ0PZ'2;| pHn/#YژL f/#*fLF(R撎?`]CVeKJ܏Sm_+ 4H9k{z$NjµTd˼aVee@񸶳3i&tW鮄7|M ߖKz5rV'pZhEA0|dM-)%`d6n⑟.z%ݻ-FZヸ[ rϢ '^Y6Q|B"ʻ R.Kfl4E*[g0!JQH 7b02^++sQΙZ"Q &YĿJDր/.SjQ|Gy XeE ~/6Q_$er{$w9 bqg? chTo5i\?b |y^w\xe Z$t`.,5z=0ɞE`P #8WƁZġy{+ zW1VY% EgՐ ^kuY{~YAh𼤣cZEѰ=.W0'?jB@s׊+pnۦ:?[IyAFsY52] RŨ<=xc ?0e؜Xnb̖~z%ɰůIObq >>w"T Lh+"-$vw5#3RĔDnz$-30H[grʭ2H%;#Zd5LY.'."3N r)>D>tGR-5"D,>[&ΫYLݙH޻z}]BnFAz(Wh4NzKGF1ʴHNDw}Hcqu@{VƦM輻>MʕoVj|D^Tb pPW Ӕ&E)C C%]jo/kEM-خ/%'e0wI>.V==5GίzP1pd57h~-p2ey;> d9JZ^н Lbx՜}eqʰp# !Ns;K=u lL7j_B(bbKQ)q:S Q8t ׂ=-C<5+`$MYH[k@CSB"?GmMv)jvfq ?TD/@x@U [|]ͻ ΄ҚL `ArxxEڂ7,`]nGOm9PA`N5G B3˼QfeCF@pbK.C/}!8]8Ek@ۀ=t6{yG,ymn232+ rk\x^ޔIU(Cwk~@ ^ȳڐd)\U.gb"3RO#}GF/U.΀ MTxU㽙l,v\YR`k?Q28$.Lk#8H~6qv-th{]P_Kլmc PCԅo÷ `pp/|8YNZHSAR?5cEE>¼ud)a*h6"}Ws$G6ۤoJ*~cP͖WE-)w34->e;x^t1~n$VbV Wq\hM#~}yfR[*^B^eA!_]'!S>=Y I b6)4"jӔJl+4bĂDQw@ȕD6c" qe^.~hhpSUyV7rīM,쀞w2Am"nLgJEj wlok6h@Ai/>1PpuQ|Ev]@~|3GWA${RL2W]'5A#QLm m2U?"uU(h ]'g$UD.\deL|T3Ӡ]Z8 s*|QWGhY#h0TƂy2y4(;"Jaw 1_#]d }~\gFkf~ء!*"I_Sf_?o2=n3DMDX똠o~+4m\s oJ.qڃ]ÒNKq|Eũ:Uii_,;wbe!/L.|Hں+:{mzƂڒV }( =_𿾘xpVrf)44-uДz*6Yfk+ED^(of/ZLI~IpRVi+udIHyV0QĹ%b7yȄdKN @Ц@cP wʰzp]0fHO% 5&;$bre?\+ԗhֻS>:J qNůž~Q[x?irXQўʨ)y%9-b[`OV'AGM[%<2R쑧 R NWK+UJ;ġrybՀxSJY$qq{ϼ0aE[! ⰬSzإs*a֊L7P^<-R#Y 33;UUB; ]7MQ +}?OI> g {_],dHC ]kᅋh7?G͋M~eah6|!BzIA! 䯰&D}FRzݸv ~g5y\wh~EKdʶ+h 7b :X[lУJܞG3{DMRN;N2eùmEI! x'0jVMA,B0?Kw̖ *e$Exzܪ"AX#}nU0ua1וnOu*}'g2e2wuBĝsp{4ޡuq0C`7' CEQ`#ȏA=PgŸ V]x1 SD> hw oK{QXr;ٱ%3|VܒɿQ1b>~$jm8plwEESϲWXHp*MdВh8m"|%eD ь߬.Y^C t_M$~)1'LinT˨ghE!fh^dEenhą4߮8?YbXX9vOzPk\|D*"gmHՊ¿ Tk_ʇ&`j|nlnv/SԴ#F(Z|:<­l̸ɔ0LN ."i?8z=_4%3Mj+BtR<&0RVA(:N9֕ Y>u~],'-YTGmh@&qX–o%a_%t?TRM)-5ET$*5@#-rr.q:{Ĝ~VwTioxޝzUcV6?GG+ iB]B9Y̓ Ρ=H[瘍ɖO9G*rbY`Q,eRՙ$NB\Vtõu]-e*ZH:mF-,J{~88pV*@% 4$&鑒uWNVu=jƺ߬]4E=MWI aW\b?_U r_n ȍ!k`'-ߗ4>mcc2n6rk2'&ᬬk2g!3+'WHULxáe"y^+;lMp" ,g0n'p('*E`>0I]0uGp8䬸T,Ñ`߷|2\)?/{g&6V<ytoyUOpΛjOuT;XjZ91)|nx?3:bjn7ϭ]~N ffI]>/{!Yн-Dv0w[>衽Q0b qW=O-"/BseKSĿ=y^?2z*ItbAUɸ˔^:LY:_2I3Cn[tR;!ah 1^ڞEj_F 2>z◁G豭],cxyCYzZ3%|׏aI\Sx6# Js)2"щ$ٚ N [/PChSKn/6EzZ*ylZVKzv{}|]$0Pz`n^"!I`{1:f8q#peZGYknʹB .$%D#f:N6YD/:{Zgu1hnuVDo .a.QJ 2E)K%"81TPGk/vbeEVA#ȼnьԵa2}#QaMaC!.[>$1ZDf@e+/ݽҿ"S~cŜQ ѱBpQ$s\H%V=ŕ  ڣ0C `͝xի\sBYzmbSo_`1%y)%rgze&L|dn~ H[Fqp: MDϟ {Wb.sU #lB.Kc67:XHl#[0TeA:i#D7.bY8J[RVnЈ&gf 7< |Y '1!FܡH5+1Ҩq p?$L\ ߹8Xx=V/E uF,K)jWx}+ohAOz-N56abזu9n+TwB"/doI%ɧ2Aw rc?Yޛ5yT}8S`mnG1budw%4O1"sMC[Jd |^ԏ<=AO/^`wC'bqNC^ r }MUhjoQk%FKgE T0΋csj5%&mJj0q!Dɩ#SF6$ڭYJiK ԪE XVHmM|U /r\g_9p䍭Q7O1YuMEQUG%&) ^.ہ?@Cr( >%~65& !lB6e\AG~6Ow$&\R*C_g2Xhy+$t (sz9y ƮH,Q%/C?+d'ϗígyN+'pfw}ɮk**tg*k|c;]lU &QB}+{$HƾpmBPmJSNdk*jGr&ln@6a&e|zې6"`&`sN@+5=WP;b'Oz! ؤ-ƜUW+d4Q*GdZ3U5݋D^hÎ 3AQlA*h wGbS"eY{ xc8a#4(&+,;rY]wdɐIqդwZAj ^9aONO-ypi4JubM0N1zIdaPF:sqH9H/ˌJWZN_KCChC*^gNR!n_tZM< ;)bo1gut^W/fv?q;\L!3+Uۙ0 j Hɚqr!#v2Šd5*k]Rbfn.tuyK b@cTՍL€WlaRw*Ŕs)ToߝQpj!\/+92"E ]F9Pq62+m Zgr}孞r2 I?Cf' "5`:L2+Nj7wqE¶q04ݮܩ,>㇤$Dw*"VBK!>bRw鑒ET@~f >6zn:-If$t*!G.&VXf16AܗoD] /V6Z>9_vSJBKDZ>4Y(6KCwGgc~dLh=|ޥ_niÒpH*(r9 ̇쪃da] zot_1 U'+|憘A;G=G &J /8teamg56}S ~wWrЃsRkZ"TC;pdȟN MC+t&'0BM+o e,e1sG2="JXhʼnزo>(k{0m,/QZ<_P4м۟}U]PQaܞ6rP3ɗN$o[vnCw;5x!6S>(m+b&l *;q1ZxϾ N"KjV^KUEG<GqZ ?p.M|_ K]ޙf 1EerfL=@9(? 2Xq=jK4YԧFæDz/ =n1tQL}&Z#}Ձfŵ(U/߻Ij0]]7ӌP$v#\:O$\1kRjndYyAKuw'R^nbed3igb?R[̴ s{޺HҦfI` b+"nN݊i @ zF3Ӗ7))缈*aC482**t%3B7a͗Ia8_5_}aؿkuJHó&uٿo+lZظa?]h~ |xԽ,q%ӈ{?l%_t// Uͅ>ZώjS'ˁ^uKx0wt쏻ڤ1{59ӛ/]8#iF|7GH(c*\aVqPfn4FUO PmYA7''3DAr/;hd_8Aި>Me5]R\ט>k.:d8i+<|6]DD=T9yu+UmL#xOH),K>Y֧b_,Bk=]-^ /^3Yo,Vj򢗦i1{r}CަYURjshp\O>lkG95]_CzVF d>YuVW@kYij(&3[.E',T4׭kMX]_id߇}⠔*xTBcn*\fT^+qz)}-?闣˃b~uEbLe#4Irr`<@27Z6oIDb|4x7KJK\PF}+ 9*î j넗Sol^G-Kυ\Drږmpsn*/w"r7|> })5^˱3('}#\|*:*)nBۊl.4׫Ȝ༈K8}ƾܸNI?JޠG")D8z/ӏ^r'(;Ԡ"*@N2c/Yz6ef 10p ]AmbF&H (Ų~Iḿ6%vE5IR>^9#Mx~%lt%?L:9NG?@0u42yQT(젘x,UN)$giťVVn%-YJ!ZxxJ $_}"zYg|OX!z ;^NFO=lMʊc,-Z[Py)%~{k!WJe peg2 m C;%^Xectp_CjZJqt01yq)G. C>T́wТ#f4uäuYY]?qTCo'yQ}nG H[?QCFCccOv 8NӺڏV=ᘃam&2ӸL0#Wr r|TCٮ[ k+3Y?ĝՃ"?q\kRЦr"˯c˽h;ar?BT|U꜋9evKgxķ/f]=B$RZ6/\Dž^ 1WI4cҧ2J)M…3@y)HŹ$u4CAâLyJBP~!$Z @͐#gɭ%-ٰ! k޷0ϸF="B!vbv0ͤhߺ%gDGw @fNwQ:^\ZVpi!mWO46t|(:690LFQKZmOuӋ!@0~: \|丗0k&Ջ($)`%Q,Zh2ZREw@U~?U5PrzB¢;Cs MaQ /rҺ|fѢ >y9 v8r)YQi|Eni~L %ko-荩!kt Ev-ۗf"!Elv/2iwfzZ5dUW3jj{E" F񉐂;P׳:^Z%OnȯL4} wmﱡpGqsX6R͘yTcos6XeLzyqОvq{^Idh@5֐$~ O#g0 ׵]lgxVL~ cZ S>u;ǜHi-凓_= gjո_'['s˩#_7<@2z+͙,9v1iV<-%$HyJ>Ĩ[}[#\P/y/#" 78UYexM+1uXύ60;cѮx7wvu ZJBq91~Ռщ3V^噺 _bf&W"{SiNvr_k 鞴<`OU^Ц@\P\!9;z5zz`Zs#p1 ΊEY|J7Ώͪ>W ޿yﲽj_iJ]co[s~SqbkمZ" >|Cb.^}6jZEu~4,ۓCptd/a 7aݖ h5E w{c?aK43{ĵ>F8|VvX-YߐhH^b1J5?(bJu8RU,lmq_-Bٿ?r9bB*Dc3Rty+o%MņjӇ0@ ?,5n@hx+}ms&ѽ iFw $ׇ:JSHV~l+SMIX>s7φfTOY%{eʡ&lWEˏ܊ۙӑJ*R]Q=]'#ZDjb?nY:MRH7@V| _a0GWX+<&aewCΊC"jwjPpSu[k""XPX]>ZAZ]',C>jۆ` $ #BƁ m?(խD"ioYv"]^c"#@ jq͉ S,v$u"txK!j_estq:HZKڻ_{2OVZlNz -@[.hO]ުIQ{ɗXu3Wɨ`M| h>P`g-UBתbկ729[#*"zNϕ/ |q uzN~}ZXɸjT"8\gk:-Fq{Or H+gF g[uɤQ$EOw]Wgt2¹(зmXb!vTt2˖ZζQj@aӰPW\ķֺ+%LaZGKs>e Zf@|+ςmxſW Bf;U0Dzi'+=qfrm߯>F+eе1G/+Wbۉ%9g|UċmF-Ϳ6w7Obb&Bs?5Dk^:,FEgL*q׆'pG+4uDCF W\&8=j1ݧ&V&::%`` k$]";e2s4EB:gR<{1GG2Wr6ؾfv $mj;D qW^<}xPKN I@V 113l1Y&8f?zދ! y20(H?\߽@S\o 7& E67X_iH)Wiged\c'zv!ȊٗC Jnig=s4JmL}zptsp~2j+]Ae?pȲl܅ GR.J'8\5g^  2^s0oL(Abxu۞ʦ&_Lj$A/CZs+"DPC ט^dkk}F I\$ncӲϮ P\ Teԉ EU˾3_CªIl*lOox`7=cl Nfu&qHWwV٘?Îr엟̘bktweJ|^ď/=SdgS)%#F Q=pNc[ve0t<\y9R9FfZ (IifHȯ<0ՓՃc`O)1#[[ǃ##;-t F d1M ';#IANbS])V@.TϰV6wL\<Æ}|ސ!x3*d%MXyȱtwotg=4Q"/3$HzhsT}Ԯ)ZzC]X-zW$9}EoXObnB!wb@p1a/!] Dh;גuU/aK~, ;!9qzwLP{>Oɥ,$&^ZtzʯGCeAUSjeFFx4 f%[1"&EWq[ 5583aǹ/RVv`AqCw]h.~UFH {{Mآ{P@ד : \;o:$5^zá#zgY8d٧ #]лՐ06'uum/3 ]-ɕ(m]dI A7"vO7 kJ.$)~ 5olsYrC”SmkHV9xUa4F՜I_8,hua@;`(ͣwa/z)JtK*v̂HD4sYeK/8Դqa4<*bЧzr6iEGq%k#:i|cg݋GaX8#ĿA Ԍb|TlߐGP[ ՑT1Jmt~I&0GfF wHEW2GbCtYBj.ڴhT Ѵѭfk[ {#`ϞH|p͂ϖf`+j\ ˊVX/|(0p/_I<$\_YB٫\BJI"~92+B=WmFJaUځQ$$,=,+BW8 k-CʮKʥC)ʻ \̗j.Kr_OHXq=>񒋠Hra#El~oDSsQ< phDyժ5xR 7^d璩5P_2)q6Ȍ$Va0I8Jfv6?`b3D20R,^heğm9Txz,QkCVSkF>voCm1^za1ymzQV8 E F/zJ6pfT,^;kenCVi"(E6R a^C@Z ,&+lvn, %~yfZ5&y{cò"+Ý>j:DD:}] cؚ52B2*@ 4QrZ) w$*^lB3E 1u[8WJb(!Z_,;.UMP\#j(}Z Jەp@ݤ>Ps[(n 9Xb*Yh- nf2S E+]pJ~H W X H* ˌ:]f[5|$k8X\M[i.E?جѷY}[@DGa%[ܑڄ;b~nox:y*LTK2)ǚX6,ab 2j5sPע?nHR;+z[ 18}._䫦b-tSu+%Uմ=ߎioB9' S`dۄ #ѿ9bF%:͋I e(OnZF(N/|u!]Hoy t^1Ty2>Of5:Sze*#ө?i~rSy)P(2Rjxt{U rǦ? }:?7Xk3!lF'S '!/XYsj0"]c=HL4l>!!,Ɔ#+ɲZI2ZC!e¨žkG<9{^7^{'l&3+_գ(|1bcIJ(FOy>(EXcXq.Eb[Vq*H<)XNhT 4IgϢaJ&'gj9Q4<%Db5T9* O}i (9_tB=R g헫+eYdhh%̌%K& Y=1=mOX*iSV1suaUe-iu]p#$fSyzԅe_U$OrF9!P ;Eͦ)#Ar TA|Ӕo=Ade9FFCL5nָq!%6ďGWp5Ay6;"tgH6tY$iܝofuySԘ{v3 Zy#]h^<ww5KuawIk Ӥ PD-}Wq pXTAF-x)?RfߔȱWKXNnY15 Om/mfS>k@ѯ(B]lqtZϪAo A\_L7k{=̌r"gYUNr LZr]: GX׵?9L#ofҦ]]FvӉߗba2eN&Pz"L wbw8N4O?)-x[" 4(-׼ V]5~w捔V:z;;,85Uƿ8iv(ƥİ Ί?ᙝ}0ݶu!!6|cVD f觻njO)}ۺ]g*u;& 3r,;Coi(ȀmS@1#/gCǷg+xK+OɨPߤ9'9- B V3EQ:x* |f$+{!cIW6c1J0D5.1y*C>&#\%'H۾&/ /boN.VW$cLQ7jv)oiG/wu|x3>`6wءYCZds1F!'KSQ= BA`WyucD`jUX0N=Sj훓 fe( b-fJ3/? xV4D]/۰L' tM-v7 ![ l+NҎ[oƶ/ŷ0VjzrD~3w}&Θy!:Ζkm A󘅖>nF(#ev'z`#Qb>i7Ĥ^]POBZ^p+Ȉ_:pkv X}|5ަ]h0 nPNezSL ߛ _߯K] yb!YӸ'.ҾcσJOgoIc z% VIO)k2c9*rw"{=̓nu# wG:cP4o3x_vĈ a5m9.4K'RE`,V"侶R9>Ge,^/A~M"hI'zr/"k?i!'[o^(6MEO =-I\)HȾ:w%mruR/t@s+kk)f_{GveLdXӘO ढ़z'ϰHL }}¿X/L*uA$Wm9/h07-޹|SL3tE8ARh0_Y$;RDzlvrf"رrktpNj]-$li5Aȁnd< M0 @$%I(8[)/`ye.mjUGRx 9kWREQl(_-ѳi*6 ɒm-Bi䫚:^VMV9w>Ayy@8?06 5jmM%JnNۅI'9mۖ 2+HvqQcZ%VN3jxNXi;"` C؍~=Ol"2NA(8is+̉H+FI)eOM{GYYo;0 D'dm9X ](E /T4I W̩]raJsIBۏ9Ca0ZUy8W4kgq31lϟ1qCWmCTIK=p(KJ.=j牽k`,$~5%%׌AFZ^fCE9C&gem\$vzw7man͉lÜ3E l$@ Gd֕6fqsrX4|O>x;wo9u!W`6\fJ9 poO۾Eǎç?E|9*}n6l$_28+3. hȷb3pigR;.<:N5g0$*_<+O9K&v`@fتalFW5Y1_fu3(?"ƪ>60~ (ڽ&-C ΐfGcq,C7ZeRB5,s #-AvKULPo(|a8]HW[8`tVΛ &V}-94w= єbBNfWHJ$D`p54 dtrJ-\Z. 8m۠Қ¦0(tcyGjPaM熶/>Q: tzWWmȌ=3h [ 2pED}ŖGo)2c J>mʴal+̆\,J#v*yFAY}9B킃EKVJ17~Rr$FPI؋NE\Uz2Tg0}\|I]לx z$AA使> Ä=֑NSNSDC3, 3En]Q^РN#d7,ϧ$׸m"pJӞS{˸:1W_;Wm]9%ܱ%~* Pr)?D{q_q:,pʸ?RUe ^ 1WymmN>rپ#7<#˒x}F3J}uV¨.3SߐXCYOC*d&n2KGf[AkCJkT'pW}>eciWr75CfPDkvgg}0+߱RO; `*x,/ AUlJ#ɾt[ELb"ε[IT=n4b-T)VJXJwZ}ў')~;Md>ؗ\Xc P_ u}W>F|ǩz+@YIȬ6ܳ !oALYLE8-[g(e=Sl C rruzpU5 ۉfZN#r.5ghHwg@)ɛ 9 @W;#_M>W;9QۦuڳqcK˕К)M3VB=ແO%r 0n0ͿB}faRgM,l qGAV=\kgdQHU^e* NP;x`jTqPБ#YKC^Żwy]2Qq Oo?D,`jxPmy%\:~LXcrL) &Q,"zhA.6*wڙrRPmŌZ#0$6ok\ۼT$7iiifSqF>tzSzz@y%!=n4):Z&yoDg`_ޱɻIU«P8 ꆡt1m_6eJ,mKzVח 4cU/ ^?ed7{I69$}^ۧ=+|%|b_jj eO`a'PPޏ[p?G4J*|oyV,A0eI a[\e0e>*!Dž[lx]vޱhcNQݵUͱkA{"*iq`p|>L]ߥ%>"фS`z;~>-}HI**F/99dx 4t7i@)Fzy2 /'SnXNP <Ǝ W5L:r8EBHdOP{QWcgljf)mRG)aNUr2̀a _ijaݥWTQW~:^*TEŴEtGK 7cBIr}O%c!ޟ|ʰ'Ճ sG킷C6)bz1TY'bR(y\ LoQ-]@wYZ9 (p3Do%r wͬ(*r:ۋq|nke=K'/xYn\w?CRhv!4\gTs(F|dj/k%RP*#/Zwt7 7fau.L:V noqVGDT2+QgL?_/)=}:SIB{V 67 '.;T4saultkeR+q )o*[ykU%_=Ã= $Ch@xYxvn4l m!w _fMfbgY7p0:+6&jd0ҲE3at9PRFo t@}غ'B5qIuBg ?_OIWȼoK~Ưz@&{#)^Y]$×JA/_z}#Ńkm"IY+j3-tC{ֈX`Hx#yvQᵯ}TW U=uUCO +Uݦ2,c|a?^T]3ɺa7鿘8È[??Kđ7#2Pk-r%{]rhXLqGk?BH}~cn#VWNYN g^s}܇a ȂJoe:h%Z笐Ƙ]/%jI󯗩.Y 'wN5t>3!uƀekiA(x.>Se9aJWN2`w")t_Gɣgr@ᗲFd ~y>m^Txnf%y K'D"s=`O$-[yLaV) &LGfə?,K.f3[0.\smfE<T7!l_v#(H~Qd>&*%LvnH#⑼7(bcscɐyAnۍ A_55PZf#1M6M#o3{ZDhYUCPCXErf0!Dq,Hv@^D>kNJۄj{Ufޭ0M0_]$܀'*4<=#b:fVb)Ha>"" ocK^xՃ]jEz͋˝1 qx\=fɯT0 oy#A6gdOto) i,CÑ: &Gs~mU 8<7xdVvј*uѤȶESokAF!G0\ eU[%XY,:"1R]gzѼ#3UٚLDJ `\q*iNfCx6>ݒ߉a"yI=d{TGɟ$P(r.(߷IOdk$^YrOh^ jZI+b}hP|Jj}H?<r͚A{2<W=Z-xEy~)lBM Z49Mh9s+GF?2!v;bHyƂk PUR):}V\kѡe[=v'ǓA q% P-Ad6^9P(T0^%`4z7H‹hŠ^[raԝZ4 P!}OBH_ӴCZŁ^jou[{#{q2%C,gW0i䤐)Q6OY꒩.Gs(Rǹ˲?BLie+-B4P(`И8-F2W%0M*>TBᕂ!l_5t, {3 qEd`s rCt7%(NmPj# `Y8gnȥ`7Mr:bKGko6z 1ԫYԧfk{F-4Ѫ+>`]md`aogX f~ץ4-r1&P6"n/m|s]EbPnie\f3E-!m@eIwUUJGmN$Lf[So'߼e(@{d*eѐJB/ȏY;ÚWX@>Z7XGm㌬fʉ aWA_Bژ_Op.:FiXvlOo؜}EW{wC?A@$D=z5)wmdk.2,MF ܉/Y .|8xaØ5v>wBV;E ;\ci#݄)G􃶩7~ ^ڈ Hc SǕ'YxWY /!*rzA WT 9K.Դ)q|_tTQm a<~x$ ~a`b>V3 X<΄UG:l kg#ry3ns<F@HsŤrM^Dp1 _d3@|b2Zp?_$>R 8r ?aN[]cxtp ^X P;zI~~4F;#gK:2dzmoxz E7G(i[Cp6ô_~Ő9ULMP<^@r;#J>2I{fA헷@ND 'vx9fAqOlkj]DSw?5迗M?>pn{%)B>Ov8J&}9BAg ݩ=A/p7(_azgld8ֽCuBLI COQDmlb(ub#IHIfP1bʹwBa]l~hOd^0NfS /~ځۺVj?VbKD.t85w=k:Lߕ 1a rkE© RTdbv,Ru1gL2/N]z4#*YkE[څ tjѤG)2\3VFkZsB-3c 8e; ]jy&iX1h0$!K^͋UF)vt3=_๺ 2[oR`gv*To`0OBmJ5S|SЂMq(&f=8)c S59r=Ϡ7V6r\~5#PgZ~n0~^K$)26=fSym{RTw- GJi h='[[LX}酝Q:Uݒk7O>_:w 6u^z\ŴQ;zO}[Q,Jƻr#uMzڽn@GuQ|*y*gF@ĄΧ{Syłʨej>>*7(Zbf{tSm}9e}گpNI npMj؅%~Tq(6Juvcb-fywbo׬o "@VzN.;6Vm0Pw7c&~O v`'rό-HdHN}J?? ^3Hiyˑw m_K 3@C7v^hz9y XpSpju8w)MgBzw.X?ǵவ( =n%t 0 Wt)]=Żc- Fh۩AX,Nk0+okS[.jޑOW~hSrcSf.teMʩ-&L`H%!|Wk3E 'ZRޓ1r3t 8 ~i3¿> Ҳk=I~ߡurB[E ,49z4Ɨ´d)!ƊFr&n5l-vT~y%`(JjMgշ$yj0ڃD<_C[7߰:O fĥ2gDW N~*gZhnվ*pf|ը,WP%tt O>^gN3SZSԡYC*gkWGc4CD;p`YDCY.șQG벧&ݥjgSG_Hfh042!=5 6Vlծ Ca)#sl~Q~w5x*En@JBJzY#8۰sEcFSXq%,Ma4yp6ν;T(`s}Q)=~p[41 3SGF /?FsF uOI`NO0"X'?%/  כd縦兝H6"UKcm3NGDԳ"6f\nFL16Ȧ IFo̅"A N@){y<2Ld\V̘s=thS]>x_Ԙ{g&(`rC 'v֍*As7fc#`\Ҥ2K4RR 1][ۘ𸌠%”eF E-DhVn;SGY˨FXo@&p_:KIl*ڃmҍ5g@cPf4EB0%8\˝4>pԻAAN:eT8 s)s3`OmDZ_*pgj 'Q_cdLIuћDnzOr&Z+o.ˤ{$G-y{KDpi)'ɓ `9WnTq.Gv`B L}7%PTh:ߟ<]:*F.CZp9@M- t^;zϳ䮕B+tTsX־{lX<.3[a{mu_f- tC_yoRܰx:TdWas~{i pm8b[0xvy`cXuz(>ad}Un8q\ k[’9JPRY0q̨cNԘqu㍉B<)>6$v*m_\8AՋҘ6_ :EMr"7gW  lX zI`+ĸX{k@w$_}{H_EY=5X֭t*yY7fٖ9o#@p*p'Y3MBHGyfҎ/Mu)Z )S(ٝ\wu`F5h|} W_-a w\ \,N`H="v"H(>3K%#(7Y(v` Uo+ ?IsNBHMViR)? uWeɰra18\ _8ȉ5} 7=B :Y,v/~VNs` F3,\I8?s(뭷xy,[Eŀv!C*ip00Ut7 1o0o j&{~y)PO|]sX']OPT UW@LJY{s@Ȳ֛/]l88P2#Pk.$8'Vŋ1"6RSQ& N5MrEruwDqJ1=!'`rVYc;@u09 Qd/Xjn: +i@s]: tXWT$ҷc~7 ȰIC{`᭞ ~F~Ԫ*-2 pS}IT>gGmGZb*gbȈ8tA6khh-4ʾf.R/Lߣ^z.4>EbԪVɤWӐ )#Ԭ6%_ʓ6= D Je>Lؼh*A _v7Xa;Au<F7<D? /}ݘTYtn*i4 ;Xі*Q*U"nf.Bkf^o0a8~ .XH*hQm?<|3DZ|l^Y<\0Ro@j=2+p0H&߾ZT\-:'Ǥ^ sٿz=pZ&qDlcs1ge F#kiwjhҧ}4ެM@K_Vń*C١H}ݣtϠO[ D_Gdsf^% jEPQ;Z=h4+ItA]Pu8nX HW&e1Ƥi_ ٽd.x؏a:\T|ZiL;CstW8(7u)o6^+J( ~e;A` hsa>>A#2 Wa@Qާrȸ:,yw f 5p?y|vӷ c7 VPKw=rPQO,! $CyB>zZ\ 9L&v7oJa`E9 ~ DV]E C%pYCIZ[(r-paXN/qSELut-&&:a hHJd)1 MqheKȡ 6òD`j8:lޑ*OP\|͌ Ŭd:+ kirsdPG g3Q:U9..Lş`*H ԢڂĔ4(=jE $[+J()|w=D>fbQ -Cj;pVYgC/=hE nTG8$g5zư 7A}Oq D8}ޡ^eucanԣ.pF%D`l ^vcZ|~dPíҜ*_ll8mv"7ۗ@ťlrFB pZ:ceWDv^>}8XT Niv0ߐ+%mEo5pŵ,bUA3acr.ȿZ禎,uY0?3Q4@Z jR*8pcbuZ 2,s}̎f"l~? h&kx>䛘[iM | Y7ts+g^*gC"u#;n-5|XJG[[M. 3BbsƨT04=?`VpL?m+1 r]xY>N`~q84c0{sMD"0DOBMk-J*q*)dOsK#Ų:0};Y%r2UgrCk`XߟjacDmz l\}/3%$G&٪})SH8jyKߒԎ!$Ob6Z/_ ەؓU̚YabVV諐nΠ<uթZٖMc?%9Y ꡁ5=mR\:!Ϯ #>~5(O8&/,DqDruw41t+c8Vysweiɶ89@_ $ B.6҉6y! ^65"N-d7 &Ifڋ[H"I߳> Kc˒R~kKX0h)(4 <85~}<]A; kVs{Z|\>|s!3f/ `|v9Ofއ')j$51`Y~10w{X;vVy@ο+kEl7ћR:3ZPqg>{?oh]ZcsahJW]lipA[`U,5iB1ֈ J?rTp` [sۙl*XEM߄#>Ms0 y/T8D@8b8a^ GKw"DJJ^)(Eҝ]r%"ƠP`~3~% 1f&.2ajB:/9ׯ¹s7`uJEAQ3+R(Hz{[ÒanCez0y.a{T~X>5^s7<|<MPfF䟢LR肸kJb,'xDxޮE`Ҭ+a p -slLe} ZOV]`  [_1wR;4YIw$ 3P"a:;mޯg<%3bShkx'B3\?kl@,&xI J 6.T+P 6؅}}nH-T}3Ui.ֺEA;0e~"'X3;tZx<7L: potӷyGӔ:꥘ "#Wf#>4GiO1 AӐϾ!O6)c=\Tl/5{;D>q~6 ޳iDt$bM_d =ۜj\c9\[r~t'̲ :>#t2>$zx`;ȔF~?Hcڦ{lfgfzڱ+,eQ&~q!Y3!YH;0^&߱~DŽj Cu@؅2CAπ6eщ[XBl*2(AyY9 4cDV.෴Ԥ6DA$QhZyr s;Wq:93FBM. f!.hC$k[ ֽsvn'p4lnbďGc:6쑘-ŗn^StzkN'ohGVM|߮h5k>Q;}#,٫ˉ2mt9C NJƩ9g|^0|R2hyW|쟽Hl]8XkhW *J@Da5%aZ|陂 ̰i5ۆx%;qE-kt~.\ XP:~^%H !;߷IڂO!I䙪j9pxR%W.(fMy,Lsߨ)?׼ ;SbFphϨ򽋍@11hANvfB^&砺H9XoNp/s_DY~ypS&:GQhL'`G* 2 Rk/8!>84(8|/._y4h݈ ]lYU$\^6Z˯Yѵ&YǴ="ee z#{}!/u ȅ{CMJGڿ[hSVrDAվj.Z_[V/ZT'1m(2+#D0CL/=7V`c)|B4[bl #h&IcSÿ$~l{K)b:cb @_>\Oe]&\ִ>-(s6j`Db}O[Y8{Kt*y_3A`WQ}+ЮDžr"Qy7[Xsu1ԟ n'0*dtR[Ӏ2P3}9C1M@  @څrA3ltI9,8C0yl> Uxh`V-wkFҀ~O艔A;Q?*rYz~"}:*]HUVmѐ}Ϧ@RGj)ps(8;^!ȩ:s&R%]i ދ`Dg5Zvmgh${`e|ߪ8UFd{ lnF֦ٖѵaO™-m L1o8؜+${w`"l33SC(\ p,aSjje;4۹$Gn Uԛz/Ytu"\jt臥RZ!ZWͦT&)4OP `[@fC,Ϙ`zCr$Cˑ5qVH.+ӕL#?;;=^>j6tol}:O0L {Pr7IW|w~f֍1@HD+~|8"P`n;0u[8 WOdLa Tw6g?&Mҵn@׻hxi# 3 o="OHs}aZB\pAA}#GI\BTƼJ Cx[P  %.zj:]xZY+R.;ҙb+la&!ji@"PMZl8h0[>a]PT-|>%JUK:\Bd=1  (Th}ĕ5LV{VึITw*U jϭ4Njq hz `AMuRV9b$L䤐`hc=XG6fl䝘:Bd\-C{"X՟lXݭSdɼk˂HIP͟fWAgvyM@Lه }dlǖ­gYƀI | =( ;ClO;2'#f<{l'yMƮw /QǩzSOXEZ;by1vA1M'0y0j.|Km <} \xֲ˭$fijh |C?90mw SΜ p{r芼/rx6۩áX~9'a-l'y{I"Ҋ~.fHxQ]9=/l {}]V'u6C*.XJG5^BӔ9 1biDaN+j _ېswHmcn9 RL&+x2XL|ٿ<؏XH^b!؎#w ]|G\(jcU/EI*KSơe15B΋~N.Uuu`!f]G)\(JVΡUpd-Gp p:1јT mh:O,duīu"?nPgQe]'_~ldZ Z*;|gc2Eρ]jбpd_\K0FW-%@H[\nB[*] >]*1, P?6|M|plhOn/ϲ^a/F0܊>2^r|'T )Pͫ5RbCh9-BIX&Lp=R_Ͼ)@IQFUԊ}MrGhm/= 6^hR~6t8Dlꦡ]VŲr7c;d?3m. QF0ڃ3E Ս R|a5DdKWkg!+5{>i`T~9t>rG@^5n"20TsF]6J`:$rr]yARTMәl=ZN/'Oq!QNN)*BB 7O|Y؋k2((PqaϧA6 g𭢤RHB3D$vJ~gK{K \>B88r$;a/:B,Tfu7gP=Z#V.띕`@,1k>۴W׵LҬ֦XL(|{N`($IK>~î}3Ĩ3?0f0¿ ~&8 kM96<ܑXz~ksH D*HWs8|MqH? p7C;+|W=tb%f͂T*bm~09hoXO .&33*[j F&fsY*5pdz. FܻPҰ H lZ MZ7c$׌++.l|`V aMͽ ,+l'Ձ0 =@)1LȘh WhTT=z=%) 6J}dhl- ݀W)-'K+ipC3[&0{pElhPqړϳI>Ǘ2Ȟ-y :=q ^\O;-qEZBv-fw ʦ>o;:5gBWʳbID>_TtIZs,է -x!aWp'Rs,B*Ax Gy<~g<'V!y8]_6Hu2m_"LY3ӡz nWtn&yUi>JL?l!ƀ/IR`-Cy"Pt9:YDN~-Ex(+3d_?/XZCY_Pg[ƒ`h?4*qoEKP6]/p-^_R&bQ||k-Y1<ׄB?@''dvVZʯGBPc5F1yY:W#G][+Nk!xZ\qx6,  j~EDƜzᩴ*q [ &Wjӫ9 ~,Ob5f ,31rn8z"*3C5%.륗d1'ݱ!M֖S}oUBi-gad^&)j_~բiҕ:{[L%ZDjo LWc0ϲ/'qod{?8}9' U̺Cfk(p71u+ 04uj~{:Z^_)aqqt>!M9sJ&TS\{ѯKYYH#csᕣsP=wTK Y7mG2VxVw ze̳UoU. g-zk7>hN!cywfEs*#Uvs~rۗcIVwxZ)59^L4]s "vb,OXb!sr';%}?nQPc{hA\Y&dk-ާ^ӸO;^lbz*?IkY~[qZ]pClB:qR-՘m'ow աFՊB#:CQK` "n~S]˧Jg/ڸX?6wts*|Ѳ'px,`ﲨ=$I$2&VUC{#'p< u RId(s~G6ز>Ӈ=컹WB[o[9 .5 -md?;wAtƀ䄆0u.+] ]Cm}[^f& :5DC*Cjb_:-k6I\JqtNwaoq;YKUybc2D>qFܲi5Voa'=YangYߪołcĹ\ӥb #˶ǓwKt5;"偣CUYzC-yeXIoMGYˮ5E+50|JǐAjp>9pXLr}z҅{EHf0 {[yC~nJhG'.)s@ @4 R_MGkH@|i)g&3#,A"*7WҬs|_}ZY%StGCRXZ/,zzYmK#t+p8Nmg{IķP9_n&B/Jh?g-M^Yl;Ij X8m>(xrֽxq3m#vfcd]A~ ol #<`̀W$[ .@fg*}D gX!kU5 /r:Pr2҈e>)f^\eZ܌Ntιs2Lώ>E=Ѡ@$-%?Gf[+8S+h"O[܄*G?~Սf{^߹sS9LOrQ:ț'm2aͣ o1C`&2uXў.>. Ea/?IR8zVJ @?0zixa)-?!_#ʖkBeRZt]D J=@;ms7HO25,5YuxEe+E4[ C QzNsoFSF"#q畘wMX;w-.k*~^INac\SQ/Dz#JVFdut~: 5V.㨌/MD@HE!K[VƗlBCVeJ+0RJ0|N=k뢔]e& vǀPWCЋe vi4pi82L0$g{tCsѤ.TauRO>3(_L-.0J,\wVȒ0k2K9SmToPuj7 k '-AIK=4.t^C#Z{UȲ.#.8w$LYDBek(xb-zpLh^ֈ=QX-4tH¹ EM jNڊA?Se6/zb4`dF#u/M,Oo}/2HkN'C5w6MR\00QB&km>7voVooskZ_bdgI  qԽl-.['qZ\PZ<ΎŚN@Y.jڢZm&czwx2-8(r&Vq羿.ރ^YMcSj1^B<%C<~SznZ>>ޓ7:+I~R8 AQDYԙ(`kb퇪A$wF%DԓV @~ϧ:Z'D켖[o"-z38Ǧw7Әp6ƽֲbGQHMݒo_XcK&Knsbpl=+p[V a~5{IZ.ͨBRQm@_5VGc Ij]M l7HwG$ԏRJ.**+$}z۔tuyϟ2hg<ȒoK@~UU[Ȭ8)7g%[s'p :j41(c`rLEJ33WXz6W> _dz%0Wp'#6!sjĞzG +\Մ}* Y xcLEx>@czO޴Ū(o1+ >r$|}P~ jV+z'}Σ*$ 9Rhb lk6 I7P: 87ȽGِx#?͓{!p@X_nbgv3@cM'a5lackj&`a4ϿJ(X ӃiQ=>*4 RlGBeRGQ'4P~hՃ>jnGi|/_+ 4s) tvq))9K77BZ<WA9@@M4W0,S¡~.q}hzzCIe@77_x$ˣFSss\R3[}gw4!k"0ޣz$~1h&ۣfjeO|Y6H?3uѴj?jM5OG g4v~|_ZLM:;y#ՎC޼Ӌ67Ze QbPλ5L"3%_md G8c(*bXY{#~ʣkf3db%K@]3[VS6\aic%vPoz?(AC&b*YM;!U~lsXP;zc C?o T4ˣ+Mۭu /-| ay!щC#s^.xIZ:@oj.'25NMB^c4ײ6'?"WhM”Wjc8|d~kflid'pM?᧓~WXnm ;Wm0w ]Hwj'A|0"$9STݟxӭ[fs><}Ih[Tpv9[dFz XsoaVe6`%yO'}ZShbWZ >Um ~a$q*00>a,H3ynN]>Deq gB/ٙ^v^6#v^,8p?h)IJJBNX{#e9'-G& u&J=pXV1i}7|@=-˫ğ9"EDt}~2`c2 ]0D#$ػH<̈> Y;sXbmSi_ ?O b@;ԋƈ9{B#ceX}X,BJUޝY"1g`9B9va!8mj> %&G5ua'')G}+K+Q}r! ex=OW@*kΨn}*O ^|t5<ƒqSLAt#cFN'bI1K&3;'_kV{M9֪cYSV$ɹQ^*]rf[$A(eQQ=!G1HIxa;Я񳣤" L0 [O.L MUyjv_?,f),a]~ScK=#Ϫ{ʤl =kزϟk ~E6y02IudJ!El jf[mY*] Gbxc/.~g׾I- ?7Ly;?A&8H#"z- zZQ! $ lCUW B8f ne/'"~:~]Um& &M9gf}(`yn}ñZ)$?&qR?{\O΀quCןj9]+lך5ޘ`phmXBha#,_D𤜨@c=,΍SIUڪlȰҸ),2 !Kq$; >(҉۩F0 Op.:?%r8**0SLTl8HW ȳMՙ"ő{X%-$2KL.z8 wol:{|"<ipm(Z̴Ii.ΠgFz9,?h&qy~_*xYdӈ2c 0,kt~nwpwcm-1-ݎzCeGqn!P8jb[nK>@k? O__־o^k;dtaq\lg^38a zl|w֒g.Q5T dwze8!%+wv)S@ 1EaN/%'{0X2gCn~?iK²!fP`) Jrk`%NWt `Z"P|pC F}q}t@YO0y*. l|7I.J rSn)z0_1g N3p Կz;Sn ۻPVvEڪꪷQw|vS ><&Sb撩 b,~Fb&y@caJ:UׯHы `1ey"D]kxqV%3)儭Ri 7P_6_TѴ #WYI'nA"4rzkEiMR75UvU.N_u0cB`J_T8t+!^-Ԟ!@5eg&!i }(fgզx#_@^+wc(:0r.\tw  .J5,46BpMGHaA"hpտ~|UQ*_F+mB\SRBӽJN^ʎ.@4KT\|"YqxNջ0kX,O0Xo4|3`懵#(M&jpˆ^I(pȩE["5Ŵ)H.$LR)@9@M`DRQx7@*_$۞~ƪ( *yN_=P a3LQz sLbeiC3bBnKZs͎卥~\ LM1(PuL,NeW&"|k aĘC ^`qvH]>Ƿi=>;YfThgjdVoHX~K"y.Ҟ4-G/"2#>%Še)=ȫk\imP:qE3c.V-& nIݻ1(B9=DdB9^WJYVذ 8 6J .L~/>Q|=\w lsmO[4G⵲}_'Ak`t4 JMFiw(ˆLO٦9(?y\f/<ϊ7 0uB[ 7 bIN硎<|ŧ?^!N~GҢ;2Fw]J7>RHZ5~+SPǛ2xlmzp J.`.n^Osd%WWԵMoH%cRyK\`ivQ1Ȕ0;Ijk: eM\nʯ .I:$I~Y=J$pq]|sGdnXMyð'ֆl{xLfvoZ&Дb7۳%r$:s4OkS Oo[pYFgL1iH8dXl٪^ck.<\/"}ݏ%Nc|Zz!:K-n$r<>PR5瑢 RW'cq%c@ rY0d;[&+R' ])J\h;u;\5Le 4_x_k?>o\w>+Cbhr6cj<@gu])TTv=ρޑ '@5 J/ ]P5oJqշ7kYQr^3)6(?bAv1g*j7Zy~h0_>AWץR&*IҠ{ -"xBֈН?.v5tdG  kuS/2}”&_҂=4zEʾ.}sGڱKCA : >b,0;C}dȍܚցW4؏6n1w)b&bqdlbV1KԯGޫc-j`UfzҖg$NVqcx8DE Fgz%2o ư`V-|)2є,稃oݟ`Qs5CcpM>A/ǡWνgׁ\hSLy@Mc0c!zKHϫظp,I(Pڶ$vSO mpk7{K+U^l ?:\Y!\mG&n I 3Jھ)V ]^}A'JdjrE'n8~ĝ+3ɈR!MV 42r*;Yڗ\7SىEIV{Ol֬ ߒƽ5;ZM8I?.΋aJn_Җ3W)`KVLOt tV ?]ި[R'v"?n B`t(RGdUo74D~Xhue7ʲ{a6++;W|֣n<|/ۄϣ0[}ϸ$ud*sȪ IA2'o[ _A+V0gQ6|q e-23Җ#v,Ri/Ié?z5\+%4yp8Q%UW w6V p qpv'z**|9!Ѧ b( kfYJ MၽtZƠOj]l L\s̪2~Yei\ ]"D0`\cq}k'͕e~uu%Hٶ!>S~Ac.`Ihx-Ε8} D{cl PeB\-^9 Bu#>N?vEԩUL|$:2hǬMÐE:Vv$,q14Md^ *} %HV!(=<>K˪7}*x]LOrŝY9-I %4̂~ [ 8Wi5~EGǽQ\&]hXżvkK~kοq[^5&۸t+Rʗbɹ[z]ױCdtӶe8 duKj+#4prD1Is?4^c& %!pN{VD$J,jMP'mJϙnXQL6k4cѮ!%Q|ЍõהGÙ!u,N7 h:԰W= 6 E/ۺ(J+[:Luئ_,Y zߧ;6J䋂!(JkMop5JXv`GV^D B u <A4oGb~D:݆sQ})@Y.}qT?iZ@(42  ~+^}3%IrZoC]G&KMUhi"ʍ AEj 0:M"O6,`,||ϾvPSO㑎ŊD%*A>aSQBQ~=EsَΰB1Hg*UJGߢcmWnBQw=3DsX44$<-AC;a)Z %.M]hfÿ8'O_:"$Y7ltXNgիXpĺet )/Gp?~yc٘T/Wbzix͚T.ҢWz!8M[.6uu4e} V,!)z6׍'̓? ţ~d "L>f)1 v^_N$])^5[(HV/J.n1gxSn-4tfRs \k@sQon ^ Ǣ؏TTbOA0'{W14Y:ELVke)vyp4i^Ne }Ì[( ;jS_qUL{!ESEA1.;h)1]Ƣҳ5}GP >jYfGkLMYBk?+K}0˺p]|l,M&B_T\UCɚr8 ؠe{> \8Rb2:[&tO nH'%14M@nvؓ?S ĥ1#ߗI/m_&mL*>5DSQFj!׿z6sQ[ZPSdIOyT̪y^ebYeY@bb'f7됴XğTNJQ(oB(Xׯ7,jIp{EY@+>իyBmL xeB?sV<@FΊ{Wƺ$qI<м$Tn2gh Y_eY9Β i+RWzƑ K፦ N.8j89c~5fl- d/g񴁮y9ܦB/q"NѝA􃨹j\(u( lVa]ǡwf)`; |rCbTj~kWޒ(9U#KU_d)19A̅0P_edFXF; ]:.-1*%~]b`PJsAS`WW\hn>\qPBrh};=6j'5%dr' 9 IU){ G#HK *= Vi_h.Eͯ2 r2\k'8i0"Pw+/J54EC~mOƨ=ODDY܋~R}pf+xgus3ГP<ewT)L85pvwϒ|a !`!o׊#zlXdzL8wŏ uu>y_5d㓤o/bw&/):ݲBsq7TS~}HPDU(uA;킥_IrؤalÒߦc KTB[F )Uh+YPV[}_,Tتι.,I&X6MsYhR[nõ*V햵4v.,bۊe~{TںAJiwHu)\xlE+1M oRkI!BQ„ln@n|kv3ۊFBizJqJ'5@]Ӷ.FU')݂~NktԮ,R?dz[/to\S[_ax=sq-y![dꧽd猥vc,gds<ze3& v"A>5 o}A%}Dz۞=,C='wN*I\Cjx@wv LI[Q!T%.Xݛ?+K3=3o{_;YL[zŀ]FQx}B=P '͖7YKH^p\QP3Gf4 [FP8|߆#Z:O[ȧf?kJO?D?9lj(xƙc%14<aET1g}?oUTEzYNh8Ќ?7{Nu8n!f)|3FFviY-_3T\ 7.Ad7y/f~"x}"vvu8gbjx#, 4|am*)Rڨߊ~yʆ,tauX/䐺tr'8>vh)>SϓteLŎsP馣n^ɮ2IiJBN4zv*᪝ c:u2Z2e{{-efV\CQ' ~tZKIm%B?GFX7<AZnmܕ&Ί|ڠ 7*ӑ4-}>a΄ZvX\iAFt4 0}|MLƁ9.օ2%OVԌ ;I[3:D _vf$~Jt PCT>s*y:WVўTэ.W>oe1_>$հwD %-" Z͵PM& [2NCv ͘?(w/) W¡onj!$Pn޹K\w_ k4;!i<,f(#㔐x͌yw࿽;j],yfqqȷMPvahG~w(h3Qπ׀ щ5=M_~UtrƒL[2,֑4mh?e)cUB QMXCzӎ?78]ybDf}*'8LOL?U:ӘvKOcqT46هe=COJjɂYXמ4sxd[`Lm؏YѐҳYqϐJBG#E@Zat_^J.cfrb`޻[AToR_}^ c!jN'^p'-r8/N*bFHp=w%Х{st$?%Zo+/rqO59#puK#@ۻANw$uV6:VzD>xW3J.1]{4uw!*9bJ,{h 3ˆqjŕX^n.iQpzCd#` *={Rr%F<SOK[{=4U#բS{yf %F흙\MWe69,<>]$3ޭD0R5A4~Iʦӿr5bHb!or p6}9zwx% }_nv"w4{M}2}4\ӯ:u$ 5*sC %LKǹk+-Mfk20ai /!.3To-c1^mNUg_6+\s>!DQ '{\C70x1=WkVUU~i"诟&,p E7-W Wp% Vԫ笭Igw;4*&* T_/W*FcңC`H| zbeOb>++=،b E#aMg ۰ TӬ([_!TZv;fUUB%I^u\^c$5KǢCZ :I:{DB{ָcwySP!Su$.f !.eeDc0< ,T֐FBh1O$8 > I^( (ULs}2(w57lE}KOj )(zYG1TzNJLq!.ذ}$9Jm]Bn]YwH~_y>rܗ9G"܃$]o"rvWUon8Eddfė&SF\غ<.y*CWK3_i-wu2aPtAR0vvIJYgNde._zc5zY8ȶR,ϣ}9Ó2͠^]sc7Q.cY͚jEtЇzm{SYn3.5gЪ~q<_ưҀi~yrgcyn6V$si;X ^,QoTC^]w:nl~1@>ݍv"i6q7\4`,9VP3w|Ơkde<,7yULYvjlL4e±4R\Ȉy+;F d:J̜aljSCZ. ~Zo0&!L<_//ܕ53iavrg#IK?mn`[֚vs/D"uɵ*ݬgg#~8]DrBZG?ËX 靊3Um: .\n 1i -F /|MYS՝埬hϳ~^lm|e@-!S0Ɵ,$R4H1—Јo 3p_>vPۭٟVs/V~t$1LG )f.P:l}Įxd'B]kYn+Wc=Gڻ@c=lZga/<]\#)C3|24J[u|'VO]۵gYJ% ?p3)Tu@Ht!HuJDsR2I] BPgsźbAAer-0D4@J&4@3@1T"|kRP pjU% BFJ>B%d\5CTiJ\:F Xb‰~G"PqȩxdD t2y,0O( 2&$"]HR`}d:PdE 3F J8 $,A%GO(CtLF ѪȯFQ1QhvTG{~otDFPO Cf&X3S? /#$t$~MƐމ ` H@40h`"s<Ρ y/@%B."DMgGR—r'ӕkff +-_6 ~ aRxq+|+|_>P#8B9'Qca %| )LكD:@ӗ>S3%g>Ad@J^ҁSMDRl =m;S1NU'^㫜Qc}B7\B.>jB!0LrB}3H0' IJxgY!~{/tKGB&Vc $/'A8R@  *wQ|Br&`FTm%k@} I@TCXpL".(ImL0&r JRIpђ&$ÜJ5t$S 11!@1N?\QX>$K !L?(0~L$H͙TuH9 SpEJ|!.I%D%(!(E K Ġ8y0I@s$U T$K@(|+8σr>$MM"f|_ p7*|Iqs .I.؝nL%`^ǻ)I83nC=L,W¯]I|[N!NC̑@Tń8`;1T7CUHB$y\H GtO99mLsr rz2zGt Y[F5M xIR.A$!QT&He^"I {~<}/ 1)诀!nDG`C! )Y.a.:@\px-.:FIb":Ŕg%1q%%LoW!~1aLbt bBa: :g$}'"4\"LP=ۡv$S yY_u/ Lb)$Cwv!xC!TzC8{'Zsv}J.^d3Ƞ˷|(t|/ ~a_ (|(s(\B5uاG]asmA"Bt1aja%$QNC~)r~UFm_& |{a$݋L$!o#6"/I7۽{l[<<?{ J$}eJAeL±7{ @)$;MF94k}]CNPد3z-AD%n `FT\ar .$n\!5FJ)b:x3Bq E`\$Lsetnb sBse[i$)Q",< d闕;˶tq߳=q$ te|kúe~m}RB?r߉aktH(#u$ " =mV A`r0Aȗv8߬ + +,tAH)@w&^ltD xg_܇XA;D+7}cB*I] ^-T5xn}VJݣytb}oϴ .%']`VUUu\h(Twʕ%<%ωګS܉o_IKfGtr{ ƞ~cۿ T\)$^ <@>*e>e ƿ1e1`c|J"$$>4PBx߱WJ\^g\HʓL r/̚%[jtW ׾Dw:x7P5ntB( :F3@qҘ&u&0h"J 7aL@ɆSdlX+y]!x16J~85swoE[-g Ggk[qFuB/HabLimn໏]wH5B5r7kϿ1rxł״qf/`$K`g[:(siS*<%I!7VÆ3ԯza^6𐋤L˖fٞYPLF G0\ʳtef:q2.ƨoE))0Y_^Z v⭬|1٦bL7G{'rn~qw{>v (:5s]۴7yA^;ohegL&wCx8%֠71z|;FvTlvIDyƋCN*uEoesH&Ќ1E27ManArЕqa o6KǢ[VT֐q9IulWXɼWNC`Y~)mn3[`݃f`ղv{ h5ڦ>v9qqt<: |ҩ6deR7rsӑ^Yŗmn ;5fn} '}GWۮUq-)[Ʉ=Fj5О5ivdnsVQtxV0f~0gвoK_dq3DcKYJo6V';OVEPJmF+˶)TIzP7=gv IyÙjUm+`>tVTv|i5j ξ84O1:e`!d²=R֟x]GfvS2$V3{oL&"R촀v@۵ɒ5;* FVG'(]) n[ґfJ:eQ[ /iR*q8v X2E\-[%M3[ㄙAirFAX/F&U1 PK۞'ɼ)3ƺѸhӛB[DZ`=oYPesWkfcdl䷺a9P9`^`YؚTTS`.fk\xԛ/+ylN]>߬oSf>kox!huaHh8Qtڶbn)[zLs]c2,\lg-7xLYL'5(( #uVCoA'RAN8zHlj=Btͅ.lJUa҂S)r7G6Hf1Lų/SMQ3~̵i`k:~癦S +pp xgorlCzW%F|Zd85vlt(<ל@ݗAJ Y_>՚ d)fg+]ezlVY~hqw0Z B2ʺm0ڝQD5bWzpzB>cffUB)*vU%vRyY0_cUeY s›!‹^X|->1իLk~H1>|;yv)Jn dzbJY0K 9ضjmcW̓KmC1ޡ3|>)ҋ(8:6X,rGrsЇ>DQqBO$ۿ 0,R93:ApICZ]ڈ-bR&a[ެtKAHu֋*tBRAgYMwXȀSg33z3M#J6~\X+vNoT z@MNL* cx H{Sg:`#޳^] < d˸! /կG|Iv*@-RnhfHTD~8v C.M ՋIڱWlZo=׷uI5,^wo)[7.6"imv[Ls*Lbw򥜮Dn&?;@Q}rr7k瀍R"T|4ؚؓu{Yb<e QA^ܿRL!2Itc󴽞{p]l/Bw D.:媴8M6~<_!P3g"EҲ^+Ɣ :n͊&8&'yQ:xMP/kQlJGe0U["GZO`2HkTJHN .=urz &;KI^;~Fy_R4ݣ@32,7ĕߟAABYo \4 6vьBrQQ\ߚx?[ @(} <` 9,S9&7$үRd6`"t}ЂAf͎~GzRRH+}[ܗ%RJT6ܮAn*F(I$y!wlOf^OH]uE*9+y*O0c/VQW |sv|Gc]y@Nbwqu"Z+哴Qp6)wb"V:}厭u޿z+ƙCl|P񼠭,<)Fr1F}uJ%WYhZHn/޸hIQ\IeAc5تj>&TFL\N۞ytV.<څBM07@Ѡ),0%{ז 3Wü56zZ U^ !3跏]SR+hgz}r?ycCGq4k_樬L>'=@ ON[QLln+C=S2P^p;C1R-gc6R1nUf$% qHeY yEn s=e>b`!XۻCذL'M>"e7%B%pp|wVq3x>o ΂@g C7v9YːOV " q_#nԨޥ^ml^ Yu݌bj9qj%SGSy#wE!6<,l ĸ*̘-!1F9¾KUeg穀DCiQ D*-<*aJ9lQR9%=E ;J :g-ޢV&NVq[jGZ:۞/&/ c߇H"LHwh%n o,l]"WC XHNpf7bֻl؅9ZrΉw'ZڹL!$b[4bКၨ/{}01Sh{!=CJfקmw˙"{5'09mF&edd[:F`qqqr?Axh\F'AwI@d.O(4` 1O}*t yӍp 1/;&gS O`% JOÓZt?#P{+r§z)|WBѼ]w^nJYT`h]Π,Ւ2 ~̈́}vGa{WV-U%;̝ gÃmIa&u =)S sG}v<{RzGcű;=+g?2R h0zNǼH5"W. (297Z( $q*/<7 >ؽ +5ouB*8Ӟo5~fKf|%2"˝kQE/5 ܠaqM;66f5mݍ(Xݛ x >H[ޠ/cLM)Fp\TjiK}[㱻-4>ϋY}5tDXw|~/^\0ə\v(ݶB͌\W=9Iar DN+gڧBo޲ W(y 8n Qx z0}{ F gtZQJTT3sJ|{~#`;r)]E`ؠK%v;J 9hӲݶG r6id R䳛y?v\T"5GS6KY8XEյ$lv$܀*ToL)݇ߞpmV4Pl u2>ߘ@^l끐I_&You# zmc)&ypcݬq K,Rsx{\ J#遣 gEمx98e{n?RM~f]$P5C`H?C{(Qs(x{r 7 |O`.5cܻdnԡPGO[$F,L\ :nA2#>谝O/͏T#=ϕ| Y|E(I_-0ڔVedhs<6 y6ݜe)r7_%tN=>ړyi'rWckշ*nt3MmY芯Ua.}"O[ ˔yy{]6I^\H@~9yYz8*jXnBÞS%%agkЅ7ll݀^_ 8tDL{O7[9kcv͌# cƪ@1Sˎvo|DlR|jc@z.TFSeW73|rHMI.ƐxG~sٴ fkگ1/In>q[wHx YÛ)fB;{BJ)wB>>;Ό"`"umr9.*u{, ~-Lzbe+6PJ:g1c¼cӒJ9 [~]d:j 7{؈GL&:5d+y~%V#8b.s2~ !!fM?| S+3*MRKo.^ hyQlhOC[S>E(>{:(lш->lbSTNϤ |WcnUƊ$s] -itu/jȆF_=V}C}¦,`7舏#@ IZ"g3?z$,єE!+4LQ?T M,,*V`( CMH~lԒQA ˧ͽfEWO/fgܞįlݧ&QH&8ԆPR3&(OVL5n({SB(DlgWb My%Mg,f"7~#ǾuՖtOab$IxdCH% oM"k'htYSa{[F9,3 _@x)2U0"6 CT11JAL`PP{~(VCFTTZ gHSb|FJ|T1z1Y99 |e.ހ(|<-K +ȬXQRq_Ȏ~1dvf:nTo Y{G8/dV.}ܻX1-eo`P{L$w)TKWKnAD63DѺoϪXw .r>;i>AgLZqWK8%j=^CG}sQwjB!N͝p7bңWˠe=w!4qr#$b''K6(]z*<欅/J3崿%M:+ ۺL?g4Ig[Ω:oKaA~O˜|A-˰y,+K rjPO yBm:E!=Y65e {TIJ;?gFrIN|ߣf dIy> tws؇;EGtkqC byקL1{Lb(DPM#V\F$vq -H^˶ZJD)wY<%i͞2ug狓]˨L_sKIq;r{dgo~0y]WȻiXB~i}æRII$>VE4D7TOpnU,D<|w w]rw>amfE=ލ⵬xkPs$yHR=on<`D+ϰNPQ*G N=mX61wëVW{*n5RYWh:> }>\}>:P4/KõƿSZ4yv;Iߞ<>4oV65se=lH[z,o/VX+WTjҞ,<<]N?+;LQg仈LWVkgG^g3rUUC'תU~yآ7 =H-}.o3TbçRycHzC؈Vhbvl2n Ā$qMG%9}pضոq:)*?9e$BnjbY6LX11TؽœT\4XhE7I٧ y RbfaoG@UrjKC_NV5 ۆpQ pFV619jl_wk@}eҞ'by"Ud,/C>MDհ~#<ˠ|uL_9mbZmE4\?,*@PB E[,JCp9 T_=:95bBt DX~߻w*&i}$ RrшcT]HqF#5& f#^#dXvۮe U1> ijV~W*v;UݯSѬ5%ս^-'p*]%xTKnB)VF3G1~<_1FЈmn{  kh.qԪm]|c U>CV'R;d !Or%&ʪ~z!۽tJ 6_Ź74kjOXB6JCj xV6эvJ;=%׿vYT#)EM.B}5v e,T_r6+֯lFŦ> _;#O'7ծ6iURR߳:=Jt4ӷ|DTS%,b9}m}VjCpeCuU)d\$(i?.ĵ]B*P)֝cMv Ƿowfl`;&w78j0e߿PҙR#ӟ>xso&y !d':] x0Z)=lPMEѷ2vs`.qޓCQ1(965,:nIg Bo3:"Ο% 0϶-ae.o24殓v818X9Б# }6 nDHF[#DaLV}x_ ?ZS>ӣfД݈CqG(Ql_d:'n+An.v '-$\8{M~W a_~an( O9m:w-7:QP3ۤ-.n+}"-!XMt/aܹ3lgb~Q!T|k[,rxxq,trz^@UTL*?\0䭟 ZQwgM9tj.uj: ,x8#J%-~imIjǃ<ݕk!ov+eňOioj#OrxE]N5]mm4[Ʒӆ~X${*۷b.W_ng?S=-_JwUo|F&%Q%ul^}JLQӴwq!e eskvgXLMP onp7ɶ)dQM9|r %_HX{,C"ė>A& -zlmBܰE9&wvS/1=N5x[鉉n؆$FMa+)X Fo8G{L97t:6O{gwٶ{jN4`n-U.&pZ#5p،^Ng<*GI X~qufa4k3P|H^TߕQ_j]j7p/t}JE0Ю -% 瞈p\qwף\(q(ŌOshU%e[!v`#)iWd|{XzG~4pW?-.@u>({Xe#H ^աy4REG<@7k;=i隫(W%g lA'8iн|o/+ǴkHVY 3´uK~{p̉o t!fF΀'&>XK}yf;T!`,ԋZr1:bK~=<ҬBYx߸XbCV',=֬ՔU)Oj7NLvHZH |]y@^7qzz{)Gq't̮wc\B \kUt+h"kk@uz):B$kWViXd (Hp4=.e]W'zf3[K>~ OˠBZmCR(7e1FƸz,g"JY. `L,zj5Zt$q4B[s9'p%1f<)u^d6OO%Ԛ]rb֏LŖZ7O Ė8ȫu0ʶg6TK{QWM vԸ- P!m{zt_USˉS9辐(o];G+v~HxCr#!.ݓ5޷Uz54:!8.|z۪TP;HRA}d!e^ucẎb/PԲ7wjz7O;rʮ%gyr=^/g+9J*y %qQ9jbOQvb#bZ1DZF>Tb65F0ouI$”ڞZ<}a"彪%3i0R@: ã'L4ZV=rUeǂ73U)fLT!>c[>AۖB'Ȼ\AᛵR|T&}FC 9LRќF:'KQ;\mq+ҥڏF$E5sqruBDGRs&lH>iH=hjG ;Qu}ņ gugLˏջ1'Pzlm\jb2I&'0h4(f3mӧYheuT5)~jnVl|)wjb1}M)?B\@H*s;2(S;Qo+T}GƔ>oCI$,b*k ^o@QLl1ٍB 1;hVVuX/8Si5+OpӪGɣU{(rBH`N*VI'?^8b]h=R>J3wP73ƕmtGP$17*oZZS$P~@;FyIﺯS2LP Zwuj=2yBh~"[b X^^? ~lsɳ6[O 'p<:1ױHଃ, 5nyZ_]deמh R.sht0;ВP?S12!ۢ}uxt"% Yb+-hLvpcWoկR gͅ-(khjVLwHYA]1zbYg +la:=Uxu@&Ťl-O2"YzMa|18-ՠE2zNy ! 6*!#/oc.%7R`A:AG1T`8q#g+)`KوD<`Z> jA_okٸRvQmtB{j89&4i.=ۛ{ qr}!MJȟ+3X[E: χyқ^a@N Hn`ơHl Z#uHRlwz R!/Ba5Zs*KxtM} z5_,%e])Y{A <اQ ߠ(p(1Dr/0!!ir}a#=5L{gw'QBT?=Θ ČlEA$S,QloȿO3HTWGcfk$%WS=|So):zڳP_9!5{Lt2845VmT IO!L׿f 3j9h7W@WD4Hm[#YA>c2~ t@&n_>I@nz1Ў]׾>wqs*2&>T ̀p@k'v#ŌB8&mȂP ק!wcT޷C'r*^ԑh PZ%]]}9Y038.ܙF JRϧj2GIj8&V=Nr5kwwc*lfCto?}3)UJa$N6QSvP5ĮmLV7\CH5ƨ]SYZZv}}NuZ14QL>X8$ ݲ]CB$<|Is}Oj%Yk߈䤅&G٠RA:]kvd%Ut}0dڧ ˮZO-G#}{<G:x`++nD<Ѽ HE}2:umD#c*W<qex64 IN{9Tk[& )CuY:`A;VF[K+Gy;`C8&0o, {(W}&<] !8cse 661~zfz։;bkJTR]9 ؜f`Z?Y6jM݂+muR:ӻnߞߵ`k]](HUЎ!e{5tegCIjI\o&687酡bx[[xzmjѳZUf8L'+[/r8OUKv+Rɽ5uOb7WKicclحk(58ym`agjJrp\,MV(A׫iFbue-HQ{ 'wmT8sщF:_N j$9M`XYGo t ˇm SoH8=QxІx$'ٞǗae]iv-[˖?gQZҪ |P&Oޖ7.} ܶɤ[Z8Z\5ԋy<Fuz8F py00סia4TA.qii{$u]פ𞉞]>-^}~=h:^&t.~=պT4< ڠ!Nk6­5 2M׵|Nf(ϵHmo&2]җ(#6#U_Ii@6nMxU nG9YVezF֓\^yp&2=_^{qnHS]gSm.[AiJlv)v@44P|GNoZ[~MN0Fo-y9YI&%(ǵ젣:gmmV_?MU x;T9nk-2*x6^:ykǡΩ7'|6VfKwvKɹ*ze@kbIOk4ONd|EF'Va{X/S{-BlA _[uN ϚJӫ=>?T;Yd)|~2/i1.ǔDSeQ%2%r^ QS|q=*Vh͖EkM$k;iƢ=ή'e I^Ti4ʈݝ_Z]w]pJ-^z3ul9*w4ت :Ս"TVhPVTot#2G ?V̥NR3k[*zgê3"~U*2vPuQ}u@q ʵ1YDAl*<:mx~q/*KW:iVztDWMJ7߰~8fsgݦІxq>/};R8L3س{D(~ cbf6qLҨ4]q\۹Xz77V๾q^A[pW;2k dr3^x=徛Uw++:^m7 K[ S]OE8:Y9b$Yj'`&\L-ۮEU7 N& w[WOkqofBx_^TQnfb:}ͯif!4Uh/J=!x! T?;Cަf#ߎ9#;3h:{J\MTF^߹j 84VxHuetF:\<4܏zg{;/ HRd.v@Zpqb|Lr+$Ŭ8- 9TҼ6o˭3VovjUe7=q0$J~/BF|2>]ϧ ƄT#Z$mЛ{w膮x2QcWD瓧8u#A0j5T,L0lZt7I6&'puՔ5 uTL@*#{ěGlҪnS KH˪)ՎH۬* g7ς}ّܮGQZ[-jy}m#]M0F Oxɮf5z74Q)T1 ;gU)7*5G3"`bylj+qػLURWQ[\!y:W qbR1Z4n<{rnp~mMU6ոɸo;U 87 1`Ks{Qטyw[}0_!yWNx_TN;)GWQmjqرNZ֏K!TtjIoՕqbܜfOw{آZv6z+մHʃJ–L}/ϻm[Kx?{żcESҞg߽il2;]th+u/\Z ,o_ʚLmVàީk^5P u8ծ|97bvt6ƹ$?<7LJnqH/q%`g-5|ԎZWfw2⫆^M9wFjmmnW+ֹny5Hb,Gh*"c%0N 08[LGl0̓D Ƈٻ;iFl9&Ό\kK/Jg5b{m%[Y zr5+z&S7t3޹ |o"֫tvհ)6mggPJ.s2"~o"l n-0,V!ݩWT9_UZZ;UB鯬%Vvhe.X4F{Qfɠ$]mzgz\gQ7+pu iZk4UTmIU}[A<]Ei\i) q/>?'̑]W#0̵ j_ pIh44yY⥏T[z 秅FzITI ɭ-\˓ic%Z^&):jI6HpZx^I6K/7p\j$ͳi|g޶TN#[q ,b+kL->'V>(sZH"ig15N,llᾒ_&.~k_v4+^Fj1Odc֞Ԗݛ܂27CAܓgʨhhP/Q_l);W&{WJՍ= 1;c3SaCzA4Lf5}Xڟ6^@؁O^,ǰlWh*ռNIvtt]}wY+ y+4Rup(Nq+ZԂ8>*~b*bނ͎m;.rJ.Qi 996MPl5Tcu4X/xz'0v7g>ڹxHsU ְ,sߥ/i%e/Xtܳl٥zGةapf\Rkm|q r`8_p24[':1/>kpF:'nQޘaϏnF+%do12#Ulv Wb*yciF6oWN+i3X;tW3%Q[q#hV9ƃ^-# k]u?L蠕CCWy8 fJ4dz40yX92|YH]׃'*=Hs4Ѽ7nw GKf5_;" +ˀGWjؾ,ZJ?Qw&JB:fkmi$mjåanqגGYYB;嗶(z7.~TXD@FLl9sZyyu_Fedl7὿o\/46NZ:}-={'/fvor$o%\"t+zYqxl2/:"M W}`B7?KC^(QAd?reF$]`Z8;]k N5>28?iSuiUȳ:6dzpįRbxj5ss!8*$k)ApL{]tt1fK7Κ^_y1>Mw<--ZPo=?]ET鞸U:lj̛Orx.Wǻt7r XFkӀؔbickyUZc$ލ v1ʵn{iB-G~ƳgȽo'ekN!yW@JxTZmw݂v_8 q "/c%$d4ՄO[O{vUiy̪lMD;cVõDΏ.AʸvJzdfCvFvkd7V_u3J ̥۵4uN䆖o5F˸eD7;]jEʗ;+yHhˎ]Zb6ٮzi-ikX{},uRP1:T92"tcB3'&6Ϯ#%62WD1k" χ"3HQ xs6m|=]C' JwMVz,AY! n;o;U^E_a7m.?QnDG竍5T%0 / SFm먭8lRiNIe2IG>y?榨DG]Œ=boF:sO?R6n+:OztQY[۠e^[~ԺPX4Esb/q(^z>*fo?6oH `U;/(p"= #xd+'ϻx;  N.kL_4JL> X#?"UfOgрgy`)]N$1`R(BCH8!b6F%C($V PRr*R1(09 @(𙩚d*|oUA5&vk_h7hGKD?~~mn=$@h>( qa>TQ|0 *||2F>ё΀| O-m5Ę#PcSa !6Gg\@0a\ x|E_s %+}1I<>kXaTE>v 1꧊gA`}3-9pJ_!ߟwixrM2h JpV~/y*|DP7` =ࠌ1P!3JO? ฟRư _~{.~29g<YP:6Dq$eMNNaa mosϖ`4Wv>1c}K*?_ JA"p@ ?V 6`5A%|ƨ Mx*GBs\>\) yDrA&2kpY#tBEWIB R 9HH`r QfR^n|Fk+n@2 l R1f<=l8HM*˃,G5M.Qn eɔ2b 5Mt%qX./[,#pox/Cp/ٱ#2F k_iӕ8z5k c4i 4*/E}paPVE} }A .P1ᮩO=qae.r5qU0(ƃ=R}*4@,f! VMy ~FCGHppT,3@n*zZ_HXw~)WQ`GWg߷.~U_)7CtcHTJO$mIsF0-9rT>4&JA^5N/Uz E·W|hJK6!ВT*l #b"SNBTaYh+)CzA燞<? ֫Ŧ2_ x#L>ae_ɟ0q.hv*kZ)kO9p y#T,? (bp>+=_qM(Y ~bf?ӿX8E#pG¹IH%P¸_4{ [OUO&*E`RM~ "*C+|`cO~ُ՟g?Db_Yb Kw!{dߟ #3R/ZM^5POX20Bi}P{)r*hqU@[1%|X.@$"̩lT4Jlp:5C} n  C෿>H5)O8"=9=ρMoA@o0֡ɟ4hG~ǐ0i%v{*p/ ad˿UNho(3<`7[TA4!x V)![ N?ju/ y/ʾ߇eŷG!X|c\|94yyԕ g9o!]&k؇e_yY!ۯޱ93 .nB_gM 5ņJ|Af,C_|)PbAix?""@@eU4"A" Xr0gBq04אLDĒ!UB)k ,n*#,ADsPA4F8Y&;5]/Q}@)>7y~Y9C5hBdTe20\HF¿6_Ga J=h>}&K_]7\"^՗!)."gTQ/|)>zVHTW8 ^z,kU5K/(UWG>%)+Q?_&g7,#I(~k_ > $ ߖ׏DoMdY4;۞8 @WqB~ yvcVxJEV֗"+Ia]G}Y^$RK|KI#~uq)4?8Uؿs#A\gd>OیjIo`(A˙+bj$nŸOz$Qo&*߷ЇR{4_##;b8qq`:Nlj&Z_ ʧןimlX$HuQb @-Op H"ݦ[)ޘ c-#0g!4Av?8?}8SQw)4~Z("8`6+b$_W,Ϧ|I0p,GEP(J$8uN4A#)8I&62,@hg v ُS~\ 8މo-~BGǒP0Mb@U Λ0íh=r[ pMzxE$Hֈ--EY mbF?~?gP,yw˪ީ(h6&S?xG!sT)⛂XhɟFsj ݒ6I &,oKPDAwPz'76mDG1o3b'`z 8[n$0 |) ؤ9c'R?LoX z,9ՙ٢Q@$6Lo)`|η'9TC7 b_PωJ2E7&9 #ǖ [?& A1w]:}>y{( Y (Cdo΀¹}ϋI#YZZE*Q28p,m[7la0Jq[ (a O[도o 9`8nK\ !ő:0u0pM0M36p#h 8 ɯ%<(Bn(3Q(5?xD?FǢ #/鷴z'h`M_P%ty|/P[_E,mX~W]޾`nж;?lr1ae%gP#piO`d7~l#?_GRA{qd?}%7"뗹~뗹~뗹~뗹.GGVDVɕ-K4F^(Hf{tfڃn(4pƏbE >愾~+c>@^LS%Y0w/deOhF@T(?@葁T 뉪{rv ɽl*v 4*DgWL.]Z^8צrZ$˟}k"=;uJ3I۵v'WJ⺍\> ;9Oej1iW}=۸kK=>_^U c.#qAck_b,?(2|'4 p g,\ ű9Õ! :eG#Ŵ]`LqnG9ጡ4ҀT Qe+l_+8W&ncѥQ3/j^|voZ8[4;׺Or+Ah΄&3q8Blau潤uY-VE4!%aYq&'G0+UuG3(NrX}_ЩjNZgʽ w7V,#Я vFlFEc L^Ҩ腱 Lfk ?|U0]yeWO0myIs4 'nC[LfZY{-X~{a}o4x[dI+vW˥Ѣ+^JHX u/^F&٦^ VmA1M4ru4_9nׄy&xB_xaP^n;q,MJwZb#"+7#Z"sL,c@p&y5mDG(\ʰ.uf]Κ E[#K2g wn58˼TK͆u4< 6w5[^{+_IK]KG)-p tTԼKƘ2ks5P&/[B*j:Ne#Se$t]0$Um (N`F{ՏS 9pĴgƏʛAa;%-7K/meܳ?+"sA)kiv9aNyL<HXm;lppcTόtn)׫6jg;Gr"(JHyE!6k LЁ혠7x _p4T0wۯ.YFw9 vh:K8*CQs2<$Իc\6]y/4z\MKB4`5q#v?CN9Zt.Y3<겎B[dEZvP}e<%1lؙX.q`Փ(!4|\l^2qrden=e:ejG^)qaҏHgݳP,ޒP+Z#S ypdhc1Z$b,,1q('ϒ3>rFt~7駏y>zd\D;WlLZ%]#V7O/̆ a8M6Q}ŠHk*2ps (+gY/Z/J/_}nZpbmOr6[Nސ'$뢋޹;ݫaSc6$mC GIyϵ!|ȵ`COM;%8?hNfS]D"mߋTOP1*aRu2iY{W#PD6apk ;'+rt(8@l(qf3;,$4M,{AH^,(S7JF\6;N:熘$[sۦ í4/ ,^0>v`t j5 m =T)-C`9 |ʡ4F>ykYeO_\4xj 1w#:7u my; A~6^kш(I7Vd}2Bs =\K[9YOu7XXgu4wA׵@g^Gq)%nڱke&Inl(ר"ΛܱCTS>݌qTmpۗ0Ui 0=ųM؄.Y~-k(: b3$/x-UZD#&@)X[h, oc!5X sUBuEËsbryM,Fb=3W#Pg2>k}-s0VtrBBW6m v#kt&.Y4~I~ѣY5 yh A/vPB׶$܅wx?jGظ6Ef4Smd\˱l|O):EEFZv&)X%>rA3Γ-I!R& >7c(CRKzΝw\ 19CKШnrSh,4$qi.!9-#lCf f%n|_w ΢ڈúX|<8(52W% w%Dll{SCn5aUWђ5$?sJy?D[Օ\J\7W:*qek@lk7Lp5필 Twj*˄:qj5>ujt5n<4pk3<IW  ,Qtd~uU&_ZE6ptDd~1xW~=e A7A3medLqB>=%d6:<4G[yBsb=p#eJL,vb mF>vb0lpdu,V8uExZ!X,`--r}e|cdBİ|'LcP<)u$NMVDȉNw9:;]KLOZ/f){{֒>y=3!(!"]SRА";:nּcfDw;`l0ܶudݗP.Wl*`-w|23Ÿs} nh$Z*CSSǼ^YixI ؞^07$L{?wuUTx&%ӉGAո=Km\xy]ks ;>% ubE,$Smfu-a$Wr_VsFh?ncܝiEn_a9ЦgdQQB}1?vzNv'N+ TUmit^Gq|^ XJz 5O9ԢkDm#hn[U{{D Sn%{kG_F5B:gچ,?W2j#ty]?W%mMFYm'gfJ]1eHHTvqj2Sɒ:׭b ^ЬjN9cyVQ~>('Fd(P(Oxccԡ%4+acH'?qf(dY28Sk=hXНzl@ 0*PSiY^ct`ҷ^W/y@gDBLBIcJCY]a`Jdg|AS<+rQ+ @<Dn)} Y+M9Q,PtU+Ɔ'r0O"! ?H0J>=nQx:U8ѼVVddg|<&=|Nvg݂nڍHP v`s ˜X2C3|jec9M9XM7TIQ|:6I%ݤC :}ۥ=>8_֞aL *",=aZN[ϠME]\k۹qj~cM9S4(vJ#Uķ\̄ 5YG@3ϟ2YboNRC}:4餚2'kQ'JVsdjح䪏L65+XIDNЗ;uiFBe6FV2.|WP/%x_E& [/r86pu /gy8D!:?!K-zP !BV+iѥw#:,fvN|_euy"訪Uh' RJ(8(EÆuu@w|NL8LKa\%a{4:5M/h6$Yȋ*̎=:qȫT:z2e6=[h CJjZ˘h-B/@`҅r0kuAv>' q@ťqj#dmt_葾LghY6gk.2C!- e.Ƙ)nMgz#;yU xW"L r",v=hˎX%zeuX`}ŷYjǴQghTF:ˣթG[u@Eo_zDfv7Et7\_%cO478;4ӹ0'G ]MØ~YbR ~ڧҬM@1rY\55/2ty̭0B50'?ų2յG|rWcog`BN©^HT㱇1:nR:DdwQYx0GSO&;GQR)NȒ7@d&48edP.: \pl%>} =:D0,e%}prJ[X$4 v-|8|LWuIW;:\ָ%@d-#t>y <:tfրFǨuSm]I,Zقœ y(HFr%Js#4;TU2(';nb@ojL#Š A| K/glz.Y2+-0R)e`rL/@~P{jk^|t!'Je{oOpV_라|C`!>ф {T+13>4CݧC)J(ّ3U(1t΅'~OiT_6-ꀌіu͙c)]F.? ^#ThJrj˚`Lr?`՘m3,QXQh;_Nv?Ym,=K0v>ܒo}ۂH3R/~ۦUšd87kjLafS}Q]4}>M̔={H[k!1W%32:MgG~!Y_u32ͺ2NώoN+^$Qfge6=q.r_eMx^9t P{6 *#ydU͘$lm̼xf d͋ n شm4ףP)b'Tdt*ZiFvs ^V}[(ko?iԣBXʥv(>;+p5LrSlhBd HܯՐO*ꑦLۈjUMD]|k7dT:Aͻ3@EG&1>Y .z_Sf߳*>9Zw U/!ز?jDH qw]qbq!+r{ޅ; p˛7#DXl4t̢,EL}ٿxg Gۅ#z98^ӡT?*7oܴMF%;cߚzۄ)iߚwVo"'oIE@EbcrC=ޭaz``"\?z#/7DesU!AVmUF>@ܯxǪlѽރ'eR\oMfbQB)Dh#uNF#bWRAa, c[`ku^ Oi8`oHnyKyHep:|rJKD؝qh.hj*hπ}aYUICWLr@ FL |-ΰ`/ ٍҏ@15wR!BK8<)vߑ4e1 Fث@"DGwsm 4749V^75v b[w!g*L@RŬEtܡ0u%l [{v. PϦ-YFSto?J"ìǤz6-?JGTrAkjs7l#V0:6mYxLfTY)!ot~{/t4).>$4?C%Kldb L8,/U _ :^(\6|cZcSY 7Ԣ/>.ȊG+Ϝ:XϖBP"(<Ϡq߄gUodthTL~:Hp t\ -uyyM^tV7 L[R9c)Jjhm2! B+?B)4NF=vb84UH.,}P7$n'͞EvˤͤwlVZ_|St _,wi%p`m,,q'(}rifS>_7r=)QeJE+/|0QO mFV2AdCoxt- ' NB݇촇B-=LZw߂5j.緮kz=eIb.nZF\d9P$ݶK?Q'!m)<'ag! Mw**_HϲRx\-ʵgh;Cl5ğ4i~:V,|#i+2FMY;)od= ~+܅P1%S]tiD{T4ԎտӃC29> gɒf5$՜|i 3α7-kj\B|Gv-g pUo)ZhSCED$AL˚:@ȧ1a"N'vfL63 _jxu,+'\xgaU9Ѓh6qZ)BYBޡz9T3T嵖wp _%-k|^k+u{iԨaHi_ R?___!KJ|-6TaMfA۫vG{cDr-`W LK==>Bwt)1\}pSHlIx5s*h"Q"΋UCW&c`~UtJ Hā=wh!S{c=- s\&t^uu?>D_ W?_V ~Z@q;$0-qa+r]a::c߷Fu$UY6l{kQڶ69N^]C;}ia|!KKQRnՁ҉l jCxqL>N3qr$IWUtvK|IᙒRv{@pV5Ib$jvd6syJDzL[Vt^\R׸C/׹aph-!|ҧ43/I΄Wu(|k=Bx2L"}M._I|eO"qV͛ʼi'}˴%3xz55&4ACw ThOKE1A-aQ΄Tk/&-ꭚළmaVvzneM$pRoM1o&߿ga6%}'.1 4&#!үM[I~@T!^s:ߺJ>_ʛ<[)e[QfnjQ*+cc׷&كI4ڏ-@)\U@ǏN9sm,7jgHztdܙJND Dֱ&RY/A>D?(Ѩy'|v`_# =\ZJBd/eiq.kz lк{$QWZ &'_-WoKR1E^-п'XHVxYJw^4h`@oj7Grjm,H9g@iŖAC5}O0)@KShgNm7uC?~ĽseL XAIb-dFI3NdtX&mm>FwW%:̐d XvCO3&Ņň;zdc~WxZ(A*}ڨ%DpRǨ6i!+n_ÃԈx**grj|CvMs/ש%71y3zc~%6xv oٞ:~u6? c^!]~3.^É}h[D=zuf@\K@,1qC[:U aGء:#@i#vmUYcJ2eϤP%nrUګuv'K53/.JC&u ?wb,8,43BxfuIiC^lB7[H#9 xB"[H0yjf L.P>Z}ruYm0RuE, qpqeCȄG Tal"cKnkZhF`7:>_Vi)dч̷@:zto1!3$(r=s5$! |o+,aP ۄ5_mnRCuqLJyT LMqWj.}񝾝Axgjy󬈥ctl.w).IޢhpĐ7P/`J3(0lGHh;l&b0j#n=sK+nnBuG[{-tvy>h"+ 崻ItY* ]0%-j+bոԽ-RAS"`MZrgyn&I:4Z_$-ؼNTKm1X^Yo^Ġlwz "L!LWXi&]zy-N;3WL"5lV'PV%EjS86v^mXrm67C 65ث9=ɐNx]Fby9&t{ѡ.[θcbNlFGz\ XQ?}9f?e_tU-g\B:keyVzu3"]D? ?Z~|\vzh[n6>]SC:?g'4Y:W1s+Vlspp0G6ZWfo:D2[9RƤ#[-%́OK^"?Oe՗W?D.=mA]}2nVOL~<p+X8Kņ얧=6]ZDm6@WQhUP !'F. δ/9oZ^Pi[.Rڑ=lǦfJ|5vZV׶RڦmoU2BRqd=?Ԉ!|INՀ lp! dKe {OwYC9:`^׿-m~_sz?3s3cפ Ҡ - eGOwz]w3E&Y`XH>q )țǙv(|JLs@lgBJz<*CNhϙn 階ʂ,.upM#K L/)Lsя"OF*%V("w3 klna$s|\"I fH}RO`#>R6OXTG/"H0.Ll%* YVmC8Pb2 <4Ta= /NҸ=g$vI4Fi<~w}L'uSݘO6'=| # T#%4žuht@xX#]:j1$2hy54g9 L `a[oͦS%M rpَbYcp#'p!%?؛0z?vFa7O%Y<; ,.'7ȱ Ҵr1LLVnݔItn 9r@I/QW0EUЋ+NuZ!'jI09[A;o "h |,W͝ks%Qn“^%Ӽ7~HUB}S^ɭF`x@6X0ZLII{e@{  <A*CL: tʄ` OdQ^}RPۦyswgοDȝ&{Plg T_9bv3w)MXUȿ=A$.vWKybjz0pP54DY_Oj>0eM%CM1LgH*6j o].=YYXC(΅K+﫮&=R5X`t)ڟ)S#^j $_dtze,ڀ>U#ny%]٤]l!p:Pp!ix*27~w"j8CJTMcm̪kjuVrI֌ `Rʬ5̝mto~RS +d(D&nB&$E;\*f{A̅'輒ŕm03)LcRԳ𣍛3i&4F;MMr9I>֐ ^nA:4{)4V1bkoW3;pALlt `6`W3=3A3Thd"ALWӑSzSV%SÝ{']T~.NHh ܠN(b^vJ|&Zv0۔AcE;e/>S;$Jk/yPA DTNZPc0s~*4'H*. ԧ~NQ\0ًR _ ɻh^`QzI΃ l%?j~"7ˏfZpf`<* >6L-U)]%>C%DjIYmH'cUd*#}?&Pm&%ɬlE{EOOzO\4&Rt -}5]d3:P\D;ùtS%H~ r@\\-ޗ"d.Lw"0ֹ6h@kj$#y2#H_-{z wk8!-1r8t3>i$W!L?|68ȔL4=-Ry4E/b1/R߷qJ6Be@z]Θ-vj 9o;&^;U,VaםW-"nCu L;zN)s&T;Û ш:obFGpN7Rhhn&%7OkT|Rr=:oC!Jkxk}_(H%y.FFN̆L[b8A~El{~ 8JFvˊLZCT腧_#B? yo KA#o4~7_29gѰH2.g׻Yިoz3$x@%9REzl7tC (Ă&5;jk k_ J>rVn |qrKr b|R;#Iǧvjp~ѥBJ+ب$K!īi$9 RɷAߏ'٫'vWF<(L`C@ݖuC/u_ v)w\KV^VýTtu1 埜}}Nq&cwr=ב/*dMQݣ!$M@kVKo Kù,#KCp|QC&@L\)I> TX>_/4v.۞CJCy^7}W6G&~?PR3dhYQZCZ! WO|z}AgΔ-e?v.d@>6F\@ᡊ +󷠡ǴFWRP@97FN  &Px `Fv>FCzc4}G5bfG%|СC7EC_Ԅ -7vlQM^ȮhJM2*<~K^<0w.]<& jYGK9|7HJnHk&N~tT8 UP~o|,,|?\cgn_s?NQ i$ty S՝x'l.5h"'98X8%X;zV߀NqF_a5k,rfYf} buD5Fz̛9i4n@-w 6>JT]ON[׍+v;%n;n 9ɺC LA+j:D#߮(9, };m#&\RU1NxK> M<QƪߢQ.e4ӡ(W7 DMߨ˛ܖ^juE~yw!2B 3ޓyk`=A Qp>EA_pwh7׆PU*|x\}@ss6X\:RI32$  f#g}O) (HTr5@!-g,o3zAD0 j6Op׻wΕʔ!N΀.eIL'S?L<}dҝ?^r#xޤ%܏d7&̾HMSZ1CٮB&?ՠ\3FfV_j =GɦDALt:LLJ/e7'&v_Ͼi/"BPE^ь WWTVk'b E<YL÷7 d9>~,U1BȤ6[wUq|g PDvt@,/[WKe|l>Fg9A˔z ._{Wڜ8lb>}71feB7/K,-7xmwJ*USR*_bTYuAErL'yb&8FzTRX7^ W 5潴- zuC$u'Z.ò*HcWoli?ώ}!.֍ͯҕռs;x+?R$WlA=ZU8W,\GcLcѬLf̦8Kmպƪ{ Nf@q.F6xP&u[]CLLs*e[Go WYk ڹ^CNz8L5n'Uq!KH&*^7};AzX3ΉDdA4+wn.ҩfBx"F XYGwcy[ɂD8fANjDNrݕ.Zj2۩ ϒMjbڴ8_MSq=/!.vSi3ML̮n& -s)H&r5Ɨ :I<|)vhBnZuKq-!Dkګ *Ivo]<ĶegN\q8__yC_Ȕ@Vʯhp0F[.ATгh-b p1$@8G;Dbzt,m&*kN߄qg MLc+r1q}Etƈ"nbVEgIU2XE-:P +RwlOCZALUZ1+ra f*Vg#ͼ5ZV1jk"LGހ0_Jē|r+-^sZo  ?45w)TrKuNmz2fڄɭy9mpAY1zФQ#iH+i!a,|M2WA.dGDݾL۵i94)fc 6mNJUi)Ʀ$U.V9g}gw1 #|45!BjV2ߕU+jmqlxU|Lt4E"]&A<&eעZ?+Nvݫۓm(9RD-{\Ͱ;֝-,qjmEnZ5E%cn:ϦH|PB[sZ^YB0XTZd7~^fAZG-RYfz1E<ʤb|-WX/>-uy仚M]$vZ9WՐ;_Ђ6:ZSU!Jl5G!LVnb Z\]^A^m@&4/aĪ36LqDļP}g]¶dF~,Iz G7Qkj)\c̹THiQZq^]†ZR\ WR7풛%I9㴸&Qo2ƕ뛤'%^n̒he^t[Hq!,Y EF2vrh^,F3-"ж*$Wz"M}!QI/8z|EzʓA}ew>5 uoz(6X7r=8ӟ^2j-$S Z캡kVfvh ["E%x;]nr\qM2O=;=HXmZX|Ձ~27Sc<].VɪyuwemCmk(SvaϪeʡB/MkIf\^؍>vR+fC\E8Mk?U8gxttf1vKC=BХ v4[uzgRoQ>͚Cwُ̿HnkxW[ٚR ^}ms]pD0 !8286# %.0'6UR+ 9QB\/o _Q5E*Sm3N T=8eWǫ|,z,t.mg/KI/c'wϿ6` fb FI7pfny s-\&g٫8d9ny3L]#g1ٔR/asaj>!g{F/m"zz#.O1V6r,d+h,@?5GI2~Zs~ُVo<7g~kpαLs=6%k-Ԟ3IQz}&W|+rM>Fwp_cD$ܿ3<=]1O{̙${ly\H=OoJ<6/y=s p#%D:o?TZ~o._o7=w2ՙd{`o,xc-N߫X_n$3Ce<7p1YCyA20c6\ך6Mtlv6Ȃl.J^2:[=RmIʴ)^c麽Nwd𾭡7!# hB4r"~c3;_2qOP !tԴŔFK aRp͎[(5ЅR2J rMVTB=~vpx2\2}EZ?k\Bck>plh-?zC'_(mKB&6" OD''>!ȅAو0fjh.@ J  W6P: REO' $KPzALg( V;X 0LI*!I| &GS>` 7v` nyP@`D2A˯ ܼJ{u0 TBPi~bk%58JdOLxGouOpe#m% >G $U`DBG=AA|  v6J hbkmφҳIpi闌6lN8gL+Դ_mKD2W䞿{R PFr/r/N}Bm>b*%66eZK%6\35pFkl4ww& mDE`ɴ<$5Y1ۑ<-#AQH<όOC"ˠIM0,h|/_T,z}zBͰ``PZxa#H=S`F2!4? 3fAS^]]BPh/`;g`~nkB4Q~Mjb'>p)aP ~"~[8t]@#g c 9C+""oZ>FX!l G 8ٜ@&Z!MS(H/?V9:Ĉo &aT-.P-RO>>1GA0E|2I9POpnb-|ɏ1C)7Xw=Rݞ jc9h$x XO=poN؂P?p/Ix K}[ - w@1 &%`ŬkĤ6\PL #kb2\cnM3OJ|͓ia\]uL*%ߴ.+X`rI=o`„0Y 7LwFRү|$+#}'Wbɀ3_|+J@VW^R%; zZ1Ta fMѯF.XO~EW(_Q}EjA+xM}Z7쀹-P~J.0@X xi{~| 4P㿍.~@4I64+QO ?:iF S6RH̞P쉧 JīտB9[lR>3#eyFjb1"_}t Ŋg y)@ @>I40+fa[+hpyTGq`yT<>*ثL<fN&?ÉJS~Ɯ eso1 W^/2@:? >"w8#ğ s@pF1=*;r&TlB6th Go~,B&;8_8x?sԟ`p7QRLSLyW)a$MM>[CE1=Y|Bbԧ)Ὠr4"$SE mҽ Cڒ067ws!([;JN'1.i"m b$qq,Z{h1; {R胴`k'->a8 D%E00%'ᝠѓd&{z+2B#=$*?8Az{I:|*a+?ΚN8Nx>7F.r"#Jz9 aF<ǡQzvC)TRX;e|߄Mw5pZ„ND(T7AL*ENfZQb~,,|!;h,xG1TLi3ӠD)J5(Mo 3ֻI66EfISkhp$ZQ%Dhڰx cPA#  4J;!d}_KkGmG G]p ո6m;'kA!G wj8P7 .h6w4 ?Q<^9Sc<:Lj£;yי͎0 qJ8J!% #iV!g#sArLÒYb!9D SUeez+9DZbb SaF>PH! e|niCHCharm-1.12.0/Charm/Icons/CharmDMG.icns000066400000000000000000002345601331066577000172140ustar00rootroot00000000000000icns9pis32@JIKKLLNMQF53IJ]_rxǾ䪵صןȍkgiqebghʦ $ 72L Hcq|w xxw{rU5+! !&@JIKKLLNMQF53IڀJ]_r{xǿᨴصןɍg`_i…`Z[_ɦ $ 72L Hcq|w xw{rU5+! !@JIKKLLNMQF53IJ]_rxƾ୺صןFɍg``j…`Z\`ɧ $ 72L Hcq|w xw{rU5+! !s8mk SWWWWWWWVXPaR ~m2&I:aQzjt]O \mjkkkkkkljnYil32 %&'()*)$"- Z󿕝tblk}hm龲Pj(&]' +࣮bzd\c{f_itկ + 7 ,YߑDOˏdlijile7AjOWTU TXNn+ؑ ޺ 3{z{x'%&'()*)$"- <7/J?Z-Pjczw*ʰ ū΀ . ਘ-׮ѡ ;Ѥ.JȟЮ>Z󿕝u_gf`n龲Pj) c "[࣮bzeRZ}[Rdtկ + 7 ,YߑDOˏdlijile7AjOWTU TXNn+ؑ ޺ 3{z{x'%&'()*)$"- </J?Z+Pjc z w܀˰ ê΁ F ঢ়-׮ҡ ;Ѥ.JɟЮ>Z󿕝t_ggan龲Pj) c #࣮bzCeS[}[Rdtկ + 7 ,YߑDOˏdlijile7AjOWTU TXNn+ؑ ޺ 3{z{x'l8mk 323333333333333333331 K.bC }\ u   2G+_A xX p   -C)[> tVmfpTL4܀ &Q]]]]]]]]]]]]]]]]]]]]]]]N"it32j* $%&''&'(sف؀ڂہ܀݁ނPBقڀہ܀݂߂Ί5ہڀہ܀݁庽 { ȫL ' Ʊu 7ʱ B δ M ѷY պ%c ټ2 nЀ ݾC v  O } ޽ ¬[  κĮi ) ˽ DZu 7 Ǹ ʴ A ͵ J ߀߂ и WӺ"a  ؼ/l ܺ ܾ@ u҆  N |  ­Y  įg 'ֽ޿ԽDZs 44ɳ @ ܼ ̶I- ϸ UAȻҺ Z_ɹ ׽-jEϺ;ˀ˹۾<  tƀOͺȽL{Ԁƀ<̶ǂɾ®VͻƀP˲ƟǼİd %Ǯ6ʛƽƲqZ1ݻٿؼſȁɴ}+>ȻĿͰЀЁρЀ%ֽº˶G齿ľ5v$l6Ŀøι"S۶ý¿$һ]Ľ¾(Żս*h΀Ȁǀƀ Ȁٿ8 sIzӁҁӀ҃®TԽİai"ѼÿƲo..ηS'N$ {~{ȵ{%<۾˵fOfJ6tt{˸ jEؾȳlکv}κ P־ǰ?voѼ[Ӽî!sr Ծ(fм,}u 6 q̽uGyǼx礃ïRŻWՂű_* 6簈Ƴm+ڂ!LJȵy 91璙ʸ D췶 v}ͺM쳲  2ssιȶwa7ќrwmkT  8="^fAy$W;AQc>=ɽпř˹ nB̛uxljN  90%M8z!P<1)O4>ϟͻpKʘyzpmN#S=ZB(|(,j]:]ADՠϾnWƔuxtkeB $5VGz"FLG7SD!7eLȗҿ#cbēzwBGFCEFDAFCR[@FEDFFDBGAm˦ 0(mCĹï@* wъ İO~ ƳZ ɧ ǵi 'ւՃք؂وڃ܀ہ܈܂ɷu 4˺ @ ͼ I Ͼ U ! ` ¯. k İ=  u űM } ñW ljȊɅʆˈ̂̈́Α įe %ӹo * Ů R ϸ\  R ð [ ű' e Ƴ4 r ȴF z ɶR  ѷ_ Ƴm*z <уπρ΃Ȓ Q˄ύΰϋ΀̂ǟT˄ɍʰɍ΄ɜF΅̓Ǐ 6΅ΪЁ˃}(̈́wFBACḰq́~=QOPOO?dāONLIJKHPHg̀ƾWÁжLKMQRQRPNG]ʂƹL ʀVS]`e]`Jpĵ?zՙXy|~|rYİ0kˁԣz{{|{z~ɀé"Z̃ŧΙ˃ĝ/Ɂɀzq˂Ɂʱ)ǂɂˁɂ̷Bhʀˑʫ˒ʀÕ5 *eЭ }I $%&''&&'&'(sف؀ڂہ܀݁ނPBقڀہ܀݂߂Ί5ہۂ܀݁庽 { ȫL ' Ʊu 7ʱ B δ M ѷY պ%c ټ2 nЀ ݾC v  O } ޽ ¬[  ͺĮi ) ˽ DZu 7 Ƿ ʴ A ͵ J ߀߂ и WӺ"a  ؼ/l  ܾ@ u҅ ߀ N |  ­Y  įg 'ہDZs 42ɳ @ " ̶I2 ϸ U0ʽ¿Һ _À 7ɻ ׽-jEҽ;ˀ˽ ۾<  tͺ8ȾL{ԀD˷ǀɾ ®Viϼ˲Ɵƾİd %ǮʛŀĀƾƲqf1߼ÿùؼžɴ}>ʼĿĀ ζЁςЁρЁ ־€¼˶)G龿þ5v*l5ÿ¸ιSܸüһ]Ľ¾(żս*h΀%ǀ́ٿ8 s IzӀҁҀ рҀ Ҁ®T%ӽİa,"Ѽ.Ʋo2.ιHx`sbB3{~{ȵ{;<ܿ˶]Js}r]F+tt|˸ EؾȳKm۩v}κP־ǰ?voѼ [ӽ¯*sr Ծ(fм  |u 6 q̽uGyǼ,x礂ïRŻJxւű_' +诈Ƴm+ڂ'/LJȵy 9_ٌ葚ʸ D췶 t5ͺ1M쳱2ssTмz]мY鲰Kb記ӿ&'d宮;tؤ3m p㬫|֥®Dcxީ{ģįQoڢj Emۯű\mءpvjp[  y  ;ĴǴkm)ՠtxonW   |    =κȶwa7ѝrxmkT{#<ʽпƙ˹ nB͛uxljN |" :ПͻpKʘyzplM!"##},"" @ՠϾnWƕuytleB {#  7fMȗҿ#*bēzwBGEEFCR[AHDF'EFHAl˧ 0(m 'Ĺ ï@" wҋ= İO$~;ƳZ ɨ  ǵi 'ւՃֆׁ؃ىڂۀ܀ہ܈܂ɷu 4˺ @ ͼ I Ͼ U ! ` ¯. k İ=  u űM } ñW ljȊɅʆˈ̂̈́Α įe %ӹo * Ů R ϸ\  R ð [ ű' e Ƴ3 r ȴF z ɶR  ѷ^ Ƴm*z <уπρ΃Ȓ Q˄ύΰϋ΀̂ǟT˄ɍʰɍ΄ɜF΅̓Ǐ 6΅ΪЁ˃}(̈́wFBACḰq́~=QOPOO?dāONLIJKHPHg̀ƾWÁжLKMQRQRPNG]ʂƹL ʀVS]`e]`Jpĵ?zՙXy|~|rYİ0kˁԣz{{|{z~ɀé"Z̃ŧΙ˃ĝ/Ɂɀzq˂Ɂʱ)ǂɂˁɂ̷Bhʀˑʫ˒ʀÕ5 *eЭ }I $%&''&'(sف؀ڂہ܀݁ނPBقڀہ܀݂߁Ί5ہۂ܀݁庽 { ȫL ' Ʊu 7ʱ B δ M ѷY պ%c ټ2 nЁ ݾC v  O } ޽ ¬[  κĮi ) ˽ DZu 7 Ǹ ʴ A ͵ J ߀߂ и WӺ"a  ؼ/l  ܾ@ u҅  N |  ߀ ­Y  įg '܀DZs 4 ɳ @ ! ̶II ϸ UK˿Һ  _Lɼ ׼-j\ӽ;˿۾<  tͺ8L {N˶ɿ ®Vнŀʲ8Ɵƿİd %ǯ6ʚžƲq$1߽¿̀>ؼžɴ}>˼ĿĀπЁρрЀ ֽ¼˶ Gþ5w$l6ÿùιSܸý һ]ý¾żս*hˀ̀ȀƀȀɁ̀ٿ8 sIzӀӃр рЅ҃҂®T,Խ$İa,"Ѽ(ÿƲo0.ιJƵȰE5{|ȵ{%<ۿ˶_L_I@tt|˸ jEؿȳlکv}κ:P־ǰ,voѼ)[Ӽ®%sr Ծ(fн(|u 6 q̽uGyǼ.x礃ïRŻ9zՂű_% 诈Ƴm+ڂLJȵy 9Jَ璚ʸ D춶u5ͺM쳲2ssUлz]мY鲰Tc稘ӿ&d宮Luؤ3mp㬪|֥®Dcxި{ĢįQ-ڢi> Emگű\mءpvjp[  y  <ĵǴkP)ՠtxonW | >͹ȶw:7ѝqxllT{##=ʽпƙ˹ nB̛uxljN |":ϟͻpKʘ~yyplN #"$~,"# @֡ϾnWǕtytleC{# 7eLȘҿ#)bĒ{xBGEFCR[BHDFEFGAl˧07m4Ĺï@> wы" İO ~+ƳZ ɧ ǵi 'ւՂևׁ؃نڄ܀ۀ܉܂ɷu 4˺ @ ͼ I Ͼ U ! ` ¯. k  İ=  u űM } ñW ljȊɅʆˊ̂̈́Α įe %ӹo * Ů R ϸ\  R ð [ ű' e Ƴ3 r ȴF z ɶR  ѷ^ Ƴm*z <уπρ΃Ȓ Q˄ύΰϋ΀̂ǟT˄ɍʰɍ΄ɜF΅̓Ǐ 6΅ΪЁ˃}(̈́wFBACḰq́~=QOPOO?dāONLIJKHPHg̀ƾWÁжLKMQRQRPNG]ʂƹL ʀVS]`e]`Jpĵ?zՙXy|~|rYİ0kˁԣz{{|{z~ɀé"Z̃ŧΙ˃ĝ/Ɂɀzq˂Ɂʱ)ǂɂˁɂ̷Bhʀˑʫ˒ʀÕ5 *eЭ }It8mk@6================================================================================;+ >(u%n -E^s"2I`u,AZn.D\q )=Uj*?Wl &9P g|':Rh}#4Lcx$5Mdx!1G_t!1G_t-C[p-BZo *>W l+>Wl ,?V h { * < R i } , ? D 8 - |"  n \ L >0 %| f < E %x?# );U4 !9LpԔZF/.GYd}q_T<(  '8M`joruvwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwvsqngYD2 ->L[fmqsuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuutrpkbUG8'   .=IRY^abcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccb`]WPD7) *5?FKOPQRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRQPNJC;1&  ")0479:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::863.'   "$%$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$$$$"    ic08}% jP ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2cOQ2R \ PXX`XX`XX`XXXPPXdKakadu-v5.2.1 |8 Y;6 bZ:o]t4piuq8[?(~^sKGtCG흻Y| c8*g34*QܻҺ{0Xz\CUvLN?bNBژ} }@)[ $Wqae J`>B0>۝n{"?Pj͌0@.lH Y&z&̞?~9g+zZ0%V6b,䧝Dhz ҅.\t4xh{lQq%=Z7a0W4ӌ:.7=q>=Q -rAnMW[l)xVH8zZBxDilc<d/ tXu_y[Az\^FieU:ݮjo)')M1VVY  VB?u.R-$,t!+EڕG^vQdXB ŷfm@>@zoU2#4#[:N]{HsX4w 6r,7qwە * ʑ/"_y&[Kfj%OCqRҔ'>Bg$Hԏoݻ{q8[ݦ = ޫ5u~}omZ?jsOpuZ|ypq`~JF":r湀,G;sa{ hWs9s-Yx'=Ib xGȉAN {Q@ڛmN fCbI7e8>*M %dX0"a {ex&4VHPW5/eǤ9Ec2`1&7_~]p>X TT g#>8/ =]V-s.qHsq Om;?1}*xߟ $k0C\w2nDAp kO(n01ǻixVI;_vTk sQO( 29P = vGwp7kUHgOWDyG:,~lh¼#2JWZd˒FA 6pgsvK vlY:n3vHFӁyCFcxK{8m7T( ~<'$sm=ZqgL[Z Xqр?[2U98&q>#x5>ÓԿ<+7c'b@n[2U6VZLeh^-'KW0¢ `hOƉmC6!.zal;X"IAs'9WynG ' DN>O ]Gl?NB+}k8"2 HKI>|BР>#q̠< c< &\ vD@) 38 jYhʷ$xwc<fxU Z:)Kn/rB2~;~?_M38fdBH>\ eS-^wQ${|iâDBwÇP䠀38ĭfMV8MݓwhN. Vh͢poNpM2,bo?WE +gF5M xh.g4i܋&?@v'Pߡ'}3uGr4=#Ew,o7V]3Rڑ\P/$'8#-9v7rdI_ޗ蝛+2_ɤ2lr/GL,OmD bZp}_: R/_񓃵m>Wxͻ,OQƜ$~+uU:[$TvI`~V3>r8{F "[sϯ<˂s U'10{y 4׭_TGV|(. .Riʹ42{nȪҀħ܀ %S]WV!r&%Y=[낙)EFYڈnRtfh^EMh+zl pj'T%Nq,@*)Of%n-k=D F1ƢXbB<{#N?«ʟ (0CG?ٞ"q ʟƻ/ȋSH6&dv4f1#yIGb2?fqS5?nc}\ic)^J"ɕHD&'@p'z3_ix$q"j/r3ݗ9yjcJ276F4 v6NwZ0ܧيn+հ^ȿiZf-_7|>}P[xqaxQKc&+X5 Vb!24 fL%Y Ri G/1'l.(@,T-H)2:tyNedFO`,ヨbn͆y8IYrНcxrL^ FGԶONd8nJDien>l3פJ: ̮hi{P1*U=q~pd3D6P.#nWcN~,\rPM8og,G5`~ P/A  Q3;3DJL gߣP- jB4Tڡ:w_rƦ!q8aCcj-*Y=h Z;3=nCOj“UQtPqÙ-u\sɲkJK\_czΞ˧afnz[\8.C*#[=C2\'Jr+[/R!YwpXB?ť7I{g\dUμy8XJOF4)KS:G9BCգܡ EwQ?ͶzwñbJaqR|~B*nu%fbZy]?FdFjl4hWz[;H;_l=ԫzi!i/sp00L1CVN.oׂEd>ItT+BD,6wt+qgY9a,9݇873R5hR-M&eD\#ZWV\'Oa;*lo@y*5\^FU4Guc¨է rׅɒr|:X8 !{xX2"?Q]Gp*(X%:<d7XqG#cًRÞ,ȟ,v*5ɕaI8RHT;몎BL}an 18yeHX8 u٠a+D L#u0E_|€:ˇc\v8 4t<7[R\[n߾yz.c:}yAER(,WTjldMsPHedqEږ'SԦGBUxfv/j95Q}O S?,H ڇ'8DtR(@Y^Fx?;> |g@ F ߙ0My$&(XgG#˯2Y Հ\b^ļd%<]`ɒmL]ZRkBi-5۔$_fզh ѿJi _ώ>20}ANVeLsa->&ɺ|vfV$щ΁}x=:ڲ"Ld(|ummJC ҚVRs9s9s9s9s#k$W{RᙫG$a*}k.B*Pt(A &W-ܵAYnsR#izmnQm ߚ I5VƔBbnHcO#t<җ7M==Mkurnxfk`ԉp^mlQ^yo:N~z hYrWȡkF!o9bJ6|q|8 ^`(^ɑK1zGè;u3o%-"s]xĬ&,$wȁd˫Er<4I+ AJ7ޭ{iE_'3!xuk@\k.s_oNWSX(^ 4XK`%4{ʹb&Ge|,ξGsoJ[TdN%)Ԣ.Qߡw[Gi{O5WPPڎ `Oi| ljc?1y>p,<-38'B0kɣ׳KI[p&!X,J4!MJQCf`u>% gVJ|9LTvh˥W*pYf!;]|O6W׌ ]A<=o_j޸`d.qKqG/_f`yGYkCh8Q~npjKR= V7/6쌬It ksC HgئzT_*2o=WOSsEr>ړB3h!1ÝàB֗U+ ƞhZ*{2oDߜ.28n[Au`<ϩ6vDSPƞ#`ރiE|{} "w܉ZٮDt{!W%jHrCSoc^V:G}^ÕAȡeڥyEs ' )aA´ $4͠g8: &g0iwpk4Vl@5)GzV&P,yom$smX TߘRջpym,!9nj"F dN,I,16;.l+ԢhösC^l(&)- ةQi,--hӉ( bL Nd n@r0#d$9>;Zb.EⴭqS"86'qBB3;Ց6 ` 6COΤ@{T |}k1C7'xp zQ-|8ЉcٴeY+I aaPv |MiXn[,Q3bWU(%j`G Fr 7B~5Y '쯰#cW$~P/PC :M<89-3Z!k 7zWzlgwe{rqO%Օh,~.?}D%TPҬh=o$S2}7P2?Xl￉Wy H"SHޖ;?ro[lF) GC'roiXlWSf+^+K _/4o&?SXw>0Oh ]<mL 9e8!Ѹ͹xH_%D ;4czI$9ΎN<M&zpSՖ@TNrЃkԯGXh}*Z1-T ohqk]zUL\(ŷT[I&z= 7ML.q=-K5EBQWdي' tvN!|P?"8}}/DE곖,,ḣQmj?s~~*Mc[Uݤh5H&W1CuK@c;U4/TKHظ}ـB̟U (ڤv S웷zI fmrA+ZiS3Q ;0Ck1 3z~KV|sSS.v,7(7vG fW#ۏqӥP͸f5=׃e("uhː~Dح̟=Gh@חpv|'J~ َ!ŏ2:{HbI1AXUzb0_:r_DMΟ%ZQ@I#; ^^ ٌU!U&XըI?nb3:VxF,ϊz| h_ml0)Va/j0TVEjg\SSnμD&ov\aor.mV?;6YD*V/ a~#_'`9ma[Lv$iEچ-3/z7QWTlV̅MN{ϣo.1f"Kx*jOE_ɆtCY^T&!GS+1i PS)`,^S UVA6κBR?NŸ;y_ o/s1k@|Cc?XԳr%H&p3&Du.15faJ,nӥ%,Z/"K~$D'L<:[pGk2ŷNq Q|8\B%#sNce`JՁޝөq^HXԖ &N:bS')/4Ġ#bQw[q*nmp72 gn-3lg77,_Y -t\?~ ʹGVF*`zKd\3-"wžۣcGdŘbvdL_& dɌmsjRd.l]Q\ww v:FS:M;y{H"qvhKNa 2jb% y tjvu36'!Y7{ H(k2*I$I$I$I$_?mmmm15Dgk1c&kJ{@h*°'=/zeP *LhhLw3IygSz›h8 g'"#QDl6׽g sFQ:pM J459f6Ȑ=2ό=VFo2,Y6ņ9&c  ezsVCqU#xlH_zs$W,gLNAv$Gw A|cN@xRW\e*ꘉGOzjrϐk@\c_ur4eUf*z=7!&U6Smm'*`B֎ГAٗ6*p10dq-;Z˝*Gpu@ٿ/00 w %u6=>i5>*l;F^AY06!n]բn-Ƭ aׇ#+;gYdǗ%o9#[XpKJGE[uUURzSaÁM)0keDgQ Ie\xΜZA@{-T,p``ќ}ê\j_ufiJ@fjybNza =l8ب^٬2v8 -6'aI|XDCs`a szQk"&BMTSĦc"-mu6\pkXr1qzJ$|A ~bd =U{9itwĚHi@ZJN LYB5k<]2֬nzRGZX >!׌Й<D[8 Uy/ 4ĝUOc"$yzj+#3K> ޵4C=-5<"']_c2{o6nOj'&UpX xEn*MNx F\_U^JJjl:+X,J@:[m=wkgcKu ;A8ZlAkcM4N!]Kk9}t(=R"ONRËRE~XD:Aef;'" z `Ƒ#q+eW?'='bVv9-Q ׮[@ER$($VDzv9(tް1X|O QAv?X}R?ݡ|=_++9$0S"VȐLS[t:f]msr=?K=eH&7'7*_|pJj×7Eh TcYNm_5R8.ntp8^^1BwdiK"2Y?.GLݐxN4ФÊĈ=BPxnJ%L<`]p.&gyɲ14{'AjIYಪƮPy?_O|uY"Q-ĨVg,TsP= @l?tjzN0)͚(7}}%76W8 3tfM?M+ ᾛNﰟPJ\YSS3S[eF }t8jS_()MkqHG]w#Y̐{Y+8 ERx>m( wrֹU7Cҡ![pӴw]y$%W]udB77hy4Ƚ^ 5AS$"{^g0s fQ HVi(j6O "{. )]P4(MH|j ĕƵ+I;SVcd\Ѻ|]CZsʛz62K2+T){}2 tշ~"YW[!٥ylB챗&.L`vmABja}LWKR)*$wztvJ_EgC%3*5rI޲/2EGŭP%y\:y2y/ .<ĉ\ &$]K*hñ+AUguK'I' r2n4NyP6كh97|LH]==ҸNx+u6.K]m4gx5B8 o;[`V'fJVLc\D>'hkmKg6'.pk&v=v\2[QYT b)ArO}ՅaNZd]RLHvsCI)~ y7ͪec<::滉*8wq{?L'_M֖QSoY/񿢅R*_PnC 4P0q16w$"ÑԢą+^pLh"7`)Y:9=3-)lWGfh.sҹc;DFC׺0-!# uFܿV7g4a>Fk!rԖ;lZYb(l{A{jAVfz+!EC֑[vLrx|Cy`еjf%+A~~1)Oc^Xn7M 0⎧ mRYzC :ck(trX/(`?;MᎺw" WVK⧏smNK@s2v*rA@f}Yu鹓NgԘZV_}o|F]]-_+B`$߅(Y/U 's7nwQ5o]@_^25tSX= _q q}%X@XAbG!r'8sm r,c8~YC(8^O|GF1iORs<-w捘۴[Phv:9A Q(4!eќ-e~)Jc6ĕ¦>1 AobäS(AdWX(צ&(ym PJݱD)\WJZURۮE˸R}x8ިmKVԀt(mM~c"uf$Q?f`q`Xi ^Qkt"2 1&q2D0p)Α9]{apomDISN!D9HHg\t &ObqzۤwڅX&Duf4`N~5,(D:Z g#5 ܃e?iu%.sڏ2O:tA Id4 B-^1vK~5=fGlϥaSL[f!lw욳+%?y q%A3j㪧S!qt[vAt ݛ^{%WIdnp?́yK aتHق7ԣpFSt2>bOo"xxo#i8FIwzFtf[,yb4r*;F$cBl h1!ȐFК:r)Hqx4>T) 3ڥ! fyv%ސfun~bۍx<%E8#s\v:Ëde2{; nSd'&?8!A#}n`Y׵Ǟ0Sw<*֖ʿ A.j<9 _XѥmuPQ׾/3І"@4N]ud$q0e)s^x<' e1Ud;#_y7rtajJeD.s} hPn^杓½sOKSi"8IyfGR$*h = 0K͜/|[I4жΑntk" Mur'H~oۻbqQK91MxBG~%*ݢu,jj29YU:3p|gɗ" n6tGjM/Yzy ŇB}ٓ.a[V16ם 7C:b1%qjIp&BMf&"@ &?aEWc1E SK@p-4L;j_T1 ;FI0Yb&d.C6cgy_mevnv!^b2϶dO 6qjNL}D FI AtXlSכvȿcH5u Z>:w׀AHH}h48ol1I09YD h빓P7@Qo? j):~)9>';mN. ~f{,C/ӊ]ldRE=y:6qA|E]"#1N3`)6 `\ ,_jP8}K$Ԯ);n:X AyAO;c%N :/,ToŶgO&[B+K?wzùDª9q)AH84W/ ZVJr|`^? dqҋ2}%@m3h\0R\Qh0-~Dt#,ǯDI8V$D6JYo}J MSoh1x ]q{ RAgW% 7gA-]LN>'?vvO O wR\%&CfvcVdg++dK ܦ?MZhMFlotr9C`O-D[xm{ݧOtzDDa+IK fz9ި폄h4)*:0ҕHV&}UЦ}`><7CuA dȶێ ,&AlXRxd uXT=}FÓF 3s:n36Hnyuk/4iŶaj+@6a}sVh@3A\ߊ& S,LBtX⎙'0Z^RcIT#ݷ}( "Q~ڟ&ݎͲ[OGQn 77"F^`JvwX®̀VL m6t+y۬Xx&,sUz-3z1#i2G\!Q(BkTC\#YTQڧ1JK3ks=^TB*C,Snjr9 X~Zվ}ח (a]h7I9?14*k_~{NX/;]w2LG(NNv'@0qh  Y¼'QF .|=x34rKŠ=0z\c(~uzV>/@07fgs Vi@k!h#hh q*GY=Jj(AB,tY.1O?}l4[ {*G- :e=|Ŧpg"AWF=4>RI JJyr G$:;i: # my9m4r++Y[CzxCd9熏-@}:sTcӉLЎ" ۢ*> T`جYک Ef\[k>=};[ۘ򍛼ײU4K/VN9—!!ST6.h1mZ-mʾсJZ'A«͔BU"+\ vNC⼹SX!ŌRHH>OsgeR]e~ m@H n.0!L > r5HQ0ǁ?q* SYRɰƒ]d^^{ڼُ]ʅł1\ >5H},E; AȓYwߕwj4GHMHRr8V )3X[+vsl;IK!ܚ/O &x2;U C0",'j-k- 9&;IdbĨ:T˄yb]X7%DIov8}ѐcI.X/:ԂðE)[[p%=P/UY%"a_5Ġ`?!NQi:P;adBTx2ƲpVܛ2_^KR}IΑ_ >gIq3H(??%qB˺o0Tx,h*}b~e`hZ\ _=%x,\MAGzZGx/d{F; ߗZN_"5[$VTDj([gOgy$ )癗dxCխ;6  jl-V E{F?Z&Lgq)I(r dZK5h{!P+I:8 :?!hp9c|XN'Ow)\d!bu2I_ P > #'tPpi7¥}E[%w$\-p)x}Lʉ#V'7+祆Lb[tsl #>@3gdeIJeNG8\Sf¿yK`=Rg!L}0.bAb8wzdNy"{434߰$BDmTz ʖ0RrI)#W~Ə.| i%@1d޳8+yٌ2%LfjEDbZGA) 烣^ȺAkRim]oƸȇ.:cj3LUtJo="R~ l-'ʍ<@#Kf63zgx;YigVܓbv4'=ZP+GZdH>˃%S޼ۣ >-%("#da8G{@\5QDr0bk8 ~xDS8y |A/}CtbdAE6f>[Kj!5SOQăBo;DZ+[HDf20&ك}^իR+ pFATCW+_[Q$3 \k9`Jã-GmRA%j ::}e9jJ^Oa`7P xMbkmՎF S$7:1`aℊ-gxϣ=1=c%**a ނIt3e螾7#:,# ^[S!LtsUXQԨ4kV#(?Wh7+?RB,"@]Jc[ϒ@ 08ZU) S~'R~L";т X؆by)t5.ʐ zi`>)U,_Fe)v#ϧ^0Dj/6*Ǚ]#- r.OQ(:pV CrzO<~f}GQ;2KaDI%RF"WX-]f]]KVs8;H~GZd kr}x-l_fVLmn((c5g&ixҌoHbn "`ć& nnYU&N):F6A|JeVA}&-\.Ƨ ubkJ'YM3~R2(z/ T'!v 7gPuCM=13C&+G߼Z<wϻ"'2c &qYQ?(_h`hѽN>v9Mٳ1Y&V\~EUWd܏lA[R(4P~yjqA^M0jYa78Y7=DMݗeI7 jN(J큯FWZ/WkEoHʥITJxpLFR:k{mw*vǖma/սV^џ63ԏR@1$v|؎d, NYm$|Omb8 1M iNP]4"b1AF( \ ^+ Y8ØZk鮼z}Zejt^hSXa>YTihB$(d[gbknq1>5CuK 6dE;pC ܉Gg\i?%9@0dU[FM }xBMXb5חy)Pg9 ӒyFg< ڦ002o]p_Ba5繴/BGjg[CJuNJAb0Q-Uc|}tƶGTx 6yTyҊ+xvk6l'N&!b?9 ͼԿMOWg2ZM%F@mA%lVkc98_,8觀ђgA\ȼ)çTfmmӮ/;۶Lrxˋ5g`sT*cp*-ūþ]Yo`5ěQr j8 k8Iz-2;n(YJb4G("eB#W\<5B?(dGGw1M@:\,fdD`Ҥ 1* _ΚImm& zK9CR\[\'KJ%u+ܸLB1zlGkPZ{LWPY-5HN1ZևTz;̙fRLU{_ז Jne(DvτŅPVWYEg󭄷0 8ܨ/wAÖAyf;)h>LJAMk)YD|fԾHՊ3jm?[veoxw@8pc=̢'/^ȿ_ %QˀNxIP3b ZpRݾ{l3wfa@aU>!ưS0^Bx^9DHK0Օhb5V_7`3i~`k҅N~<81\Hpq↓.el['րmY㪀_7VLlpd.+9)~nm.ZUl*)IqZ v:'m1$P 4`}T[]m&RYi1WbK'SGNk2GŤM7ڷ#o=|U_Zݘ337wے^Q\ڔah ʗZ%#kvC{Xf_~H>!!] ٿ)4zuA~VeFxTZ G4i#IM۬2U1>'ءO+ u. {`.3b[A&_F /ɡ I,]"x:xCӢ$P\ɥJ_ \tɋq'T36]TtVGbm:W +2 7+N$w{`ټ 4{%qf*1 ä+(`Acܽ7w?e ~x)1|j$NbU3D5} z7Wk=-2n8C[}gdU.{i5FR(ȗq9'CdsPG9Uf`mI۰Sڪu: wu7#Ts@RnAJFg3 "eډ˂Y<͹J_Y/ ?Y~b'T?Hi@ԟu2!(AeťK1hD_UC}UFԹWo짴3orH|5 Z8a"QQ~/a#$FV]ñqW*Cax(} ۮSwYs7^s:Uv6A`I/OFH TI*)ˮ+ CFpqkÇQ@@$IdNZKy}{&Tk#kl/!bIS /Rk#h5eyئzu(. 硯jPda;7YHtVHtD11dykj-z1em`<$^n/`S= 9hFJ[dPSf~I 59mmmmm}U!H& 0Fg#(nOޝ@|.\Jޜ0. &Wqj A }+4&##"L7h#"ϝy!%^z8GZmXM?I&Cksg`X1iɔoꋦ.Z滐4-sw+ޣO:s]Jc+)T8?0oT,^W"/8qi~e;ͪI,nsZ~2S'0#!;HFcŅ> 2Yó RjYW*H÷?NP6;raZG;8Gf=5_((qLޞ0.ڦVl~z4g硽^y3H%(Ŀ9t=Fsl5 F +_}Vp ̿?{n C'36Z9lǒa *sfi͌'GP" fII#' f͝HWcF}LĴyq_u' Ha&0B9W < xaֈEaIr>"GL,a¼4|`s7yn<战}S hz#\v1)-Gjp>F`4~w|xx}>^7Ruˬ^r`,QKP2lbevjl 5^>Z}wG \xTxa'u1=Nfm+&8Ay8D{0CDE-YWt0'#9"%#mja05~fL3X |ߚ?!"<'8x 4#"'x3gd 6" 0ɵ3 9hxIb$Mm Kb3s'lM e1-⎙e`9RW?{\ ׆w.ܞiOpx5/)x_=C.^|traߤy@"Ie_X6_&24C:xǠۖǍއ^_h%' j}'Ȕsf8&(M2ᘑL$n61O8seԃ4(DI)X@DұGJBFpBA} (9S;c">sǬL,5$ز,dNLrrR pϗslF@)ՙ@R&L'gu~<)-)k0jmNF#*EֽEm?+ɺ|9I(~~+ZfI#|_iߥ*) n.sW뚁HII1w> Avlq`QM5I3l{t qN簊\L\@_ DBbQ'#1n xMr>'"$XI6Zi"k$usJA$̤|S̰KΚϙ~J56W+̘pN9lV[ÀH[ZKmeJ-+9-_֝SA9(pg~2v( HL%>ah>f=u'B "ƻ,*"oD42棂'ꭣ_?<K;p,c#N`f+^<~[iR|_o*aC/Mަzjr_p} IԻ1|':fPjOD5|~ćI@ }zdgnVWv 0/y!0'ihW7P#:!ji@0';lΔN, x` ^TDj.We>J?):XYR&qnO׺ n0Ծ ca`#8F\iyҫr~Mý'2oE*Hl_jO1ܣDNJ|L(Oѣ77 H{o(B;15>^s{|L#{Է0,Pr_Qd75ݷ֮>}LY,m)LL EjlSR[jk@JI `<#yJ23FP2VsRP* @; @']!'DXe1"<+i@yIuRA'"Hdi"g ' 0! >C^kRqHgB z@ׇ%% b~p'(k \~mvsU܁V~{z:>XõUU_ʷ1E~PC׸׹oҌp)p9KGt卵cB0{Vxwݽs_˖ħ!mjȨV@ʨzxg*$Zr9KEq"XxJ4bQm@fcc( TK60@3є;$`5,[ Yj.mVV(RGGڹ?Rt@]WVItc}|@HOhф?/|ZlzCzԪ tmsqi.*ɷcu9Ts&s\u*rTL4omS%~u $m DB A^Ӧp0)`t|1)l*7ID "B]a(ӹ+ai(#Uts& : sAu,v)D"4JyYcL(hC]]?#U%s"<rFΔ@)L\Jt @fK\rm nr:-{⺍c|\<7aҝ(4oL]͋`SUmQ,RiQ tr.18&RWu `Qi)U\*6uvs^64]o0tD[g?WQruݰ EFzx '@\0DоwDG;~y(U0mKmm!9AG̯N/Js8'JX],YZafAڧI{" {|4ɐodgyZ6&#fcxbR%(1IεH1'r|J"&UsnX Hpxx 4-Z@&C#_(U۬k18r6`k'~aLQJ0dn60Sk%Fe XlXux^z-^uĎrҏ?v:J;>Kguqj҅Qtmɠ3g`@Oxkb}fSmC;&'zhi2 ݭDyozYoCx+-Еtq}+>yGޮ`%VKK1/['s]5I5cј7MCnz(V!3!GQaFK>hdc"XT0> ]lFow~`6faN %' c`N񜾀ZL*$K ~$V`W,!&5IEՈTI \ bq]D6aJr`NRtKX M6E ezzF;vzN2??KM`3d4LCA4m.ݖ $yk7UWiٞnk:ֈ)j[t+&QC),-], 7uM-*SNih1ztB/5h.%bؾP?X_ĕʫ 6^[={UK\Pm&y2SkzasL",wUnKDx0dt}KZڶR'Vq]EE"4ad)Lե@]*ie@> AZ*3i3C6|Ը-CU&h)(;Yh.m؏rxzon;_Ĺl<=pmkg z6{NbHj3n[ToqՏϝ)m *II TZUr}I3$]tgT:->Q( wQR.L!Hͭ}N0 hsqݺ?{rb?CA^C0G< e)bR Ê'sn-HYt$ *k a6VʤRL9 9A:,[1J?q[F0$LYSPц㔹: 5 Y,vig%4! V ~H|Q98cRĕ:Ki-x#}S$ @RMR'rP !-c}2^Jϭ,^ܯd_fkDרF 1~lݷ:vn6]Wid&m/^.5&I/c R063Ȁyfm NdE77330ZI4}RM>0dzE逜͊)Ǔ v, TKR A(PdfD%tθo|#u81g, #%Qp<(wZ&4"6$ª? š<V\ֆHDVH8JÇ퇄i6zCѨ$@&fUN"F_q_`8ђg@:{$!, dDQ{f&w\_lls{(m\cop+' _FdMg۠ơp -%\M" .KKXNu5" cPKEȌ}`3ݖmJETƪ $ VD҅Y);4J b #ΣLVwT| VG@@'j1=bI Η6dP c@7g2PM#,oQy,LL|kmkRATÅ61JF9RŁ6|3V2oSA=|'Eɩw"HG,IkIK]i@ E̒~13Y.ӰehJL'dgvS{l-p^H/3Gwj%k3DD؅SL(inH!c](vv&u< 'uֻd1v(y;ճToj QѨ?K^2:TMtz)|s cgK'x4i!k^kֺC:Z_/'osrXm@28ZڧMnUBlTw_q|;4'[s@_'7\Lܝ{aO57t8 D~ETfBL |BPcY gj̎ ]RМVއ36PCZ91E=p-;H7۔q@IcIdlDUNLy<`LBFNicxJҞn=TǝRƢx$ESe1ô XG*0|fThBmDJcRLC ԖSB 0Ym͔;'f''c.u*[)w2~lK4i 5HO0(oTҹ-QN IDATIhKRsML@~oY7A93l)rB i-m,bFZG"wQA$jJ wX_yQӧ?۪in {_`S7ͣk`b!N6[N I;e0[y%bN=Վu6wNѡUm~h{z7V1?$[쮮y6uߡe4=ñM NMS,7.r(eF8R" &RpQ6VK &DzI8?%O$;II8f~ScJv:<7tZ31<Ě"x+Q<%*,)8Y^tq[|#sGX9w(MSx|m#Z Vfcp.wH\;<Ӳ{vzfJ/2aߛ[ ꄯ:6m@|=7||U/OT?+թT/$H4'eg _(`㤃S@J+dILx!ZDnvrjXlt"t'pdghy̖F #`J1>A?!K\DܝOﶱm@]cJ-՞'<9CTM\XߠnT6T!6A,7ْח %[JFޝK`) a"6 c(D\s'[+ [h[ͩ껔r#7w؞9k~s;` C|eg"]\8^u++%P͖{ĭXc3MN1fI)̖R}2$XYyyfii%l`DR4G 9'e.SʈҘAjZ%8i,t/GJO~$z IXn L )'*Gm%쁢Z]+ZD(Q2Aqx'''Sk,UsЈ$JGy|::""0# ES9P>Ty/pC JJ;jwiT֢I{v??ؿboD>{oW'y4_Rlӽ Wk=-/)Iܺժxю[x!m*^eO sߟku*q#?GE뷼b7_.[3,3m0RI>-MDÖ u@=ojc#%/ [19Vo?J0/'^ҏIäXAc1괫LEQ2ea>8[qe. na`+8V(`K͖ ! k ,kM:KiH%ek59$H+pR)Q] lfs0Cgۇ#{Ֆ V)j>Ãz}+Q66FabLutc `zb#Z7uHIiFSX[3B*:HE@q 鼝*Pvls<ǗQc%<3ǨF9sc"IS,`ȋ C0@ir;WٗO|p[=uW ZINÆ *EO ^'^,1A/}7,ϽY:vkb4a$Y 6ۭYHpzjQ%%$I𭩥#堽L53R{&-6'97X @"``&~>[䜇tF|R[!$Чml_ƞ56Pݓ̓۷3&dc9,iΟ82{ڙ5n,/]2#HHS)[5So '0TDL %-TY f6$Ic͡jA*;@!Х0 PǾK?K)IlN nʢ-Z3+1#C*4 >kBX8[DyagW)9Z2`kjL 4:\ Ӫ U)\HϔĚO*TS fTdKD#.~&%_u$=(ih, d.՛F׼\<~-OP&\:yEx-Mov_O{O 8e6p[:PMګ-:u[ױ|a$}U*Ǘ lI(:LIK[6lJ6i7kٶ Wg)uawy5S@&R$MOь|Ocxmd&jv)9XMKgRA:7T#. 9Li [ROcÊWqM"yT|goLXr b%\FL&p@nt1,%S_@y15 `Ҡi7Oic'lh{+=M=xcЁ>?~fx0 {c\3K5?cnIј8/s"EԾ񥁵gk#j6XV-XDIIڬg*@aA,Q$"l0|&HA OT53C03Li2 S-c2g]фTͥD(3(C\Z@$" ѬM8)!@ ?Psp!0θz_L(AW˱80*,'.0J?&\JIg #B!:QHnqd CJpeL`1=D/g\{ʞ{Y77IkG]/e#7_~!dv?V&C- yMĴ-?ՈT[AyEqI~:,+9vw25Goun-+<Ő\zxi'P̴):aJ"w|׌O{L"KAi 21J G6̟1zLf6#Hhc g^g0kwO5`[w%`(z)0&뼜ќ(:: HVa 83"hcWP%O;`9hCl eЬ&8UFOXza@)0Nc@q5i̛h$Ӯ8u4ܢ)ΨBnr-+[Ѻz#WgGZd3PWy76;)ǣ?׭|$r[ [ iFO{6E>{!㺬gކ%3[ om/ºJ-q56B ƯT\(? ̤oTӾQ`dW#P d|ʶf8l֡ ۪%B.A/LPő¡/9%%b'_ߖ̀ƈ~3Ю _(Q-81qA٨?GHD)Ir<"$=t7Aj㎏C\"uitBcX@m,AQN*U42JH:OJquWvk0{^yZjǮD|c6v?iS|jI6I.͍鼗spa{Lw^~\) Pbd3?[)ҦB u KzE} ȍ fMCb9)Q >62d|@Mw_Hfzʮ4Q=˾ex2sOPs0:NAKj]^c3Jq>N2ܖEp7Ǘ9V\y D:_ uHFt2@c:SMdMH^3(\m;FHI\x}(n 7BG&i7},.ru)quE$F{/R4Ǻ}?6vUk!Paa+KW_*濁e{\u;{L(0{=_Q4xvF`zCWYo\ՙ4obvP쀲<1g?N jm2k À@n1?E++^@se1 Ѝ_dPX"s)}@Z3$5ƨC( =EEI54#6Bä?rt4`H;is<%3L0 G،F6!O3j+$1(}P2RoZEc~d&yRȒgδm~(>ItJ#f2i:1 H@zZ48T )[M 9OtiaD[zxE g['X8S YoվرR,}>6Ѯnjn e7 RgցNycα81J)YQ-Ƚ!șI@`J@ I%h&jOrdZ:)RbJ]Q43V$~mƪnqN\۝.CꑲENZK?z n,/Bբ2J- a>Vӓ]+ADZ>,V˾zղa?r_x= N0:Mp`Ҿ+},̜vy Gq=y;ڝV%:::\J<'HϘel:/Z%'ȻTpF0"s3$ XWx4KUq.)A[J]tPRr2ȓAݽF,ȗR| 0J| [BxuDw24%Գ@Q;xvSsdiypGijK“-~k tY@Ҫ\sj/c9Qfv$_cGpgC~WM_D7ي?]tmRs 'cu9tI/߄?0񵽴Dhc SMt^=BSRFΧ2bHI1+>)SćLx'|Hw7efzrRI%͝$cYV2pRUOitt)ZGeti3BCh9D֔<`ОZA.{`)JՖ e9S+fЉn nDwRLS`f6lq?Zo\~w~PTIx_X oQz\1z>*gLzλT*_מ󵕓wOQobuΫMWʯT]ln H[:n TsPT'0m%%`Vl<)ΤT1UWMa*SfNBm3t4$)q'n$l1`fu-#jo&QwwjnR&RA@#'rn*#>1?5A쐖7=PY*-jPZ ֻs@ 'i8ZSR|1Gƒxlsԃz_|r Z@eu?H'7Bs["7O;p?죥jPDإ ؈ol9a=qdzr]I@kO :k:V*tǭpݨzxvPj13Xްr[O0}C?%/L8"xEB)yhHnό߁yz )ɻW5Ib׮X=JzT&5j2g5Q z'yQ`lq, )f$ö 4RjE5)|Tz>ZӷN)"AXk=߁ѰS7媗|x=(P&fVŘ!} e۴(=h]S::y>*Hԍ4)Q__*5Qg),ރ\ڜdgߟ$^ j?z6u_˟y[?]Zc ݝɯ7ߟLp <V  y&ܭ#w66qrwľ3_ h][}NJkt1eׁ%>o'{ٹ&-7?VYF]0St % aZ@#=Oze2'M m9ߦd5ƠՐ-!֠x e\4iR!AAAR+RIiMIEtIq#NwbFq0)[.rSi#myO=M 81atƿ㡑oPd.0#I ?r͝#3\`c<|(]v0~rIÅOkR{.[ [}އoB9ld:]lUZ? ]8zz*PG^Hãi#ADI띷[]ӵ=EG7%ᳩݻXR mS X:Z+>'y Hނh6Y~Xiy$$?Ѽb DKWi]eQ|iWJ[& v((>U|m1?I>:ŗ-ul&vs#J2Y TlPJ=eђvܖ"_PN%'DJ6.)Ҹ!(&X&/$\%f鄁D:22Y^5&FE;ITצr_9lNK OSzGrtyp]s47c>Vex+/̷v(}oprT={ 嬹KyB>dfRymHiHlm$I3:fc.)0F܀US32UWtNvb0fZ2aTِz̚փ:55&3qyZ JH)tK[*X٩%>lfzN('s9?H)"81<2&1~ $:FR 3BFJ=y9oZG&-II|_60&^m_}hq1gd-˼JM]kQakc8f~%${iQs%t"P>.R~4\3KJI gr;e޾wR:{-ٹ}% .Kף&Y @o֥oZ@k>u%m[C"n>QuX^G㸦zbhfr\"r?Xi: f8g-R*,{&,^)wUzSkX!œ%,GQGXY02ڧ?p}ʖcU"elfLI&UH Z])VSL,mǗ=Ŀ8aԄtW4Rrk&D+(. ՁxQ~#xJffU|H J VRǪE:,7tHt Y5Z{A_%Ku[{rݩgl߆Z:?|ý ]6Cut ~MŝRdNꚀcMlZꤹ 062G%zN0F2&E'I|k~R؀g=_[*Vk %8G&'`VpnSHRsҐH@;YE^D|y~JC=9$ 4hϧFsOV=vؐpΎiV9D୍SY90OS#uC1R-`aLӱ +dV3"q()txcU b(aj B4)+3x Ұ%2C0[5EI9Wwl tr :MJ=QV}uwb魸yn Q;&sLdRu)O(&a/R`>'G Rqpnm SKoߙI=P9&:RL7w%s0 oղvIRga۵զۋHXomf +^bfPw™ufA5B?)fe81!@&W2e0%~}(+U|de@l(nb嬐yۊY J /ǟ&=/mRrV<UTM"і_q>ITA]\m2T^;Oa`f׉ wN2h3*7K\gKqkfz)8hO{1*E'Z\d&+䜠1Os22'7r ' ͔.&n#NeCLr$s7d]Bmv(_Kֱ,NӺ{y~ $Z68ԧG˯CaJ|HBcfCt5ܳL?GiHlܰc¨r`bYij&Ƃ5>:,:Hd/TLnȜi! T&l4~H!2;˩*jt ֜0x@ 9a]G|1Y^?O=$^3= ́`؈(mܕ)F] ֤lfxKN@ Ju KLr lڽdMSYZV9D Pgr]VGL5kӺۅ`#X_@MxM8Z A^^'/VizE9UgE,7\Ji<&Y#_luRCs|}'K'h,9(ͭsXLRsc5u;}1 X Q!9ʜ|BhVʜ5}Y&mZk-¬K,>~rGC}Xc* @3 {l{9(1*8 4͹RHOjkkRAP: 0y@RJB'9yH*$b4\FD0N+5PIB؁- dN)4 P_JKpa{Y9%;rA/q F,Ru<ՇͰizͣCk#Cv{! d_g5J?uؾ6Q=m`]2]&4{ދ^C 6UQۣRf(Mç'VJ:x9 @FL`Oˎy: OҁTHrިk R,g#sf9ʷ*`>ܕqG *[w>Pjʲ8@yy瓂C.ٶ C7% F&s3$$0}A@: (Qr~L1$/P9dF^!ޑy|IO/[y2gQY陗Y54/ , m~K'qK1W~$|Vs^y{%yN^m4NL#bK9׳y}Q :~΂l'3AZ@RxHSQϘ5[lN'c|`XnrQR91O.u6j4KQwnG6]KuƐ&6Ծ%0L}cݕ\q\J\P3QLsf8ci}4o#O&kv!v)Qu qF~ǽ ܄ 2ruyu 1?ku6Gbog1m{2꾗MN)($U@!M`-)=T;LV-X [ O"/1@eW(,1RK PM'[/Z8TVͤ9)DI@B<ߛܽ##xA0P@ RN84RRoF_=*#\Sk ]OF4淙'X.@]mE۶y}j>1L)IuȱXkiG" Ol3W8J`UuɘHWZ gg$tPkj7IG4/$a%>r?VI$o]G86f mO-.uhUN<),Ipo0߹r}R0fS+k$Ub>YŴ {@6i)/8ٳ'c5r*eHzI3FG|0ay3I(` y֦)JfJ4m>$fkS@XmJW9 #$apwueI܄tiP?k%Xq!- biKWJqutҨThCȃj&ϠTC~u4ԃR#e~H7vSa+uCRy4pN!F[ ﶘ.!e*د& ]$- ul0>=CPo<"WY}c_\!>iFslE_d_EH{^:"&*; H2)%_%V%3Ey#`9g p[8_G_$Y4.Hbd뎤S)QY(n4 y-Q.FRwv:A=Quㄟ'd~y,5~qv+/w 쥷p ڈѥel!Zcvh?Dz>îivV. z2\!:Iǂ͚nČ͉uݭwc lg{&Ų4'w1ˬclMNwq#&=8&`N{'̰%llWx@8UrIr4[. `〼itȓd?w T&Ϩ#XŊpy0Oc!yK l$%$r@U kTxȏ,֐Qy-Wg[D%̈5[z95ވ΃ekޤOj}":LN1*٥/c/_/2Û午zX=xEA-MQbm[°ch,g[zؤKSYt_D8D:?ɖ$)<✺g`x̀@f⡁Ya`7+5$E hy`Uu2|Gx䩪`D:H==K@E/jgbX☂/,.AG\H "&b &qK6u ĸ@IҶ8Q  m_KSmح1" D nIE/蚿$dҍZXxt-@tMDC%\F~:࢞j c36i q 5'Չq ҏ@KDL|kݍPnYť(Pqz>:#q|Z=ݛ[0jҾߵrbFݘeTo7=ʄ݂'OۺRÑ-&_='CXC1Hĺ(H ؏D&IĕCQKEHQz7&MK6A2ҭT5ŭtk^POl&XYSՉNl/$d Y(P+//.u"%萾x_KM'^VzxB[7QXht&aʊd\ Ѯ\[QŅo*FA_BlEmc"nMqjB6A\ #\ѫo¬ ɳx'6uS68]Ke@5pdZz a*K{R s|-6w (T_~7_\5 E}̇>s.Q} iO,!`VH7jf9ո` <Uv rvt-4'1Xп#[QBtas26l!>t.wkmfYpš\nDz㒋0i 4N_~n CzcFd o4|_f`uåT][  tS*DV Lȕ( j[8{`wy6MbN B*!JSX#9" ه LD&mkL9e+F*ʎe#DՃH{+>:Ӣ+ ~9* JXqN@Hgxo0IJ0(0)AyƤɶ$L^7cR]}2˨lPCQ>ʸI7muڌ*bPƨ!3.PWtܝcg'\K; 6ZGEjb@km!w ϧ73)s ˖H6= U) ~-=CxG3퉾aퟵ]F9BMYn&mXpM Q mNBޗ3!^S"(1BL5.z"iJɟta˦@i$RuCR)EdDH) v[e(M5R}c]P#{>Эar܀1|O ]7WHXm[˸U:$3Grr*ig .f& *.R3>j%Hs>V d{OjȐx?8WԃpAh ;M${XU"wо|ò Mg&qnQ?)3W9 <L2vԕcB{ +"fiu?y(ȚT #[v B[cI*|!_'%/hY s!9|~ 7C"WW H| jo'0h'dabP>[ZSsDTjUh' RRGZ0>F&:ij77m|maneao8b)m&} )Fc70 St&=EL tҷ꽋G#*mAb(͜dsa]PV ׬j8Q 9G%ZZPtKRu ̙Ò?ŇIMZf-!Lr/@g|J!/Am=k7(lz=@s"ŮĴ2{  0!1 RPG&D-C:F=2'CƸhF 2چ9!pfmF+K6IV9p~).d36RP'ھN! u<ڢ ub* Љ7i~HLEhI#jF#Hۍ9EE/} H'QQzټ3f᨟5 T  c`AJf;V"1 t"Im'307T}͌T#S#uV'N@ \:^q>dW:)Wԧ\Oz% Ke̾g@z]Խ.LrbZ1ޏhzPchn\׬&j@طt-lMCߺ3IIMa~.$ا)A'c # (̑jPͲgkaJ=̍DEfRW&# *$=JդU"Kb{;f$vՙ2z9mUr=vߜ|L*qIC l\-BMn";6x Ef=捊@}Y8;N+-,]L ?u:P^ ,*srZDVE5|F0>?.zE]v͑tr췽@I9uSTgML~pUYbQy9IWWoQgouZa=IMnZFy:1&B)44<ѪӚ cCseܨƲӧ;k%a>" wB$P/`7aqv}t,rM@zCR}\ڪXd MzԳ}R*GByd ,8*i@7gL텈F)c+[pfh"};Mf_/<5uu'꣪xd(vܬKNBj[L2`辋:X@NC=t:P]̍3RIAIgSՓ8yI6P\%!TBe&:A`.aP}ՓvVܳv_C^Su\dxvJaDx5wKHl/rDN @ö T5>JE7ƴB .X)P:2SZ@-jҪ CƋ)nK&S 'sܲȰEc)l9\x8RXog2XA%1BJ=&5hr=Eu1A>S v L#tjHscNr$Tg{whn{?YHun3=A)׬fRiǩaPg=-*qpX+fݽp?rk4")nR756 ʋ5zaC8pw/&})FxYI צK 5XEtm\RcBӗVx| if@r-0.MlN#F(q<@2'Dؙ~IFN䦈:_ŐFGPI%hTY(\JR1*)ō+orN}. HB:0w Rh{ef> tgCE1jAm̗i! |KI*.>ҿ_ɔf#B7MU|.uE_64~5'7ql{d4rfЦ[:EIIDXحlN%u߇UEtuD`]}wAJʆl|)elۦ:&CI1@|K csՠK0HDTn=>5)I*czE|"oL' }rSa~pJ)jUK=AݦLd:qGzkNvԱ{>(G]|6qeX?<׷K;F(0>%z5+w:"ֻ]`ۣYЛ 8Uv^[fiٜL+f(d%!k#1'5VtXbW޶FxEXYuY'uONO}Y"IlZ}߭iSږ6똃 8cRPk0_Z9:gKHbc19̑=+t,*E"5*ŤΟ,d*{$afҋH~ crE4v^T&ɼrQMݳ䱒=GdC֯"uZQe36K\eY 7pFMa{i=U5 * ij8y̴ˆO}#n`:=Т7Tkւl,{ihB jȾ4;#t}Mi/QFuVLd30DEV/FE, [m#%XMـED1B{9bPY"Ѣv7B' }c` [׼ ] JDbhGG@ꔣvNlAe>T<@)8݌\}u=&PTנM:CaIEECO ,@u:vy;3^Q xdu }@V骨0|0GQz-iW 2RjsSi43W[Oô؎a6e"cMYXGc*ܽ984̂J>^^ȿɫ;PBe-<{xF4_gҨaHTfVBR7 nMho.\'X~Hu7#N GLE"P(eiCtNf*PvUwZlY[PV&`]hf NlL &XAM:LE t?Ƽ`1pX[te V7[+/D%hK$őO逧lA2t,H@~SF:0xAIy G@4r]ҵi{}E&u[?h#Z|N=WyB2c!1V )Fx2﷢όC;Y-O݇+4Z}YmZwGWe s` + \~H3fŕО>7@6bw)U2tʗP-d68(r=;*Gh Rd&HfS"X3<"6LF?l(S&u2mw]'w̡F\h0oFr4L߶Hj Tu"sxq%y 9T;CL2+9'CN 45i-*dUT Ut@5x@sCe`Na<'C's:э=+xfL=(꺆tFR^GmV\@9%vfn;ߧK7`H[6X~"Z@B3 J{|mJM^U+M/26+w%!(_`+D~ :[h[k @'Bـ ]Pϙ 7EWOV¨FW |dmE~:ڔy*tenfNhؾ34`䋸NU 9:=y+̌^54NPyPl3K |/+BpdSy쳘D`da֨ j$Pluc%* {Ss,ݜlS+ԣ+oƺ oq,T~>2-̑L*^lbs/T )Zq5[j6ѣ)_dHQZ ԲQi.20zn8T{FL~Ue*bhNv㸪0z })`9X@骄T`K+fYtOQWSZXy7r R7Lz<=w4M:wZ]gW@tj꣸ {njUlQ|3YCN@h]<}[ezb eN4"M8 |5hW$EbLm/MqUK @us(;O s`QT&<&cM$Ra# Uއ91iviέ d2kۧ!U[rqR ȏvT`9%QaR I@.EFgfXT& n$WT5'?bjoNA?5RuT8;@mӀ$ǼPU ^LƐLr^{R#P' "SQ xoWonۖbVڭQYdZMit2-ex"٘e+f۰s)Iڙ7]"Ɗ3&SB}i+^3m{8uT`Y='.40o;ϡe0՛og!!k!5J#61*/`^32jbY١CkT.wkj3(:m:w͓o8+3u$ΚLzӖvH6*7iEԣVtMiF'8?μމmnXnj2F]iN׼Ǔ7@f߯4:ۼvXKF`_F$](Ҵ):dRZoZ% R RxURdOSV@Pf^ti'% ɾeyHdXtf `uxݜnآyuzA Iʪ|xpeERrra2wP)~>i3A$ i4I 5y\KDu]ہ}rYEއ[h p/tDlw.mZ@<5O]#£4V$NI=5Ju쐵?L7gS_N.)3c}IWr]Yh!ʆVW3i(af:^Ŀ:婖<N뚩VmXt"ɾ NCMjX,6Ych¾W7J 4FJ3Vc7mveA.Eq3pVzh+ӱ=J>kRs6Iعj٘N'0IRZ6$j&*;[BX']Vd/]@|1uuF+mdRk{f+,cF]ׯ 4B.Mv-E~2-C{XWaa} `8G-u:jbnFbݬ^E igRՕ?=gzdm d3̄<$gʱ}?90CdM&*~S Ϩ7=%4>j-W/FPu.)OJq%;nK~qߜY>#h0?C"+^Z_|/+=@_&+nl*5>R(YZfb$,ul:jgƬ(PvʆUn$ͳjM$†V6@cHziEo?\Wk>Y_@Q",دWTfS#gmFݗhXIJ \D\, a1W:-KRN([]H?RtKEOu&viùbMr14K>u#OTWAX9F:NsD :- 7qZ2ၶ;'lQgi8rXP*U;0sB ((D\Dɸ`ҜLLq,kC-r Iw2gUɓ* x &Or)Jڭ2p1&y#wҼ<2`#s:3ƺ uRoN9gݗ;u緧?[>'FF6UMXQ̞/Eh]ާPx_4k?&d]1q%a4 S^z'ߊYe%\AuNɷ#ԡն0j/Rg~1h 7e؍/WAiԢ4jvض;{_[=|`3&\~#0]1lwg^  xd̕OwOsPHQc:l gsn'H}CCOԿ'cR8үGt0lm줱Aj6Nˇkv+W .m{?dCRfkNs˦7ɷ ?gO[봍4 t& gK|<G.mzB^-3ڜ|)ƐoNJvu{혥=r0}êiGsDsikևKF# MW]5=aS:ʙdZYUTo ك 7I^Vb4A/[EP}Rog#|P?c6SZfV1p㧚8,ڸڷCnLitٿg7WC)?s|>{ g{(fuhBܹ%6ÊyŃWJ|hn^O `w 4N0p G@ʵ _hz0kTg؆X,L%\i%"-fH򃡮 y!͞UGgԁ}(hv qԷ۾+:M#Ƕw%ǦB4=gvO||z\YQR.Nqͦ|7v~<|nJ)46iG5fߠ&"e2SN~%k9,]#IDqxr]Hм_vj$Xxȯ/3{V@: m 5[ I=\k s6L HFۡV[-O5Rjo` :41qܭO z'qEit \ DF:{ ~,U wlqe#ȌP)פ>. ^b*g qW>J4om&JI'HO#9ΖDogvF)44T* yyT_%jh`uj: %9rf VlkGTxl$p1'9@b@(ׇMh¨@!|8hYWN>oCg&º)q&~${6Qb0(lLEL짟 S&4N`a DN8SgÀcz7($ɘ4BMKTjxc"%pEz'uPy'4r],D&}IC$ybKV9%f#QN B˯*Ef=5W Ie@ԸQ!CLE|<0˭$|_v<& y^3,4j<.4pFglUIAr0"E3T3@SVżSIEZHğI0V3|GEIUKCy!Po7JiPvcx xJpPf&v 0 [z=Gm^;6-]`6,l)~GHI JHdţ(+2p6 :Sb$fd*Gƫ9aq%dPT՗j<>[ dga63  uoZGkv" ;ȴ?Ү$X*3K[-bߒ["$dl륺ѧ|ʷ:ڑ{ Ã%\PLUFF9%IgiQ"4&S:Q& T'jmD*W0l @TGODe|ExBa {g^]Ŏ^US_qxkqcގZ)V+tmh}}_0$с72(INPVIDU9D2NQQG׌ROV_fH"G A&$ݛ* `f$ֳ~?%tIA .`Jt݄IGDgmUf  )E9%3̉Odo\6y|\ri]Sq<{#?I\ɹ"{,{;0[Ns_fߊ܂wm3+nhMs%!๒)pmw@W[l7beORPg`Stx&5h$LH >r}\K6ر:4bvěGMD?=?{0cֻp;mAj{^{&2fceJ$^l oUzWc.e~uW_?|'JA0bg}ΚV!vzWV(%9`J)3i` c:Y 2()Np>1rN1>0L7t:q|9coV~.*EyNSndv?P>]$YZ}_Cp!5gY8pփl˳z|F!sMw|vϤaD@5[ʰvɷ )UڢO3qgw???ϾTKQiC*ΌKiM+oqNLCDYN'#~'|>E; t:UDh",;4 ..1B6]k>3vd.^աJo>Wdԩ]|CRϘix/e]6f㓼=BǷ6 O^ޥuET@N] nl:c_,ڦ,8&x:hBOMe!8SC F\6.ض o!?9/ ^}6"eI k˔6O'sL/d45.@ f@ȧSFۿ-<C+~ 9n`(22k!t>ϷQvAlZl[d@I{I=)ayȰx&l IDATR2o~:5!g$w|f)q>Jtظn |XnM>kQR ʶa\P.legÿ70'V?$(xc=y0=پ?H3MD ozkSz e/Ք:% 3=.|>t|tF|BJYl&h-PŹEҦ&X~qZ2Ic H>f1v(RX(Fcwϧ}vgV=}Q}k3 Y gSi)d:\4XPfFFE#>W<=TIr?L-֪)!&d_g+eC)l\#.뿋_o+?e|x  ς؉kLvkNW0c2xo )p;|wwSt߇\3Q=7\1)\e;X1%U6|ҿYirK1d`Ϫ`Vct2!Lrɟ)^x-<pfo)|,h2}Pc޾>lWkz=-3V9g>`I5 De P,9-pRmb}1ǴNlȅQ.v).?0 [-k$&BĠ Jn5MP}g:h;dj+!4 #%37~[p:݉0В*] |h>6[̡^-fk%vS=1 #6yԳ,/5iάMm:D:y)h&,t7l~BAd]>>i2m@x4a10+kl|2P};UCI5V5@c|R&՘Aߴmc䜑6FId w/[_@f\ -$KDl4˷*uχaz{\0*$Nƫ˔X$}S3Nwp~ +L8ݝ('xH&C48'޽Q1~aC(:c6/TU+86 !f+o) AWG&mj[нR~,4Y5gf!];HZZ2x{Le< @sGi v-] 7/cE/# %hy޳L6ksY%O VN IE$D)Kp1gN،l(O/QlYaRgɣcu%hUm* z6D 'Zwwp:A(4ԅטԳoEKCơu'ehT%lXHcouϚ,]df'jpiߺ6}Bcn#@]^nXFz&9Fǃk+voسTLI>Ɠ@B||9ggS2IJF04GzkUcmcEt#iz7+K0FN4^[ɯ:xHsL{` |M-^',悎&^uV$!3ii5n@ h:[Es)\.{s?rBE{j&$ %-P}ZZL8+:)g=Yw*h]U^kNt_i]ai}|kw"RlY#$HNs:IM+MwqUǁt]G?ܡа|ri=L㸺m&mݫk(I"V~/uI ^~c~H$xm- t\fRz#fY])bÏH?UW6-2H RNp lnF(*4lYrC S+3(˶w]Ȣ[\y'N2 .^Z6ߓDQSRI(Kp\(IzF;`Asntr*Kr;omoǁgZo|M)ʦ|:yMDK5W RoDSͿ@M3Oo|}Lbð>~׳NLE#p}'~ ] $aPgԔPR=@)R|T*f'1ۂRgvG<눼ܱMႣaLJG@tOjS<ʔgYK}D3hw}2/j]-3n[k ޓʆD qGJ8gF:#N9@{TvB?%QRA@1X*;\p"jcmRߜR JthT`y3˷c>/:@ruGɜ,Uip$Dwoqw"}?+yw(Ydhx/ [@WI;;o~ڹG ~D;L2r)s޸z(m qۮYf4El #2IDزf"(Z(&Jpڣ<Գ1N/rNs˝j0 PtM>|3WYqh5o>R96})iOX3dp:Рyg*rA0J{-38-=F)eu~ʆ//d?|9J@#u:5 nWxRݺ_]f3]BR o(˜~͠mJ]&O1Ι.xH\9NInN 瓜jm\|aDͶ4Op MFWul=R>FCR72.M'V}81[m Wz~O{طR8;ROqc }SdFz*dPF fO g. .=XgO( Y"Č}fwgl|J^ wgZoA x{xԅ:|D1g封S:o¥fpN$%WpJIUv*@58eI脔:+=6*g=yƮ 0ִS#VkJ!Pih̕ 0w?ځRf9zVcMl@7\ #.ƌM__GOeJtwl:@~wvֳ$o"G@xk쉀焳 |CebiʬWr`01.ݨ|b3MR՚qcAy}PC N`jɧoѸEm$*͠%?)lLT.mO-&WI9f7:iz _.WM/4q ~;bEəem<ܿC)$VI,A 'a &p;I;A1yV wd;enKcQBz:>a79 ./>1\Fskuфx/1 A%~j}>Am]>o 9`rT> 7*O6R]v3$<h uDzB|8&# G# XkXB^#xA聫:?FJUU@ըD5*2Ϙ;*$DqQQ7vCZ[fSol'u*r5p`g͉$f^E˶tq@ܥ1 nM27  <<xxre@J{޾}7o?C Y"U2_DmiOSO ;yu C )~QfW '!z}pNKH\lٻm[[22 %lyz; rꌄ)o,qN=Y[_H/=p4k1J IqIbL\Z4ߚmjgܟO+yRJu1hNW'j6 yxRY}oZ3Y%_Km4Y3%k"Vw. ;[3&V[bSIRp m pG(5W[UF3> !'C3nOfelt V#v͟u:m^0`vۻwoꜰmMdftf3+ ¤G5 }G~L??pZw84%_H{@!a~ D!&̮_8D@C}R•y~Q FRr[3m@#$hOƝJAbNs@VWwr<^Ҥ~F37Ud5"(mݿI?rHi+bK?8%)+3_O᷿`'_ %LHq#%x}`y$] }pyTPfF˖_crr|R-V4tj+{'Ӧ=v^1)- C2L*زCC{J4bcSIijQ 滌R9e_~)lp%޾e[_H N)ݛ˳8=V[Y E6d=VA4r)g<\kZCN",Y;{##ɯF4wFOΓGU ?D("flLsޑ5,ċ?ʄ/ui\)J$=vP)S.y \`͉+1.,={(:DMX[Vg,85eFмC*$jC$\.k~縰9GXlWL69JR1Y?z9CiÜdv瑾PX Y0+4~V#Y]?n2~羁O?go?Żwh \BN_y69 㓷iXM<`7r 1ׂzn.smn̦YԷ 5 ޳>(Oz #a0).ݢ]ɱN@ދDy&Z^LSdOB 4w}b~HGf_swͧx3P{žs|*t1>y瞦զ,Ԋsx(lENJ\0`+^"c+GN'B,vꐉB^YySlLT8sL R R$~c77xp˦qL]`p+?tJ?8ϗy(xs?7guhKܮX\_VL <>Ƕg"ORS[bL:w '̯!=\/seJFx$i`%?e"p߈&P)|%oٛ[?ʩePoӫsGZح-xRV߈O-ӤUo6 ̴`}z~_$Q. 6$D.wϮ&aWf]X-/:KaW:>Сv9+Hm_oy;\lߧ8ˏ/3VRN^gs>y쏨n.0o#7 *G# ՞2-TzA<2f.:TzYx7&Oh\G}ΐ IDAT'VnI~b СijFw&zdӌDɝN&D}*45V;8XW&WetVͫé_pcf38K;5 iĻH2UmҘTF>f D_Qiͮ峥)EQmӽ9^vD(/y[s]N}=G/V+,?/Z 홚bw__.q罦c6wl7y]&:H`n|f-Lɯި94h gDG\*R$^F/$lӯ&m)G$礱O#|/{Pߕb@ Fo޾?r7/R:J"W wλG S{:ZoLXKx8Vm[<ӃMxx+ZQcɂ֦nOX94@+:x~foHלRzbزU+_>}~5Oy |@v4!{,r޼ k|p>#rJ@H)Ý/Xb$>>7NPu K.QОovfZ[;aa>f[Drx\bcBr> NEle{Ų+<Hwd W x/5qe}ԹXTb4ۛؿQ6wN"Y2ٛOoݿի׸;|>!NHDHgp-I|ѰÂ#uԷCDOwXN_W=EY}t`Xw;VoIir?L}8Kk9KD-`&|D~y6(c~@kp Gun3!Uʯ">>C~|FJ'd28r;2(ˡj`9>kyܑbTlOqb1ጟmZ)MADi̍`~]EW6Q3R@,a/y\KŞT5j%m\)mr//^ږP:_'ۓXukT%+<&rEiDՖ w˿;|_?N;Dz[|_NѝPaÆў;_ib V-o (-f%MzLn݃k);$P;QR5q0zXWz[31La-} \̶*rLSq;fv@|S"|{__|?Ƈ|$Rrbt*>#R ~qʄZ1MO}Bn_{JzO;w ͔J]s_P|p yR1PL |`H<~V]x7,yyxRQ4vnpU2/SߊgYdt}=6HC!>J$kxu>/>«ׯp>tBIZYI;?;\9"D" HsvԛI:GTl|I% w__k|W>Ƈ~ ?xk1401xޓzvqS`]&|Oa|\yzcLO};qߜv{fܗ$lnwǀ;Ryv KD z.'2C7eb6]n#ei~3k9<6QˉB$R||0#"ċc"gtUꗙ9󄾺g_uu,"wm.31~}ڛ>vΧς|7gw mQ JX몧dRUXo+Ɠ7_IUm> 5(lRAitG^gU꽊w@Ieg6˶u19paLPRE6R z<aWWe55!!&W En=$2L=k{ P#9,C3b.ωez:r~BJ5 J؏"0 Qt)jI?W뿁J/3"L'SGx0iȘW{G`/uzto ^сHcVW5Um]K'X"~hb#2l.LMo\$zbaDwʥȴ݇W`G#}_3*~lx Yun))<=mŗvgsƖ7ܮox?"y$!x~%_;?;ӓn&]6#=3~]xD*<MABX)tل6EsUb֟J2)a ^1ROGX'dpE}O=60;q{nN|5WUzHTs'pt8 R|k\#to7cmg7a=-S+|<=MfYp9?am3ym eB0;9H kӇ'jC` :!wAyƏκO-VyG_=7Xu;YJj:yT"PIj k/v`%[3A6ϑ':sIŹF8AAD "NAh>I,d<&՝y>HP_2Uā(]dtNM _9${9|wK @HsPArJ:RJXdtD=N \NUm eH/ yRGE/~7.>}6^0>i-k ƒ5Urm30s~a5A艪EfGh3 XoӗqB`|t\?J9dثUAd'3s?XX"=hBҗ /)i}`JxGŎH3`[j؊ka_郤ӡJ]44Xu7/u v4n2&ꤜ³5i|13@z0LPAb4j7Ee*-hJ`al*7 wj{&Ddž2" *8Jtg,x+J LMAu"ؕP>w%Vi ۗ;;N:N\7a@8 " o'V6$n7]nGMw=<>}q7cS^}]) %6vuكDTw@:j{mkJ%TJf>h:;'")˰=w >ubG{P˲tSUvw,ܱJ:,:'4ѻq#9h  wgFωKh2u]$;Fv~y= @Tp!`YD,*>@ -2Χ3Χ3HI~/Ow7owy}ޖjo?4@Ox}g^m#PB 2Hu=YϽCfÉjLwrw{Tnx~ME)J#Jd clКp{?z78Gၦu prg'͉TbO}6ǼYw:]U3/6CnGd%fmͮʢGWA ^qF.H?o_} >RÓx Խ*ivQFirg?U7} I΋^RE8B£ɺy<2ݣjqجl{4`< mD(դ5h`P^*YŜѦO v$ wf-yFzK'5N6,5;*C#e*`̷#^opJ%AȳP"?nA@ IgY.'ϗbgOl<?zȜ N3u%ԓH1.~A@Gxxfo7 w5)~9Nk&y59|?Yc/ū&6{RN~W`|<~gKD m㷘xO9f`Lʹ35gԋ 9d40zh8%4'E}jpe'J=1:xpBBwPs6U00{;3[^qZ`f{;,`p^8UIv{ٍ#`Z<}=rj6v5y442 qw}%no~( tc㶧ZSfAg6ߘ,z{Qd?4]ޡNjQ>+?eZRyj]FdH=x|3l~2!1kg @ [zm< DǿI[MY׮FB1: ~ yBv[^)uLwjzj~ (+})%ѫ?X(c:_ mG`E}(!Y=RJuS]![^ȩZF(Tv(z2$u#5#)s|m*$͈)Df͟P8qfHok@@`OJg Bcy1[9,KS"3i}-`b}n"O2eC|jwD:)5RQ8FsP9]w:!G^f*Xkw6H7TPh"p\H4)664c' ك١P4H,c4X{ `9&6S߭٬i>zjF:hq^{m% v¡)FT1V]Z9@K8pj.n ~)F8`uDNu v|VG|ª?h7|( JIg̢!{*4 :8 j72M=) ͊׾JD? 9"hk"B nD 76F'h| KY%"&{'96z~ӕ=cJl׬;אp=#=W^{;oV}G3>Q?PWYƃ[Ӫj6ZlYܝeފ{|;-V$]49zuRKuccuƪ=$rXc})H r/~5?zJ?z A9&d  # ,"rb63jՁ{Ѥyk# \|J>-?!!4B@+ֽۿh ;4=G6/9蔹"laGcD Sf ?~+0|xoV=Yuyozdlމ6ߔu&cP󵓧Ce!z0. >*V!JzFnK;9Ym6ny,{q0ωz['A=E϶l:.:UcPN[k}:p3=*,7CvRYöƦ# pFERJ ~pzel[LJK L@D&-^cu5; DA5XjZlNg%lFT]z44FA\Y5Cg$cDk l|RnL^Uu7@Ј֫ȡPRn@DUh#!`6s&"lN ac[حCths@=iDbOE?*L*(=Fs:Cww\]5^e}u:9!]U#`Ƙ1^:9l  aH.Ez)yϪZ: >"W<;uo Xy'<>Sۙ~>p|r(~dKVbF +d6*We7a$v{p~c nwqˈoH+ v $gޠդ1v Ҁ4ř~ܝCbY*c~b}DhB`7eW+菅9r"bfƌ*x rLeMpdTW[V'M#={0~S8)Oit0xݤB7ߛk}m~ `ַ'zdbaIKC-o_!B. ΁&; R9{3J:zq\O;K&3n B'Ç'6l@4D1?cO3>r3v3whY6pAjvLDm?ǘsr=<&m)Q\B\{&}nFNq$4mp.H'x:o}-1@:g ={v@du65Agl(if$9AuΜ!o7 v?'?C՞py1')M^e ;g 67~$ӴXU){*o "fUud_?]]5d @mEZ PVHFT꜐:hYGebAcu 2DRN$Y>hve&$ͳ!|,.;}@s0W }~ʱmyX\\55p|IU#>r;:4w tm2l$mes:rD|&0b@hH M-:f"J umBK ymCݐf<\zG怀]YpgGUdXۛK[1ȩ9vNZEYg~??_mW5Lt]O`\h55ʀérʊ%r޹,@K 40QMbQݞu4]'^?$BK M3^ t,e LeUpUNb-dM^AU7D!b"z(ğipx~ݖku3շ:h4YlOqOl6c{ba'?y2eg鹘*xwnh>* U j z`"ʂL["zI QW`9C,Y+H1 ~trM.!tB.;dr^:_@"xd(C|ѫd1ތ&u?k|gfv޷j88-iUW "|7Am?}0&1S >ThGE{-V\0Wn}r;o7-NBss &1*? [72C*m ;ڤKQ%>&bˑM%+ lW< nflҜɷ-(!R^sIVex Ӽ4\~•lj@ !_ f +Y$B b7^頸ד`j27\y+5R zR,=ACKM@Vge5)ZY&;NO'o.졘dyt2A>LzH\p< xq[gR1.VGU]1lj[fƶm+7gWOyUm3h5Q I%,efXaRB? dW#L1u #|_Ơ +% sh(7f%-L*uKlj:Z }0`+C(0@"='a iGC!Q 7Y-:IO|Z d(qì, A lYc=6 ^oNۧw+ukkg xx%Xg_/:ťS']]O`6F OP:qF؊"OD(jpf-#ጏ?ʯÇ'\lf< MICT{Wc-6]=>ֶF=_Pm1@j(@Y3@`QG5?WwtKi%viIt*P| F:HT"pX mT)]}E|jrMt Ǡ@vwֽ@r.%`Hdzl[xޟ& -Dp:@loKʂ=xy.C^p Bd;vO:Nk4x`>վѱAhnL(^u" =s@ș<-[ur_?଒ o*0ZB=(9nQ2TO},f{s{if^kn ͝D{OI7\:]z$HǍ[H\FjOj+D^^}y3dyY^Yfm#/VklxjһS 3?sXb󁱨 rG%V# 1X4MFbztwL (Qi>||ŸSO/~-Ă,v  h .Mđ#D;{w-8o,}I)LL 2$ 5ݧOW(iX2Ivh*ArPm=갉ّ|x_;DIͦ^Hx#m !7h^^{kҸf=Mu [F]V*S &pAys:w_V-o|,< <;]}( Hm(pDޯpl6 SKgj€ͺQ P'e)gh_+ݞ KRy/+5fZ\z'C\;e%,FtM)qOhIeH |̻M%f\:AW+GuE~u+зմWR>,٬Vgգ|('5"l6a=4 1Lի$5fg[dFpQI*ىH>Iaq0s ml J,/U i(Y wAԪL| ,dX{$ ;Sp8 Գ z_/ŌF 3rv>TXBJx&xrZMe2 wD525;LJ֮1A(iG;ò9ۭfd}yfmhvc"Yv~sBžhHb~WW)nķT=l_pup ߦ7\V 6d04(d\wڱVj.g$j70ϓ&Q]W1IZF?yqI% 7)T?%RD3'B9) u?P" V4_?_U'Qc]YOFzN)`$Q"M;3n#bscwN@03 YTAtJj~zhDuuMa?QM};n_M+CY%R0ܪ3 xЏ؟]ֵFܔMX$Ǿ(]bckm2@0vqI)Q4{t^p4Z>n_e#ID%bBdF>H 6͸sG+3O>ƛZQ_RVA\ 2o2SC4dxl!6R&C`3W6*F*nEjcص" `RyKI$|UX_P@VV.<ey'*zjE  0ZG%O'Ӛ:/W;n{OͭFX“Mk2<\l$(Qc,Fbןi %Jt//JXd=ٷ+ṕH?. 5yQb`:CWcC?X)g(%pYNhȤ vo`۔.a-Gґ_e zQoIPRp==۰:J߹pjCEd ?bIbhd)QJZk1 U|3{>>N`g|BCJ yu ۦMRޒǬ4͠ԑ "Lop!Y FI[U4 ۑM(d*xW⹓.ጛQ\c3ħ݀c| $V^I3qڗ.v@_$ʫ =щ@Gt+9 x1)J|JxzJ*dUW:T+3pc5#yC F>*0m=e[u{󎷛RJ@ZiU52t@C šcj/f}ih&#Hc0yMTӾ%R0dj ^޷Ļru?SH0S (PW]S\UlMUUl_޸0$ uI-SJV-G(nEJw_JݛyUڟe=<3x{8:YYJ-$>8P!Umwx #T7Ki&}>i]|tԢ/!k;G>b55 p @f ӿ8!Ĵm 5ޝS~LkDﱇry؅'P ,Th,U+*2>U쑩PQF _@ZBii%|R'+DW=8HS?|Sr>yҖ|>AsI:q/DFv{(u Htc|NXXUQ oC]Cd>1ZUb!.8vvxcZ v8 zBkzM8%!o'f[yxJf$`Nq)-QmH0a-2C]azFSgȜ$J5ls-LS `L\zf qD)=LX;pIC.}=MiZJ-%>^N,xyg->xS /Dz~ _Kx>'\NRX7'nrnv);hi,$fBQ!9XBTꀨ]W#OA>6'7FvtJ ȝv?9NtIrβzF: p)u tw6O(4|/Ӊ8Q5x|i-4Ugs[MN~݅JJl ]TlMݭaFp :Ӆo- ]%-џH=9~}=51{ĒPGΐZMIHvB% -UtZJڧpo7M{INeÜdH#'z;`KRKxjQEC(cXF5C cnԷMf$D uv_m N_=~4iF&󈛃-$wisl=7Mz~/WyD,sLH=j2p=t\Z??f55ؔTx0+\F&yuI4z2"( ҌVXꉯB@ cj`M/wZOu)gvPe&><(c):qMw&u }>C.b4A;A܇VeFI(SQG(@2a1}\;JtYc"q<#n=[B^AQ::Kk\md^JijW̠ڻx]}wN HD蔪J?.WN/qJ%sn0ځڎQo\vaB>v%d5/-qzlst)J1ޤ!ck;ww,}$x߲ *#h[?Vcc c#$h0VK2_bFZτ9iX09l{޾l\،#z:]zIim@}%A*sq:j?B]y3yd$o)G)zxK 5s >]WW/7u%+A*0}a0ΛKV۲zbZo嘅; nt"Ҟ2Nqm_}TX6zJ=.k‡K0Rt; MMz:Df/BnOay0_I_34LxЁ IDAT8UWd'\}ÆwT5-f Ϻe2]y9FDBBmUlPVzՋl"‡Jg[W>>!!x1\7s4scN V톛=zXlf7Ij<1a]:yz`%qb& ?%LnYdF =ܕF?%QQBrsƍ絖np6iET"}>Y@l:-)3[ؑ8motX夞 0f!p$h? xяJ@! 8D\NOe(ΗsZ$P6x2PTQkVi:B5ֵr_n.pz79΂0~w5}pZU$Z\SCrXwKlzd?v{p\7f m2%u#9nj?[Mؤ>/O1NPjj2VJuC}BD34f)1`(+Bvs2q*8!aS5[RNE w7< ]th3Qvh7NpˬAm%58u~@<758]"ͬZgכu ڦ`1-02{v8tPG_^)"=!^^/qxZ OZT$$v|ݰ1:RB(~ΎWLA%1qUBsj'Ҳニfp=T|V۞>:K3L7|N>,@qxE˓"I zu @ONCES_v0T[^fRM <;}+3ƾY /{@%)a,-\P1fHڇ1ìŦA%S0yǙ/o):6K}k"|=RJ?^>[}fݸYLy>]5__w>,###1X |ZjH!G_7l:rB@=LRj{n[l?L0?p R<*eB5,MiA0R^2{ 3-n͚t'\=mz| `2M2IS(?ftR@Hu@C?.Aj<bWǺ$|$M[ޙ; O@h'ÞroMƇg%~xD744 c+E෦&sEKs5d:)#>u4Ky]řt͹6˟i |"<" x}j6 x*}˅$ 54WBj=Mϓ9;BXkH4Y×|g]8Ҽ) jVIz$cW,g=*7I8y}T XO@͢xb8qDM`Iwd0rNCJMgސ ήV`c"#!KLv߫xJڐo6AŎJΉć29kw.A{ޮd>^2I+s mz:%|8h,d$A"UmmM>'FLb@>"mgEʴvҩ,"`Iu0|B.Eq:<\jvR;pJ렞8(bÂeUê?zk"`.t%!'tXDhWrtP^m@1_%uTR/y.N5^W.V^x\zcMޙ-<#mLnoWBR b%,<߿1ߤ^GS3aX⤙H˽ }롤k+Fa)`oB^$nx駪\o2 icbaX^J[#~Lk\PIi AIX#IH[sρog[c,r`w6MD<DD_Sj 4,M*Am8P_M j%J ieuRl >n Sh[sM@(QH.eR࠷nnt _o^}<F&¤V W ֺ'-nq'HD9+3!u LᄴLI|8/.:h]~F~#jeTXpoKI%Y>!?C'e_C :SƵOT^Ѯ -l{́μȼ\M"A:#TIX*= If&U:-J̳Cd9U:'4lUV{ip6d4\q܃Bq:.g҄u*s6s]3' A)q >"-8:Ap6pnA;76rQ܂|J^?Gh"uLn>?He5M-`3>#;`B-c#6HQgcIFtoLi偢+ rm}å((Ve$£ Τνr 4,לDQ65kTmܤ!al+Oe]Ֆbqu+*Em:GQ{]ew]qisFZRl1*qK~)IK(rj ?&.m&@ܛ)yZ }ypz&*u xM&^=iT7`iM7=Ծs^ :v,dAw2}ey%<\-מyT h vh;NG}f)'tQ)WR`YтBYQf2IEA2gH]m$4+P*R+Y5$%Pf=E@$U%FDW )ϓ7B*$h\b̐TΩoTm(f!2μ>TUt16{p6NE~ Ɲ9X`R;K5HLIF𖒃o#JdE\&Xȃ@W^9SN(߃OWESb51>?%P-QTb/ڙ$4u$ ϳfRuW–.ëJi_4x}N tꖁ,m4m" ^O %44x8zM^'jPRuTe_k="r,M/z~`pӤVYRUUbG`R5NG18(a cK@bu``)Fˇ,Яo"8:('v>P `|P8D'*a6oH;֧ $|"||!a!s4Ŭ3|iFBw})Yb Uw[ uj. Ȱ j9׎u#us½:Z dyj4O['k7C7 PSکu37y \'E1aFw,9 ~2{i! شG:{_ֱo3;:c#fƲ$MQkQB*%PƵʥ,& Frb1x!ͫ:xTo>'tbIAI$8I@qVDPhIdخtQOXh`FPB]LO/({bzycü4븮\jiPv;oIsw="PpkN,8[魷!!~(pRM1T?]@ѩHN,{R3t,JE X2 63Wqhb^ZS^$?Z['E4ƛ<i"2Mt5<04kE\JfJ|ߏolj$g e&C=r]4ݔi%eɑoHY,@b&?%cdbiJ `jdhaQE!a{TN6 ihqoQ8ԁXyaq5F!Bk%_:"/]F=?|Fq]c}33fmIQ=rruEmg}D|&̋LfH7k,սTYLuxq0%h;%0g$SFQ DlŤ8u$-\!H"%IbuC"a$ܢ ${>VdEXYJI7&lj̶S;@a0W;6@S$,E lxSZf+֢:N4RsNjt}GEJSA՟ܛK$hR`֥Pm "[.iHI5M$* *&<|R&-01UYDT; CHH@T#!Mi9D*˿ H @yn{2Huˤ(=<`^+i~< r]5gY.%j,& :|x16hd(^ @ bǺ )r3j\ "/{ˣwU5KP NIi\H &uû98M(%LP+QQm$Q*| h6A@Y2un*Ceˇ v oPG@ 0%se Oǩn{+r U|:"R5*P|85ۊ` eesH"=n0A=* Rؕ$+@W` !2WcE R//ZO8ri _EIp.Zɟ^EO7J,pxgtNhW^c䵶;4K֓FOK=RE4~iڔQL|Ig>.S>vh},G31 zYUí=n@km K0&.& ˔e~2~xò0[\a_ɃoTtYa:%KX\3)DlzWgU8;Ŗtz ]'zp>mJj<8VI[9v:+hUJ UC[Fӓ%hzcn*(x${V q:(n[HTc㱙 N%U rIT>N4:mK{ݲR>^. #-q bR7 +ö( 1]N]1s=(#`ܾMD6iT G^s n[CjOf`L.fq4ӱXprUі/QTaSgjJO U㏾jXg}  v)q` r{헙tT%J'mdȻ[ nƼir;`v3W@DǙ$%T6S*Ǝ(~Jt(wq!-WPj|Rw&8gHq$sF`8F1;)x2G$5#xX^qh$[/m@=\*F,m@*Uf :@Q 4 UH*B/aDR=$Bѯ2\6oQ]OGDY1wm7₡Һ[߿fԓ&؇@;T@hT8#~ =tt2o=<Gr=_Rsz@C NL}{/4|ӮaǰA$x^) [lE\:Ɯ#!LTWȠ\2u`(bRvQZl`>:*].}K(0 IDAT#"wb}0|x P*rLg7TNAbˢBr &s=2~m6,:&aJeBH.Qfp8 _AUJ#&PRЃ DP3zzPPqZ.A>!w_2`&bZܩD5kmS1^ڭnh[wi^!8=_[`Y1ra~ uNjGs/4[vh3П;f6A±ATf1&JN@Cwku:@QoPL.E==h4֛3@كIMnW]kUHEkDlt) t4:zB=[Z)P>n-꩚GUV6Z%6^^&؋[buSb@b WMKZb!5(\Mg; [h&I% GȢZ}P1G؍{ [vգ/x%T8C4t|pF.'ZCKo*CWhB7"i/ߕ~ih>~Vy9Ǖȃvdrs׶SL$'[fa~O!NuPɶz%bzq"#t%/՞iS˷IOCS4e*8w䔀r юL@śn&h2=ʃBo13cD <R}bksa\B}vQrlۓ +"b*'Mb2D6NfsdЭNI ]#`N]%DnBzXGklWr*yj70s0|֣djTfVsŹ' ۇ@ԥPMn 4,iNn]9eiᄐ1v#cg)v=Z N#ގXbsdLo{^x?˻ *zaiJD&pSxeF>9p|0,%͊kC1Prqlfl^m!%A4Xk4΃x<ifD9JAO$3А5 CW |SҎ}FhYtZ" W6`${k,.II`5>فe}+%76” !4oc)@ v=rh$LK*"sDddR?.A #s{нzKk(>1_4#1wޝYݽ@?:WC)zZq?v Р`b~]IS"  :M x;jw1Y2d124CMf^AvA49U U9TLKZ6d 6K>]ҀbCdk)FT``abBq\,Xסm|x|G1Dl2H]-rr+'蔎0HD%43#ɋ#t~8 K*Br(ְI"jA|CIi- bwNu%eaH&Ѵ*!\;MǑAqǸeGY;?ӑpl{??zJw- z?'xv;ЙOxKnسG؞u n+3egy9jp ?bxEn_@IW6b`R+Dt= ~j7 )ĩi? 1-A5w5fU BM7ߘ`VqMlLz̅PCq!IkLÐKh>>9!멻34PS )NRUJ^̽4r27C&[y>n|nV[fxϷʣ򏏎 쳌P-mQK(O˵1]l̦(ӛO/z~ƒ^_7 Գݷ_ /;(ao nfXv[jƴSKuٗ".zCt* )?^ *k@]`fi R2*av̝MROAIЂ9[>!IzYH%w)pT 2#k{o߀AˑUO\\{Oxz7Lg6V[r UT"gN/Hf*RѰfФXA̔s="12"̌-I6= mJ^YU.|̕Nc쿙?Fy乖m Dɹ20]d{gRy.#=tKSݠ{lΟs|t~pLH"t1_ZV˥~^Bc 8.tehEz I9+e Z"Hch.5JCO6dkmH=n|r>81BJ SvIiOL˕ZUh K],_S/ (ܷ5qʤ \YϘu ;BhSfhϓCTQD6M59G *9 2"$dS̻!'w _j-^ɀS$c\69ySȱ }Ja$.ߝp:}= ǞX= FKg?NuMc=!<8#GݝTlg5ȟKcًQ~bJx1CJƬv~:?Y~$F+O{>4藇Eq9c"\4| "J@2!Ik# -L wިщ[`mzG"A‰w‘^.݁a~g,f6$B*ŠiHMzX=z;T|Hi-е0I @㴔Ɩ 6Bɉ$J!ri\ (NHb>lnGm ]g]C)L:iO+!| JOD 6z.c|06Άz7H 60/ jtẴk!Lw=uzeNTNa@!-bĨ`Iy|ldzƄVa$W`tnI5Ho[dySP~!e=_TQ^a^3#6p?f`֛WaTx@-`Xʫ-IZH*ZБXwX4t\pb ڏ:v|=q6:#g_צqXi 1ku;`}lfwrN1,w˷\?la4o 2-Et,yypV ώ.SdD; |@[HVezY*s v(f(!Um(GVik#ˮUZK/ˢTCZε׼~zi5]bIt^<6޻kn UχG)!0EAEBm*5gPFؗ_#\=d!GTh~M KzZJ0Ko=[eFePr -ヹM7/i,2 ׍M"C 3ogZXu󼹏ͽ\YV& o>w/Ϸ/rz R'/v̀XkR"=e1\U<-ĭl _ITH'CZRUAk~i7ilFwdў??@=\TW0vNy+i_/KLec??O'hL VDKÏ&<>9-=laL9f As?ٚT%5x I&`sAKOi zkQ zc4-YЭqvR4)K+ DgVyl\ob =5itk-d[dx"@F]J6Hbϕ.+8U4誸?߽n/.|@$ m;zNwj?nGH}FC~6v]0.^?̀K ~18ǧulb++G???'WG2R%"iw4D+2G }`)@DDe_I~\ecs&7:V@xGˈERyrt"aq\ޞci-fDwHѥߩdJ+ә]U ,+)ۂO yҀ{+.GFʉr^a̺u?#j Yq6 åLL]"8JRːo?^Iy::~2,^Dͺnɫ.k0^P ]*mϵєAiúEv!ˠ)2CD ʍjETXqFP 8r#mUs^ ^g~~$IB6w&hOy@l9aәo__i>);v_{_ڥv8su>'0Əc_B-㸳ؽOc^zNbLE0b 9 b\5ڄ>n v`4.,|:>MƟ)A 3YR :xYB mCtpJ (ff6rH jd.`i8$ 1Kg`E& <UU/J Eg^&ޥ/TЋČ`L:Wz鍠j8\RdĻ8mpl/;̟9ah[^Kx tpDa]_jϼ`_z] T'\Ypޮah>o)Fz|?lv;?U~:~2ew}!Rkǹܧ\P& ͐{]Bu] wy1+ hĈx>nZ|vd H' ҵ>K=4_\)}׉ u2izq-f*tګ Z!)vF(CM>ЁKzICN982)7%5V.Y/#&OΑj6oFKn!s@Ae+SLD5A n3 \ܬ.OTTJSGV`j:;@:+8ų}Ѿ6+o Tc\⫛U u~d{zc7i `ߗ;ݬ-Sڹ sZ3n?\/Cznm;au)筹y'sقʭysU`x[h^ig7DydoiMJFkg26wxw`P@_~1iuEԤBv`pXir(` WрK)0# ~:AHOp&܇4)6uadPH.*T7yt r6@$!RM8hSuλ@)t6GayoH>H738yTeH=F[=pEY! {ݦ;mc;6BB޹׷3}[u`g0@й.\k3:pSA{v</mLϛc7ǃ{"v1Ljs%f!H g0P~Sd8]ޖ?_%'dËI/QoR+5Z/4T:F.-\C_̐,yJe!`( hAC>;*]}*^;E p2i 9H7,.{|Lѹ L|јOk#R錩 MPV6Vj  IcʪIW),)g=AԽJs TE+Kf։ej IDAT邐)8._O/mQj'в=>Y9)ZO@{N`٦uX{v"{ |Q- j&psvYXwɏ5p[r:u xe5MG?c6,t|m^ՙߟ !Jfx)%E<p9>Vaв{Y^^1(J$$5% vKfHo9{,#G*w^ @X2ĵ %<.ASQ"@N@ڒE;ZwhXL @d鮥m^N s'4z&uy 1A k6Q'EIſei5&$Н9U." BNmE>5Oph(Ji&`Fm]xDb$:g`x۟Sm J-0~/\/$?o+ږX x]Uאxt*Un[omfq |B4;.]DZZNj@ |rO_bnkڥ~ U `&A-y B`nx^׵3iM43; #բŤ6w$́1 fYe( `Y9#_7TgEsfLxţ4(@+JL!M齁H?!e 6U\ %ٰej+T7DƼ,:T#(2e-CRثGsYF";qfہsihmO["=mw>g s4:+ވbz09?^ W1~f!sCb~ч/;NU??^O0iPgUܸʛNUm)̥fyqي)Ȭe"a KܛЇZ?y_[e;t4G8SmxPݵxg<~^ms*ɠӾ~Aii˸S`Jtz.I@fLpkE^^&֔;~{]p]d;A@jQ{,l0tt3_G wTpF-9J;%[+54Pܼ'%B}9CxÅDd|Erkb&t p`%G{~ @IgLBS)~ +f3)Z_ǜpfmF@Y4E;WlZB|-Kzs t6fVn3$o/` [{q NwRKԯ/gzʁWۜO"QfXҷ_~7uIJmd=C 7MHlz653:qh$_e0h~p|vbtȘqP &*d)TR{^i AVRCy*/V㖟&]f` sgKgҍ,|MCvr1('^;IJ%-ڲ/F̆PTZMa:-g?#C~Iu~因Zɭ=^Tv??%>AUv5y@ht6=do-T]n+؈~~brPЉ#p T\h`V0?/=KgCʪcO3Kə{0we&c&Z xdw Hkm=@CdH+[k S47;AH7EX z8bMZ! [D?s=8*y uZJ43xw,YIi_YF͔ _&xKv+ ]/E_abdμnsm(^TVI"d$cͦ]Df=B[;5"6y,ocwt%38-M%tyޝفxI'D\߷,pQh#mNA줡kT#s^Fݔmun>柇el0Sea8z|aH}i%ȫ Z?? 8H>* Ԙ@.l ?7\iH:0m`6"8FiwHzz/-r?2$*' @h5WZIE[#dbN ?m"p%7IJ:L5)s&׿LJ_k!G0mm%#\}`x]ҐYhjAD]mk05l`JoŶt_*0c77"Q$=d90rD,WVeHIYa*%zZ&FCy12/X.W2)\qc.$T LY/ΐ Zs{IɎU2{P恬tJ S~6QΡP*!ŰEq dYH`ܱM:@9 *vm,*X@e^"3r/B)JgX67 `?7'7F6n lb⨓/=}A!9jΚx6&vjAbj |nUoJ\srxڴGkB__c@vA< s{xY*$N26i R[0 N폈EI_id)Xm"yEG"z)UNc$F˫!@SAY@YC 2J"ׂd kēG`#ZGHp \26o+WeJ`ZF#uغmcD쁑;la]dn(a];bʌ @-X =Lj.rO<!n!ˣ.]b5|u0м&lo(Psd"Rŏ޷m*{8`w}~`3~ri4ڎ}X\Pt"ٶԇdOF*`˅*ACZN% J (X%Jl]]Tj2jѧ S)ܱ|:7;=&e L'p G4as}˥*. ti~'p];yN O5wڎT~4q??3m^Кr}dqpf}Җ&E}=<74(Sv09~\``nۊr+{H+CU*W 5x44#mklreIk@pUP,i 3zސ,i+{ߘԔEZ'>\s7\R Dd bc6OsVt," lIxF7b @\<׃-FI gdL ' ZIZ"263gmDc6T V9#e4Qg . .3m8B`/!Lm{ N:P ''}Nq|nO3Yk3W1s`^X:9ɏK}%p.^K?yߞx{9]*P|A+`< qmCWNk/|mJe:),=$b6~>A!u5xex ɠ'j5X3- gr S{.䀺gi2: e*I*wJ;RJ=L'`|VYC9!LMe*Af r\0TnHG ~J?HzHݣD b̏yfb 9*2Yb)? =C hh b #Uf" CgoTN ZI]ŇB{TmS\pxuv+ 杞<81)v0j_`hп)I++Pז{آ `9pkf$n=oێx]7hr]}xf@W9iVZЗeʉg.sF.U>aAD#trdp,h̖2i.|-<*zo ѓ&203EݓC 3@l枞|.zuT&-Us!o2e/C0)wn#@,tHhS/{d '*1b :LG[[mrÎƱ(DIJh[Ys@eڌmLئȹZyi4.gk`F9CS7Ӊ-?8k0S7߯$-rRMdӷqrmcxm/0CJ7hG99ܚɫϓRG]1]`LMnb2XvR_(cm$M'F6_1nOQT}2,'EG9i_Xr&Jj0PgziY<M}# ba~I]f e80f@%*P! -*w.C J}0Dt< '/]krj)Y!P4@' Qf"3T:pL='is4 ҝ2<燙E)=wyƕ5ۆkt{vvѓkfX4>J&$C3UIxE[}tn=65S$b3K;t~Zvu'N Urگf_=;]?zeb;hK5,Z%}Y+K]oH3D+[>^]s fSR>]9ڹC|7Y{tL@ya@A]\ AIQnҸmHr$4wM&ԨhZyI]Q\j% ,r`s &F7q52•'m2ý l6 *W 0bPh0I'T6>8׾R =`YV{tZR_˖RXzm%ͩ6AawH cܭ>~} θg;y=I+H:vgd\햩rUv |(e\o_w,[0ݑEJrbks|iD:-tMx`%1t>Vjtd ~:4(TD=\BE *tH ٜ ]YRHoGT~Q`>-@+K<@VD03R^k~r i(+KR;T>}C6FʘCHT`i l iP*+X.t996k#1"\k۝zmF{e321gtCCdv̋"Y&k}tUZ@fCc"Q vT.ȒR[g3z1L߁?#095kL)d%j'TÚ[Cnٯ͌ Ljì3glK3,* Z[ހW^z%GAAY0"a 9Y~;=L`Kq6VmQilN/C͕LjV->XsPF86ҝC8.}'5])( R!lg; Gߤwe1NBΑ 2Ed0_EIUOf6a׍{qlp]/&b}X RLQfF(̬8hQ螎>С Ν;+ g4<;eYg#̱f--@NRRBX,*HJf W=])mtv$iG&N LSTf9$C y]SrZeAd޶92F8Fi1QeQ:d4h`R6&8RRB7HʹR)PPӵG$D1!` qQA vd?pw \HvB)3iw4Z?>|оGw J"=DVr޷f@VЧ9Q:AcQ_uדrCN;5G݉#z!Wj<9hGK߀.Դe#H:T/|\ %*=<鐱ḛ0@0\/rߐtVQC:COaɄW Y0ùCX|d D.!fvtENt'$DDC[HPQB5b([yLc`PGJ-wA|'N1- @ fht-1}y ) nFйN; |\ ť/v:2ܞͩM˖629ԐM o|u zal#,tB2RO#jfh^ݠ̷[}}҆XFԽd1'L{[/d|y΀uueSy%#=M 9+#Kgi+ MU:X8+Z5%8|I$I,e7ΓM @Ї iLFQYW.%(Xdce)75Ys M`$LMTJmm[%ORN_,[eVURernn3rG`5-Bю<؃oN}~EǞGoei/皍c|jVCP0Jx؞||Y &|pQ*jw: r^۲u]+ڼ2.VȀz%&B!ɦzm*g=7|igF{3k=<-&ѐ҃؂)#6@z jywPUhIj|ڨ 6lx<FET;˩-1(nR# KPt\+S4 TQ+*1T^ (K@Xt {xvLQ*l.cl[|nzd$1 *jjL5&0J l[JY\g gšԂkjX@ 1?ᵜt#}q٦Z`zrA{D7?pi[D@'8RuJ,B D|ߧgaYNYVZsg*NtzηMV,slh:ܰuRt첤ZzUo\ԣ(7DbT$ݩg-ヨrY4b䣰EaḷHqʥfCL1 c19T"<nG 0 _D#^'J[MIM4 Y (9КAoQb&, (./8I9ɢm)e9Mѹe`n 2UFY[bft>zZ:mJu0>s =s:͘ȅ8geζAqv4<#?@ƿҎhW^fL#÷Ozמ6}̬ 9ч/6㾻fB Sy-չl$fNYr`3\9½a]gź-}؞P ⊗!q0u}>yɑ*U3, * ථ@`SjYf]`+ 0XiAu]\鴥URYfSOB xAKKd= VD$fLPvӇK֝`r2[$҉~Hދ{`_C%x0[E`pI]4o&pOp]#U@QBj̰RBl.*Xm\%zCdgڂ.]freIM!ڴU_;X1P৕&@5 LRY(>L!eIRX@1' m<8zِCkdZP Ξ/&EƔSP6s@>=Z1)9 XD35by"˗aⰦJ2KDL1eGBUC`dRh ڱrjokX=M2 x&81vgH;xÃ4!`ad ,_.~j{z;l?}^ge$K\W3jT?C VL*-c1E7 9g؋cm/E]^SK{Kb|7ͅ +7hq5&A0CFG,RO5iJ:@V@VuPa 㔹(aHK5OG F0+ e",Z3{v&it'PBHgusARF] `!Z B#umڨarR;, M0[miۦX\'bpi4\XApTh+V-U'CIFkAgw s;PYf[ u_J*nȠΙ)UdGBV,EwT-9]Q4;H+- ki2_;:nפ@es.I: w" <-#S%>Ĕ-/15iCaLA'{tB QgЂ>LLRR6P$9 =fH-2LӽkhA"$o&k8K8R nZi b q^͹eo j-S9hƶ$)Z62$ٟ7;nN4)L6YݓZ(EH`ឲWN4 ;ˋ!cҌrOuz caK18#ךOG0@0, :q$ކ6lI @Es uピ*(а(% T0R2URh1}>+["=AۘitYdexi{*v:_K!hsI{h *wA *u 3"9}\jb~n>ѿm@-A:l;tZ+Ӟk,rNˬ _&?<:OhDڵo<"I("&ucz\:lPdg$]jKGlWiHL̅픴ҋD{@Jɔ9mlH0 5\7䜅O$kĐa[{2Z7I=0Fja#P[YLI9a!0I6A;prIuwE!nqX69k#Y8R=h)[e0}nz'2=h)\9izE~?atv8=YhK1;9G.SZg#UOy8Gm2GRt<~ȂM`(9tEѶb⊎,F,\SA w)b {9%Wh\lmξSzs<fI-nބՔ+-'&itKV~(U-i0;Ja 5_2)FVn ܍Z\R,Дqc҈4w%A22Z~RMf,V |?3C61k6)Mא}f{H2ulY;m_Ԭ*Lsky $ou0órg* RڙML!䘙KOOj܃idR zZ `coA? 1ѕ7@g϶g>by߂ջ{`Esװ_LSƱ`[(_>`)LXi5ɌR`W\w8:Q d#7%#{7J?0ͦaE蔂|gC 72dǙy|2GVsPXDx4U&or61I&ܩHhVצg)XZzp%C_ \ޛ2K5dSLӻ@Mux`hADlY} RI]*JnٓezY|@'7gӻ'jNw_]9h?6Zrm;gTXG8r${+=u=m5쩎hb͐h#3Ǩ_ 7YC%OL &˱ٳJĦMNC5dBGLUFh>lh3#aӜt5\H$Ҟ J LDg2vi)x({9\aQ&s>xCA8 :(1>j36Enm۔> niE%9/08Z뇻xy{鎍cΜGmz0O>o#ဇw\?5?pu?Aav2Uh R&Yl8r>͗V6Qϰ5m/hC%D/ؤe{l E$8M"R Oor QtC k &V1:)рRM]+3ӝ^&3Δ@ vZyʰF28: >q)r^HBu+0i'"\Է!}.q<+n%|~kOن'vSS&n#z~ػ;tS26s2)1 eSm! M F,u#>XMԓY&(Ni"5Mbc)BQOI:qxP~<}/9V4T?N} *lhlrAKY=PSibju$#v' F& |rb#]IJVQZZ1a@QwJ/ySU98eGu|:yػ~r!oR]qԳKgGH!45f;HFZ!'i? '=a\/u0(njdȠ؈`1'$!ğ"Tn)nIENDB`Charm-1.12.0/Charm/Icons/CharmDSStore000066400000000000000000000360041331066577000171660ustar00rootroot00000000000000Bud1    trNon  @ @ @ @ .GRP0ustrNone.bRsVbool.bwspblobbplist00 \WindowBounds[ShowSidebar]ShowStatusBar[ShowPathbar[ShowToolbar\SidebarWidth_{{575, 726}, {331, 258}}".@@AADDDDLNNNUXZZXZ__aaafgggpptwwwz{%%%%%%(((((***-*0002222233367;;7;;;==@@ADDDDLNNNXUZZXZ__aaafggggppmww{%%%%%%(((((+**-0000222223336676;;;;>@@AADDDDLNNNUXZZXZ__aaafggggppmww{{{%%%%%%(((((***-*0002222233367;;7;;;==@@ADDDDLNNNXUZZXZ__aaafgggpppvw{z{%%%%%(((((((**-+0002222233364;77;;>;@@@ADDDDLNNNUUZZZZ__aaaffgmppmvww{%%%%%%(((((+**-0000222223336676;;;;>@@AADDDDLNNNUXZZXZ__aaafgggpppww{{{%%%%%%(((((***-*0002222233367;;7;;;==@@ADDDDLNNNXUZZXZ__aaafgggpppvww{{%%%%%(((((((((-00000002224244;47;;;;;=AAAADDDDHNUUUUZZ__aaaffgggpmtww{{%%%%%%((((0 # ####,U__aaafgggpppww{z%%%%%%(((0;@@@ADDDDLNNNUUZZZZ__aaaffggppmmw{{{{%%%%%%(((((***-*0002222233367;;7;;;==@@ADDDDLNNNXUZZXZ__aaafgggpppww{z{{%%%%%(((((((**-+0002222233364;77;;>;@@@ADDDDLNNNUUZZZZ__aaaffggpppvwwz{%%%%%%(((((+**-0000222223336676;;;;>@@AADDDDLNNNUXZZXZ__aaafggggpmww{{%%%%%%(((((***-*0002222233367;;7;;;==@@ADDDDLNNNXUZZXZ__aaafggmpptwww{%%%%%(((((((**-+0002222233364;77;;>;@@@ADDDDLNNNUUZZZZ__aaaffgggpmmw{{{{%%%%%%%(((((***-*0002222233367;;7;;;==@@ADDDDLNNNXUZZXZ__aaafgggptwwz{{%%%%%%(((((((**-+0002222233364;77;;>;@@@ADDDDLNNNUUZZZZ__aaaffgpppww{{{{Charm-1.12.0/Charm/Icons/application-exit.png000066400000000000000000000015121331066577000207210ustar00rootroot00000000000000PNG  IHDRabKGD pHYsvv}ՂtIME  )xoIDATxڍkTW?7o^f2$'u\4?Ԫ *Em.ڕJv]A6]tbn\ؒt!313̼_yxt!҅spUk~hݛ}[ `w{je匪_t/5E ʹTΝcX$􌷶F$:@'E|6aD\I߀"[JT6Cd1:Fg0Zby_wV p~r+s( Ey_Q[¼ m a)s [782ٯ.&m2dȴ P7VjrNr*ji?1^.i;ju-_7ATV=buwp"@Al [YB$CZV;[_MxPZ#*jR!:yIENDB`Charm-1.12.0/Charm/Icons/arrow-right.png000066400000000000000000000010171331066577000177140ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYs:tEXtSoftwarewww.inkscape.org<IDATxڥ?KQy\Ӕ(.!8j@ɤT'3ss&EQXA(D{ߣY-lK++?cc[ɂfPn!<|8iP,…f$?IA!}@^T"!7/gGHY%A@[M㣞-^BC; +ȆhK o=VPl`@Qpesc80u QzZA8p!WCƃ_͑;N#xBqKpgW6 4" sUiTgxv5J$ZY{TNΦ8I(!%pvZU.}ʇ@4h57=K3KIENDB`Charm-1.12.0/Charm/Icons/back.png000066400000000000000000000006241331066577000163520ustar00rootroot00000000000000PNG  IHDRabKGD@ pHYs  tIME ƒctEXtCommentCreated with The GIMPd%nIDAT8˵1n0DInEplK &{)@Q@$3߷GV1 Ð4O{fImӟ|7Kϛf2mt}5Lw}cKL@`2yD\aecL2A` 4Af$< ^dY0oQ^xծ א0"g.M>n4MQ96T]Ihʫ\jUU% >MS8q sy3SY(C'~оIENDB`Charm-1.12.0/Charm/Icons/configure.png000066400000000000000000000013151331066577000174310ustar00rootroot00000000000000PNG  IHDRasRGBbKGD pHYs^tIME7WMIDATxڍ]HqƏFldEGaBY]Uaز.jЖԄH؇5c.f`F_LPY cn6vkms<AL$b++H>>h1B>ԍC edydNkuRYM +_?Q2|NamʶAnXp8 T ܝL&X0t)<@*c(B!pUU~7%k9h[Ys/ۢ_y3ss's-D>`? W70ذ>tW)mVuFIXOA`Lhtpp[{k1s߇\.d=^k8MY_gr7@4x3$Wȑw‹8R`^N[REDly#O8ChWXSM᠖bL mu߂t:-"IΨ1HĚAD[|5tjdP*멁=SSq.`Xs(=&6Wwq`,.hxٗ_m_ BNIENDB`Charm-1.12.0/Charm/Icons/document-close.png000066400000000000000000000017721331066577000204000ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|d pHYsaa0UtEXtSoftwarewww.inkscape.org<wIDATxڵohe?[tT6i7{U|h E nӕU¾o9G;0[ZM!GմjMM$4m=Ls$m _y ]׹|!ā+di_,4Nph(d ihhp{t$I`Ѕ%MpE8NMcdè\6X~oxx!f 6ߏg_~# b?҈r2\i1ű1 O}0OtʣPs8]b/5(76 ECaN) ǪR<_ oì~8ؠ\m}^F.4t7XZ9Ӷr!mx.1O@ӳozJB´.- }}$Ϝawk~}u2K`4wJ062AϧЪa rGS")p{(Iq2^+6ˇ1dݼ>99zfW(ؚPsLn:-t:12rh0$tr6lXVdYcVn\`E2L cBH$5B2nVdYsssD1F?޺Ѯ`QhRaI4%Y&_jolb]Y]Mp`0jOH$f /"C:Yͷߍ*o E>b FVeXqp8_SxC?ym孲KVOJzIENDB`Charm-1.12.0/Charm/Icons/document-edit.png000066400000000000000000000020451331066577000202120ustar00rootroot00000000000000PNG  IHDRĴl;sRGBbKGD pHYs cytIME &QIDATxڭHu;8.$e4t(RP\ID5"Jĭ$dfkD6t1λ}vu}r'Þ)΅֖Ux PȾ=/Y{w\Y.kڬ34@<;˨y㻙H '6G"HUX p:$ Yckj$zd:x븙y =j4{L2)ZH?QN;k9[Z˲'G^dK.:[5q)`IEi:F0sfߞ"i>41Y:V&O8MZE`PD,BW9;jq<>F(@ &Gfrq>B4Zqk[IT +Hڪnnl7>L017hXJDh!41YyOM3H,` `o//`+|1糩jl_CLx)|¹q#Ge*++ill@4ؐ'Њ @GL\h?g$ZbPXǁe.BLzEfI u/OEE XcRs]7Q=C&SZR e/xQ,81%,[z>Ֆ *u:777Hnq;PH"s Mӄ" xܢ]PP t8GGҒ#* 'NVkgNv] j3B`,pdAWfcrAIENDB`Charm-1.12.0/Charm/Icons/document-encrypt.png000066400000000000000000000010611331066577000207460ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYs:tEXtSoftwarewww.inkscape.org<IDATxڵOkpǿOLlR%NBKKbf!"3}4MSz^׉ȲpTV},{n6Wa_ Jt:}\nbL&Sfo*qevZ-?L'bt:󥳾GA"'t +fY϶ퟞKg͏$8Ό)0 w> Du4M'>;ct5p2 ȩkhv `)=|' UHF&r  Z(?U]`#@ a29'kTS%ܻDeKIHK%~Wyq=IENDB`Charm-1.12.0/Charm/Icons/document-export.png000066400000000000000000000014711331066577000206100ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATxڥOSQ{}DkStЁ2 &A(F!90( N4M00Ib& 1c AhQ㵶^Ͼ;xsߞ{_%!m]\\&A3܆,K$)Q 57HzO C`b0pzd2@ #M%OH/0 Qh "Ȥldx(O0z{ o---%@b6`uWWB( fgg^oxqqtIyU0dJp{||}4M4 z Vx tOXV1<$33_> H ^CAU9&ZX|ݿ: 8<;1q A jp|Ag$'] z%_e׬Mr\[ '.(pIENDB`Charm-1.12.0/Charm/Icons/document-new.png000066400000000000000000000015511331066577000200570ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATxڕk` uXv*  "d/QAt? VwUP:h=̥6ٮi/n{ǛᗗjBփvO {_$$ZDKn!7ma=d7Iah@Z8@;}  ] ]?S<,Ick@w&{S=uy"LN<=A#=g!$_5&'2.W\~>3 G?G"C.|{ٲQ(N _s8|$/@W]R-'UHa~74Xףk8omoUѽ&r(ʶk{ چ4"Sw5 "Tj+Pmq=yo' Nw"~w>!vI)lvBЀfwV&wSeuiv;,˒."t%#7 #̂n¾I,// aT*NժNԱs}||G}kڧlW(PxzҼvL6MM4KDtr98xB<~"7ZlŋRt(bBI1`q(Lq9c^G!}fhKh.%"$?$ȘDlÈya͚R껌0bTNI)åͤ/w+ .fa`~FV$#P߹ Kq3^n$á͚mϺ$(0֞lu DsUC/4<=xΕR/qo]B}sЩx6ͩzN J{NZ;IENDB`Charm-1.12.0/Charm/Icons/edit-find-replace.png000066400000000000000000000022661331066577000207320ustar00rootroot00000000000000PNG  IHDRĴl;sRGB pHYs cytIME.bKGD6IDATxڽkLU_)^`0-(rsC2.vVPrDP]Er *݂Sa&1BLȼDCBo,?tA?\c('yrғ_r$I92#^눇"gw)Bb()) ===Uii)ݍll'( ܮj(B1?Eff&~{N 6lbppccchhhBlFuu5L&R)`ׇxLNNraqqqK(%%eAPPPZ@DFFҴNӡhllDVV\KK>F Պ.KOOJBEERDFFl6, ::;J; WZmA 5-_|Ab!B"O k a} @KK3!RCroH[qqH(ؚ떋9\6e0W 99`0 Tq(1߁Ls &5 =C$Sb,q7|Ik9_GoC~n$UNNkIP$DAnC8LǑ!&:exsp._J'}k4f2)rx juuO6!y5\ \Mb߇#P]s^vsJg~rVVVx}}SSSx6>p_ Gx u۠r^kkk 9*:!#4:=7s?&bsy(>0Usc=F?+NьTN碧BOJ Qvy.'M M/v#Rx2ڝX|[)IENDB`Charm-1.12.0/Charm/Icons/favorites.png000066400000000000000000000014021331066577000174470ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYs:tEXtSoftwarewww.inkscape.org<IDAT8OHTQy4f e-6FA`AED\U ZժER-t-ED R*jFfF{ФB g߽1W8pjb}ضȦNp5ߊtN4uyʲOϽ>QsԐ@7To5Ɛ0#:ҫԾHpךN/BPS4^~1ͻF0W /m{'PJ % ^o%WRy.xvUF%״a' 8`|d4cG鏣GZ[5*LA4)U^yߛ岇f-xLʃ.Ukl$Mfv|{\c@K:La kJ"?7OOVSX֞"Z*-L>VؗuW 9~](]]ʁNzD(K`C5#B DѶ! EPP)ى s&– vHuO297LJ3{BMgvC_[z1p<<# Q8 M楚v\>¿{~c.E}IENDB`Charm-1.12.0/Charm/Icons/filenew.png000066400000000000000000000014711331066577000171040ustar00rootroot00000000000000PNG  IHDRĴl;bKGD pHYs  tIME79HwIDAT8˵[Hau77-܊Xx&\)HlE=ehJ *5P9άjudP'Ȍ {TJZ[ycdRB3&1z 4%4"޳~V%+T7O'fRf[hu[^34.%Kl¸%\SK+xgE{S!=p~/IENDB`Charm-1.12.0/Charm/Icons/locationbar_erase.png000066400000000000000000000006141331066577000211250ustar00rootroot00000000000000PNG  IHDRagAMA abKGDC oFFs1 pHYs  ~tIME #8IDATx͒1n0EQhbO 4t#X8JHig?B`W<7Ⱥl@;i+pn^y{(003RH)\.t]p;9m["uZ;nS೮keaG(v0 G,{$ !_qι,ߜCN!H +A̅bb1X? bz 0IENDB`Charm-1.12.0/Charm/Icons/media-playback-start.png000066400000000000000000000013401331066577000214440ustar00rootroot00000000000000PNG  IHDRnsBITUF pHYsaa0UtEXtSoftwarewww.inkscape.org<_IDATk\uw9+g6m!Պ8 $ !KT4?CqS)EqQCD0tbH,:"i-g~{*j a(6uk>psboC&Su\*q;.#5є,%?B0߮6}朆 R #d[8MzELDi*Mue\K=$T8q'ҷ?_IZ3M.dRt^URL ~u}Rl&wp2XL$s.ɤ‚j.Ɠ8vj@~DPSq,!uO*LӛMTWR!/ʖ*̴W8O*PCFt nW q6,նG꒥pnB)`T"JזWB[ՄqcFaZXhLHIENDB`Charm-1.12.0/Charm/Icons/media-playback-stop.png000066400000000000000000000013401331066577000212740ustar00rootroot00000000000000PNG  IHDRnsBITUF pHYsaa0UtEXtSoftwarewww.inkscape.org<_IDAT1h\e]wwkTT$!898DAMTS΢*"tJբP4CŃz^.pU.[ &o"+nO 2Gr\eeߎ!J҇qgOe(9C-0Oٱ\V ReC)g6ч+ŷ}[R$;3{:7> ⣧+QK H7^IW䶍 LRk)Sgl9,xNE414p?9RKfN 傪(Pe1)r=ĉ<7%RʅT"tر=kZ" (r?KH*U~R%z j.jk J¡D ĔBjJCF(w;hԽ}m $/ s7ڑKf.hjh21/Յ"V6kHA!7ϝ˶7k`2;5MU *('DډX^4'݁CC5V݇v- Rڽv+.[hwORB3pIENDB`Charm-1.12.0/Charm/Icons/tray/000077500000000000000000000000001331066577000157215ustar00rootroot00000000000000Charm-1.12.0/Charm/Icons/tray/charmtray22.png000066400000000000000000000023341331066577000205670ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<YIDAT8OH[YƿR͌_1og"yP#.KBĥFDBЍF| ]L7g>:Nd4hℐ23́|ǽ߽ K̦"pMB[_ccc`Р($OǡP|rr2ic  "6.@wuu=oJ ;1t$I>8^RL.,,HR:<P@)%-T L(=e2*yFx!Sp'6~0==}VxZ47VXͰR0n,[x!Q"]cc~ccaggP(xPRVVznnnor7_Ofė~GGG"[Ncؙd*P]޳5{Y(&v/ L&Sy,;pJN4'833T*'¯T*̬XVt`0x@)C_xհfvvGS[[$ix!-IҦblcaq#Ų%I& i8^8~?Ҫ6X/ɇ[[[z ޜmL|>"?zw6i@@9*GקlহmϞ=zޖh4g+7MMM*LFm,--mf^n4SAjUUU5p||9BgM5LSڇ0IENDB`Charm-1.12.0/Charm/Icons/tray/charmtray24.png000066400000000000000000000021551331066577000205720ustar00rootroot00000000000000PNG  IHDRw=sRGBbKGD pHYsnnޱtIME 5$IDATHǽ]hgo|͔Uܝ"]vl7jVhd2u,Mx3(-nt:RPNZMڬ1bOk7ہ=<9yɲUr|6 k8 `TD>44-龤qI%]tEyIHR>IHT%]ܱcGIx%ɓ"`%g+'NtI]e-FV>K @2[em=?~Cwf\Nbׯ_?455u?prƦ@mmmgÆ ]eZQmmWcjrr򭖖Ih"I p8uuum%+Wlt"sWqs&bӒ򐊊|K.,if%]|ƍsP(t8bY9d2͛7=F.jʀד֭9 p񌕕]D"WL.._rŀ;99#cϤl64dNR9N$}hKTJvuZr*p8\9[6ŮAwBRzMZ d^!۝h0$IENDB`Charm-1.12.0/Charm/Icons/tray/charmtray_mac.png000066400000000000000000000007241331066577000212440ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<QIDAT8ӱNTQoo@H,j d+Kmx#PPX-X[ +X H\.$g3g\c<:4rúmxh Q&(ȣNk-?H_纗ej=/4~W IENDB`Charm-1.12.0/Charm/Icons/tray/charmtrayactive22.png000066400000000000000000000022021331066577000217550ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8QH[W7l25mXSJq}R0\H%Oҗb/ X| ͋ ð &W6R6l3h]6 I+Sts;$ôeπ@=ôʵ:3 իW{{{?nx<677e2|"xF i=9̐WlW9577yR,..>N$,..B!:Ni37L $흝呑[@Nt: ܴc[;;;ccc%Ix}MӚ^$0e}k|Q;6599z5Mk -@rM gb1n꡶b+`AWhO]UԡJ,' ŕP0b =,48ơcjUy,׀-̼V14 !>@0usx| 4K.5MLL<_N]UB T*w 8T*͛/f}>_,J{nv+dI:_־\\$f{B. !>D"'g51%偂 2LQq<4U"urYq|mm@!LB:tUq P:痖^fgg+ z s5;;l9 -v6+pX^( ڕ 몲2==}"課 Ӫ{nX,NOOfW cCTTʏ7<>::zT*ǧf-877eRM&?$_}}}NS4L&^urr׮]k=q…V1>K$\.i==̨ DSB D̺H2IENDB`Charm-1.12.0/Charm/Icons/tray/charmtrayactive_mac.png000066400000000000000000000007351331066577000224420ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<ZIDAT8ӱjQo6)vH` b' Su,/,v"Y`h3:.+xsNrmSoaar\VmS /½kL|@Kp=< v8gLR.=~$8\~98=\No/;Ap(|mSa3^{'=NBw>D|:+&\QlF_amrm:epX\.7oI8s6}ՕN؞q "-]\?L S.{q xz"UIENDB`Charm-1.12.0/Charm/Idle/000077500000000000000000000000001331066577000145445ustar00rootroot00000000000000Charm-1.12.0/Charm/Idle/IdleDetector.cpp000066400000000000000000000102671331066577000176250ustar00rootroot00000000000000/* IdleDetector.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Mike McQuaid Author: Frank Osterfeld Author: Mike Arthur This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "IdleDetector.h" #include "CharmCMake.h" #include "MacIdleDetector.h" #include "WindowsIdleDetector.h" #include "X11IdleDetector.h" #include "ViewHelpers.h" #include "Core/Configuration.h" #include #include IdleDetector::IdleDetector(QObject *parent) : QObject(parent) , m_idlenessDuration(CharmIdleTime) // from CharmCMake.h { } IdleDetector *IdleDetector::createIdleDetector(QObject *parent) { #ifdef CHARM_IDLE_DETECTION #ifdef Q_OS_OSX return new MacIdleDetector(parent); #endif #ifdef Q_OS_WIN return new WindowsIdleDetector(parent); #endif #ifdef CHARM_IDLE_DETECTION_AVAILABLE X11IdleDetector *detector = new X11IdleDetector(parent); detector->setAvailable(detector->idleCheckPossible()); return detector; #endif #endif IdleDetector *unavailable = new IdleDetector(parent); unavailable->setAvailable(false); return unavailable; } bool IdleDetector::available() const { return m_available; } void IdleDetector::setAvailable(bool available) { if (m_available == available) return; m_available = available; emit availableChanged(m_available); } IdleDetector::IdlePeriods IdleDetector::idlePeriods() const { return m_idlePeriods; } void IdleDetector::setIdlenessDuration(int seconds) { if (m_idlenessDuration == seconds) return; m_idlenessDuration = seconds; emit idlenessDurationChanged(m_idlenessDuration); onIdlenessDurationChanged(); } int IdleDetector::idlenessDuration() const { return m_idlenessDuration; } void IdleDetector::maybeIdle(IdlePeriod period) { if (!Configuration::instance().detectIdling || DATAMODEL->activeEventCount() == 0) return; qDebug() << "IdleDetector::maybeIdle: Checking for idleness"; // merge overlapping idle periods IdlePeriods periods(idlePeriods()); periods << period; // // TEMP (this was used to test the overlapping-idle-period compression below, leave it in // { // IdlePeriod i2( period.first.addSecs( 1 ), period.second.addSecs( 1 ) ); // should be merged // IdlePeriod i3( period.second.addSecs( 2 ), period.second.addSecs( 5 ) ); // should not be merged // periods << i2 << i3; // } qSort(periods); m_idlePeriods.clear(); while (!periods.isEmpty()) { IdlePeriod first = periods.first(); periods.pop_front(); while (!periods.isEmpty()) { IdlePeriod second = periods.first(); if (second.first >= first.first && second.first <= first.second) { first.second = qMax(first.second, second.second); // first.first is already the earlier time, because the container is sorted } else { break; } periods.pop_front(); } if (first.first.secsTo(first.second) >= idlenessDuration()) { // we ignore idle period of less than MinimumSeconds m_idlePeriods << first; } } // notify application if (!idlePeriods().isEmpty()) { qDebug() << "IdleDetector::maybeIdle: Found idleness"; QTimer::singleShot(0, this, [=](){ emit maybeIdle(); }); } } void IdleDetector::clear() { m_idlePeriods.clear(); } Charm-1.12.0/Charm/Idle/IdleDetector.h000066400000000000000000000052671331066577000172760ustar00rootroot00000000000000/* IdleDetector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef IDLEDETECTOR_H #define IDLEDETECTOR_H #include #include #include #include /** IdleDetector implements idle detection (duh). * Idle detection is (as of now) platform dependant. The factory * function createIdleDetector returns an implementation that * implements idle detection for the current platform. If idle * detection is not supported, a dummy object is returned, * with @c available being false. */ class IdleDetector : public QObject { Q_OBJECT Q_PROPERTY(bool available READ available NOTIFY availableChanged) Q_PROPERTY( int idlenessDuration READ idlenessDuration WRITE setIdlenessDuration NOTIFY idlenessDurationChanged) public: typedef QPair IdlePeriod; typedef QVector IdlePeriods; /** Create an idle detector for this platform. */ static IdleDetector *createIdleDetector(QObject *parent); /** Returns the idle periods. */ IdlePeriods idlePeriods() const; /** Clear the recorded idle periods. */ void clear(); /** * the number of seconds after which the detector should notify idleness * @return the duration in seconds */ int idlenessDuration() const; void setIdlenessDuration(int seconds); /** * Returns whether idle detection is available */ bool available() const; Q_SIGNALS: void maybeIdle(); void idlenessDurationChanged(int idlenessDuration); void availableChanged(bool available); protected: virtual void onIdlenessDurationChanged() { } explicit IdleDetector(QObject *parent = nullptr); void maybeIdle(IdlePeriod period); void setAvailable(bool available); private: IdlePeriods m_idlePeriods; int m_idlenessDuration = 0; bool m_available = true; }; #endif Charm-1.12.0/Charm/Idle/MacIdleDetector.h000066400000000000000000000025031331066577000177050ustar00rootroot00000000000000/* MacIdleDetector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mike McQuaid This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MACIDLEDETECTOR_H #define MACIDLEDETECTOR_H #include #include #include "IdleDetector.h" class MacIdleDetector : public IdleDetector { Q_OBJECT public: explicit MacIdleDetector(QObject *parent = nullptr); // This method to be public due to lack of friend classes in Objective-C and // the lack inheritance of Objective-C classes from C++ ones. void idle(); private: class Private; Private *m_private; }; #endif Charm-1.12.0/Charm/Idle/MacIdleDetector.mm000066400000000000000000000060371331066577000200750ustar00rootroot00000000000000/* MacIdleDetector.mm This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mike McQuaid This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include "MacIdleDetector.h" #include @interface MacIdleObserver : NSObject { @public MacIdleDetector* idleDetector; QDateTime idleStartTime; } - (id)init; - (void)dealloc; @end @implementation MacIdleObserver - (id)init { if ((self = [super init])) { NSNotificationCenter* notificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter]; [notificationCenter addObserver: self selector: @selector(receiveSleepNotification:) name: NSWorkspaceWillSleepNotification object: NULL ]; [notificationCenter addObserver: self selector: @selector(receiveWakeNotification:) name: NSWorkspaceDidWakeNotification object: NULL ]; [notificationCenter addObserver: self selector: @selector(receiveSleepNotification:) name: NSWorkspaceScreensDidSleepNotification object: NULL ]; [notificationCenter addObserver: self selector: @selector(receiveWakeNotification:) name: NSWorkspaceScreensDidWakeNotification object: NULL ]; } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } - (void)receiveSleepNotification:(NSNotification*)notification { idleStartTime = QDateTime::currentDateTime(); } - (void)receiveWakeNotification:(NSNotification*)notification { if (idleDetector) idleDetector->idle(); } @end class MacIdleDetector::Private { public: Private(); ~Private(); NSAutoreleasePool* pool; MacIdleObserver* observer; }; MacIdleDetector::Private::Private() : pool( 0 ), observer( 0 ) { pool = [[NSAutoreleasePool alloc] init]; observer = [[MacIdleObserver alloc] init]; } MacIdleDetector::Private::~Private() { [pool drain]; } MacIdleDetector::MacIdleDetector( QObject* parent ) : IdleDetector( parent ) , m_private( new MacIdleDetector::Private() ) { m_private->observer->idleDetector = this; } void MacIdleDetector::idle() { maybeIdle( IdlePeriod( m_private->observer->idleStartTime, QDateTime::currentDateTime() ) ); } #include "MacIdleDetector.moc" Charm-1.12.0/Charm/Idle/WindowsIdleDetector.cpp000066400000000000000000000037531331066577000212020ustar00rootroot00000000000000/* WindowsIdleDetector.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld Author: Mike McQuaid This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "WindowsIdleDetector.h" #include "windows.h" #include WindowsIdleDetector::WindowsIdleDetector(QObject *parent) : IdleDetector(parent) { connect(&m_timer, &QTimer::timeout, this, &WindowsIdleDetector::timeout); m_timer.setInterval(idlenessDuration() * 1000 / 2); m_timer.setSingleShot(false); m_timer.start(); } void WindowsIdleDetector::onIdlenessDurationChanged() { m_timer.stop(); m_timer.setInterval(idlenessDuration() * 1000 / 2); m_timer.start(); } void WindowsIdleDetector::timeout() { LASTINPUTINFO lif; lif.cbSize = sizeof(lif); const bool ret = GetLastInputInfo(&lif); if (!ret) { qWarning() << "Idle detection: GetLastInputInfo failed."; return; } const qint64 dwTime = static_cast(lif.dwTime); const qint64 ctk = static_cast(GetTickCount()); const int idleSecs = (ctk - dwTime) / 1000; if (idleSecs >= idlenessDuration()) maybeIdle(IdlePeriod(QDateTime::currentDateTime().addSecs(-idleSecs), QDateTime::currentDateTime())); } Charm-1.12.0/Charm/Idle/WindowsIdleDetector.h000066400000000000000000000023731331066577000206440ustar00rootroot00000000000000/* WindowsIdleDetector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef WINDOWSIDLEDETECTOR_H #define WINDOWSIDLEDETECTOR_H #include "IdleDetector.h" #include class WindowsIdleDetector : public IdleDetector { Q_OBJECT public: explicit WindowsIdleDetector(QObject *parent); protected: void onIdlenessDurationChanged() override; private Q_SLOTS: void timeout(); private: QTimer m_timer; }; #endif // WINDOWSIDLEDETECTOR_H Charm-1.12.0/Charm/Idle/X11IdleDetector.cpp000066400000000000000000000047511331066577000201200ustar00rootroot00000000000000/* X11IdleDetector.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Jesper Pedersen Author: Frank Osterfeld Author: Mirko Boehm Author: Mike McQuaid This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "X11IdleDetector.h" #include "CharmCMake.h" #include X11IdleDetector::X11IdleDetector(QObject *parent) : IdleDetector(parent) { connect(&m_timer, &QTimer::timeout, this, &X11IdleDetector::checkIdleness); m_timer.start(idlenessDuration() * 1000 / 5); m_heartbeat = QDateTime::currentDateTime(); } bool X11IdleDetector::idleCheckPossible() { m_connection = xcb_connect(NULL, NULL); //krazy:exclude=null m_screen = xcb_setup_roots_iterator(xcb_get_setup(m_connection)).data; if (m_screen) return true; return false; } void X11IdleDetector::onIdlenessDurationChanged() { m_timer.stop(); m_timer.start(idlenessDuration() * 1000 / 5); } void X11IdleDetector::checkIdleness() { xcb_screensaver_query_info_cookie_t cookie; cookie = xcb_screensaver_query_info(m_connection, m_screen->root); xcb_screensaver_query_info_reply_t *info; info = xcb_screensaver_query_info_reply(m_connection, cookie, NULL); //krazy:exclude=null const int idleSecs = info->ms_since_user_input / 1000; free(info); if (idleSecs >= idlenessDuration()) maybeIdle(IdlePeriod(QDateTime::currentDateTime().addSecs(-idleSecs), QDateTime::currentDateTime())); if (m_heartbeat.secsTo(QDateTime::currentDateTime()) > idlenessDuration()) maybeIdle(IdlePeriod(m_heartbeat, QDateTime::currentDateTime())); m_heartbeat = QDateTime::currentDateTime(); } #include "moc_X11IdleDetector.cpp" Charm-1.12.0/Charm/Idle/X11IdleDetector.h000066400000000000000000000030301331066577000175520ustar00rootroot00000000000000/* X11IdleDetector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Jesper Pedersen Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef X11IDLEDETECTOR_H #define X11IDLEDETECTOR_H #include "IdleDetector.h" #include #if defined(Q_OS_UNIX) && !defined(Q_OS_OSX) #include #endif class X11IdleDetector : public IdleDetector { Q_OBJECT public: explicit X11IdleDetector(QObject *parent); bool idleCheckPossible(); protected: void onIdlenessDurationChanged() override; private Q_SLOTS: void checkIdleness(); private: QDateTime m_heartbeat; QTimer m_timer; #if defined(Q_OS_UNIX) && !defined(Q_OS_OSX) xcb_connection_t *m_connection; xcb_screen_t *m_screen; #endif }; #endif /* X11IDLEDETECTOR_H */ Charm-1.12.0/Charm/Lotsofcake/000077500000000000000000000000001331066577000157615ustar00rootroot00000000000000Charm-1.12.0/Charm/Lotsofcake/Configuration.cpp000066400000000000000000000077051331066577000213050ustar00rootroot00000000000000/* Configuration.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2017-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Lotsofcake/Configuration.h" #include "Core/XmlSerialization.h" #include #include static const char *s_group = "httpconfig"; static const char *s_keyUsername = "username"; static const char *s_keyTimesheetUploadUrl = "timesheetUploadUrl"; static const char *s_keyProjectCodeDownloadUrl = "projectCodeDownloadUrl"; static const char *s_keyRestUrl = "restUrl"; static const char *s_keyLastStagedTimesheetUpload = "lastStagedTimesheetUpload"; static void setValueIfNotNull(QSettings *s, const QString &key, const QString &value) { if (!value.isNull()) { s->setValue(key, value); } else { s->remove(key); } } bool Lotsofcake::Configuration::isConfigured() const { return !username().isEmpty(); } void Lotsofcake::Configuration::importFromTaskExport(const TaskExport &exporter) { QSettings settings; settings.beginGroup(QLatin1String(s_group)); setValueIfNotNull(&settings, QLatin1String(s_keyUsername), exporter.metadata(QStringLiteral("username"))); setValueIfNotNull(&settings, QLatin1String(s_keyTimesheetUploadUrl), exporter.metadata(QStringLiteral("timesheet-upload-url"))); setValueIfNotNull(&settings, QLatin1String(s_keyProjectCodeDownloadUrl), exporter.metadata(QStringLiteral("project-code-download-url"))); setValueIfNotNull(&settings, QLatin1String(s_keyRestUrl), exporter.metadata(QStringLiteral("rest-url"))); } QString Lotsofcake::Configuration::username() const { QSettings settings; settings.beginGroup(QLatin1String(s_group)); return settings.value(QLatin1String(s_keyUsername)).toString(); } QUrl Lotsofcake::Configuration::timesheetUploadUrl() const { QSettings settings; settings.beginGroup(QLatin1String(s_group)); QUrl url = settings.value(QLatin1String(s_keyTimesheetUploadUrl)).toUrl(); url.setPath(QLatin1String("/KdabHome/apps/timesheets/rest/upload")); // TODO don't use hardcoded path return url; } QUrl Lotsofcake::Configuration::projectCodeDownloadUrl() const { QSettings settings; settings.beginGroup(QLatin1String(s_group)); return settings.value(QLatin1String(s_keyProjectCodeDownloadUrl)).toUrl(); } QUrl Lotsofcake::Configuration::restUrl() const { QSettings settings; settings.beginGroup(QLatin1String(s_group)); return settings.value(QLatin1String(s_keyRestUrl)).toUrl(); } QDate Lotsofcake::Configuration::lastStagedTimesheetUpload() const { if (!m_lastStagedTimesheetUpload.set) { QSettings settings; settings.beginGroup(QLatin1String(s_group)); m_lastStagedTimesheetUpload.date = settings.value(QLatin1String(s_keyLastStagedTimesheetUpload)).toDate(); m_lastStagedTimesheetUpload.set = true; } return m_lastStagedTimesheetUpload.date; } void Lotsofcake::Configuration::setLastStagedTimesheetUpload(const QDate &date) { QSettings settings; settings.beginGroup(QLatin1String(s_group)); settings.setValue(QLatin1String(s_keyLastStagedTimesheetUpload), date); m_lastStagedTimesheetUpload.date = date; m_lastStagedTimesheetUpload.set = true; } Charm-1.12.0/Charm/Lotsofcake/Configuration.h000066400000000000000000000027631331066577000207510ustar00rootroot00000000000000/* Configuration.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2017-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef LOTSOFCAKE_CONFIGURATION_H #define LOTSOFCAKE_CONFIGURATION_H #include class QString; class QUrl; class TaskExport; namespace Lotsofcake { class Configuration { public: bool isConfigured() const; void importFromTaskExport(const TaskExport &exporter); QDate lastStagedTimesheetUpload() const; void setLastStagedTimesheetUpload(const QDate &date); QString username() const; QUrl timesheetUploadUrl() const; QUrl projectCodeDownloadUrl() const; QUrl restUrl() const; private: mutable struct { bool set = false; QDate date; } m_lastStagedTimesheetUpload; }; } #endif Charm-1.12.0/Charm/MacApplicationCore.h000066400000000000000000000032731331066577000175420ustar00rootroot00000000000000/* MacApplicationCore.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MACAPPLICATIONCORE_H #define MACAPPLICATIONCORE_H #include "ApplicationCore.h" class MacApplicationCore : public ApplicationCore { Q_OBJECT public: explicit MacApplicationCore(TaskId startupTask, bool hideAtStart, QObject *parent = nullptr); ~MacApplicationCore(); // This method to be public due to lack of friend classes in Objective-C and // the lack inheritance of Objective-C classes from C++ ones. void dockIconClickEvent(); private Q_SLOTS: void handleStateChange(State state) const; private: static QList< QShortcut * > shortcuts(QWidget *parent); static QList< QShortcut * > activeShortcuts(const QKeySequence &seq, bool autorep, QWidget *parent = nullptr); QMenu m_dockMenu; class Private; Private *m_private; }; #endif Charm-1.12.0/Charm/MacApplicationCore.mm000066400000000000000000000144701331066577000177250ustar00rootroot00000000000000/* MacApplicationCore.mm This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include "MacApplicationCore.h" #include #include #include extern void qt_mac_set_dock_menu(QMenu*); @interface DockIconClickEventHandler : NSObject { @public MacApplicationCore* macApplication; } - (void)handleDockClickEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent; @end @implementation DockIconClickEventHandler - (void)handleDockClickEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent { if (macApplication) macApplication->dockIconClickEvent(); } @end class MacApplicationCore::Private { public: Private(); ~Private(); NSEvent* cocoaEventFilter( NSEvent* incomingEvent ); void setupCocoaEventHandler() const; NSAutoreleasePool* pool; NSEvent* eventMonitor; DockIconClickEventHandler* dockIconClickEventHandler; }; MacApplicationCore::Private::Private() : pool( 0 ), eventMonitor( 0 ), dockIconClickEventHandler( 0 ) { pool = [[NSAutoreleasePool alloc] init]; dockIconClickEventHandler = [[DockIconClickEventHandler alloc] init]; eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:^(NSEvent *incomingEvent) { return cocoaEventFilter(incomingEvent); }]; } MacApplicationCore::Private::~Private() { [NSEvent removeMonitor:eventMonitor]; [pool drain]; } NSEvent* MacApplicationCore::Private::cocoaEventFilter( NSEvent* incomingEvent ) { NSUInteger modifierFlags = [incomingEvent modifierFlags]; int shortcutFlags = [[incomingEvent charactersIgnoringModifiers] UTF8String][0]; if (modifierFlags & NSShiftKeyMask) shortcutFlags |= Qt::ShiftModifier; if (modifierFlags & NSControlKeyMask) shortcutFlags |= Qt::MetaModifier; if (modifierFlags & NSCommandKeyMask) shortcutFlags |= Qt::ControlModifier; if (modifierFlags & NSAlternateKeyMask) shortcutFlags |= Qt::AltModifier; const QKeySequence keySequence( shortcutFlags ); const bool autoRepeat = [incomingEvent isARepeat]; const QList< QShortcut* > active = activeShortcuts( keySequence, autoRepeat ); Q_FOREACH( QShortcut* const shortcut, active ) { QShortcutEvent event( keySequence, shortcut->id() ); QObject* const receiver = shortcut; receiver->event( &event ); } if (!active.isEmpty()) return nil; return incomingEvent; } void MacApplicationCore::Private::setupCocoaEventHandler() const { // TODO: This apparently uses a legacy API and we should be using the // applicationShouldHandleReopen:hasVisibleWindows: method on // NSApplicationDelegate but this isn't possible without nasty runtime // reflection hacks until Qt is fixed. If this breaks, shout at them :) [[NSAppleEventManager sharedAppleEventManager] setEventHandler:dockIconClickEventHandler andSelector:@selector(handleDockClickEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEReopenApplication]; } MacApplicationCore::MacApplicationCore( TaskId startupTask, bool hideAtStart, QObject* parent ) : ApplicationCore( startupTask, hideAtStart, parent ) , m_private( new MacApplicationCore::Private() ) { m_private->dockIconClickEventHandler->macApplication = this; connect(this, SIGNAL(goToState(State)), this, SLOT(handleStateChange(State))); m_dockMenu.addAction( &m_actionStopAllTasks ); m_dockMenu.addSeparator(); m_dockMenu.addMenu( m_timeTracker.menu() ); qt_mac_set_dock_menu( &m_dockMenu ); // OSX doesn't use icons in menus QApplication::setWindowIcon( QIcon() ); m_timeTracker.setWindowIcon( QIcon() ); m_actionQuit.setIcon( QIcon() ); QCoreApplication::setAttribute( Qt::AA_DontShowIconsInMenus ); } MacApplicationCore::~MacApplicationCore() { delete m_private; } void MacApplicationCore::handleStateChange(State state) const { if (state == Configuring) m_private->setupCocoaEventHandler(); } void MacApplicationCore::dockIconClickEvent() { showMainWindow(); } QList< QShortcut* > MacApplicationCore::shortcuts( QWidget* parent ) { QList< QShortcut* > result; if( parent == 0 ) { const QWidgetList widgets = QApplication::topLevelWidgets(); for( QWidgetList::const_iterator it = widgets.begin(); it != widgets.end(); ++it ) result += shortcuts( *it ); } else { const QList< QShortcut* > cuts = parent->findChildren< QShortcut* >(); for( QList< QShortcut* >::const_iterator it = cuts.begin(); it != cuts.end(); ++it ) if( (*it)->context() == Qt::ApplicationShortcut ) result.push_back( *it ); const QList< QWidget* > children = parent->findChildren< QWidget* >(); for( QList< QWidget* >::const_iterator it = children.begin(); it != children.end(); ++it ) result += shortcuts( *it ); } return result; } QList< QShortcut* > MacApplicationCore::activeShortcuts( const QKeySequence& seq, bool autorep, QWidget* parent ) { const QList< QShortcut* > cuts = shortcuts( parent ); QList< QShortcut* > result; for( QList< QShortcut* >::const_iterator it = cuts.begin(); it != cuts.end(); ++it ) if( (*it)->context() == Qt::ApplicationShortcut && ((*it)->autoRepeat() == autorep || !autorep ) && (*it)->isEnabled() && (*it)->key().matches( seq ) ) result.push_back( *it ); return result; } #include "MacApplicationCore.moc" Charm-1.12.0/Charm/MacOSXBundleInfo.plist.in000066400000000000000000000025021331066577000204100ustar00rootroot00000000000000 CFBundleDevelopmentRegion English CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} CFBundleGetInfoString ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString ${MACOSX_BUNDLE_LONG_VERSION_STRING} CFBundleName ${MACOSX_BUNDLE_BUNDLE_NAME} CFBundlePackageType APPL CFBundleShortVersionString ${MACOSX_BUNDLE_SHORT_VERSION_STRING} CFBundleSignature ???? CFBundleVersion ${MACOSX_BUNDLE_BUNDLE_VERSION} CSResourcesFileMapped LSRequiresCarbon NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} NSHighResolutionCapable <${MACOSX_BUNDLE_HIGHRESOLUTION_CAPABLE}/> Charm-1.12.0/Charm/ModelConnector.cpp000066400000000000000000000057321331066577000173150ustar00rootroot00000000000000/* ModelConnector.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: David Faure Author: Mike McQuaid Author: Allen Winter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ModelConnector.h" #include "ViewHelpers.h" #include "Data.h" #include "Commands/CommandModifyEvent.h" #include "Commands/CommandMakeAndActivateEvent.h" ModelConnector::ModelConnector() : QObject() , m_dataModel() , m_viewFilter(&m_dataModel) , m_eventModelFilter(&m_dataModel) , m_findEventModelFilter(&m_dataModel) { connect(&m_dataModel, &CharmDataModel::makeAndActivateEvent, this, &ModelConnector::slotMakeAndActivateEvent); connect(&m_dataModel, &CharmDataModel::requestEventModification, this, &ModelConnector::slotRequestEventModification); connect(&m_dataModel, &CharmDataModel::sysTrayUpdate, this, &ModelConnector::slotSysTrayUpdate); } CharmDataModel *ModelConnector::charmDataModel() { return &m_dataModel; } ViewFilter *ModelConnector::taskModel() { return &m_viewFilter; } EventModelFilter *ModelConnector::eventModel() { return &m_eventModelFilter; } EventModelFilter *ModelConnector::findEventModel() { return &m_findEventModelFilter; } void ModelConnector::commitCommand(CharmCommand *command) { if (!command->finalize()) { qWarning() << "CharmDataModel::commitCommand:" << command->metaObject()->className() << "command has failed"; } } void ModelConnector::slotMakeAndActivateEvent(const Task &task) { // the command will call activateEvent in finalize, this will // notify the task view to update auto command = new CommandMakeAndActivateEvent(task, this); VIEW.sendCommand(command); } void ModelConnector::slotRequestEventModification(const Event &newEvent, const Event &oldEvent) { auto command = new CommandModifyEvent(newEvent, oldEvent, this); VIEW.sendCommand(command); } void ModelConnector::slotSysTrayUpdate(const QString &toolTip, bool active) { TRAY.setToolTip(toolTip); TRAY.setIcon(active ? Data::charmTrayActiveIcon() : Data::charmTrayIcon()); } Charm-1.12.0/Charm/ModelConnector.h000066400000000000000000000037711331066577000167630ustar00rootroot00000000000000/* ModelConnector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: David Faure Author: Allen Winter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MODELCONNECTOR_H #define MODELCONNECTOR_H #include "ViewFilter.h" #include "Core/CharmDataModel.h" #include "EventModelFilter.h" class ModelConnector : public QObject, public CommandEmitterInterface { Q_OBJECT public: ModelConnector(); /** The charm data model. */ CharmDataModel *charmDataModel(); /** The item model the task view uses. */ ViewFilter *taskModel(); /** The item model the event view uses. */ EventModelFilter *eventModel(); EventModelFilter *findEventModel(); // implement CommandEmitterInterface void commitCommand(CharmCommand *) override; public Q_SLOTS: void slotMakeAndActivateEvent(const Task &); void slotRequestEventModification(const Event &newEvent, const Event &oldEvent); void slotSysTrayUpdate(const QString &toolTip, bool active); private: CharmDataModel m_dataModel; ViewFilter m_viewFilter; // this is the filtered task model adapter EventModelFilter m_eventModelFilter; // owns the event model adapter EventModelFilter m_findEventModelFilter; }; #endif Charm-1.12.0/Charm/QtQuick/000077500000000000000000000000001331066577000152505ustar00rootroot00000000000000Charm-1.12.0/Charm/QtQuick/Charm.cpp000066400000000000000000000021411331066577000170040ustar00rootroot00000000000000/* Charm.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include int main(int argc, char *argv[]) { QApplication app(argc, argv); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:///qml/main.qml"))); return app.exec(); } Charm-1.12.0/Charm/QtQuick/deployment.pri000066400000000000000000000007431331066577000201500ustar00rootroot00000000000000android-no-sdk { target.path = /data/user/qt export(target.path) INSTALLS += target } else:android { x86 { target.path = /libs/x86 } else: armeabi-v7a { target.path = /libs/armeabi-v7a } else { target.path = /libs/armeabi } export(target.path) INSTALLS += target } else:unix { isEmpty(target.path) { target.path = /opt/$${TARGET}/bin export(target.path) } INSTALLS += target } export(INSTALLS) Charm-1.12.0/Charm/QtQuick/qml.qrc000066400000000000000000000001331331066577000165450ustar00rootroot00000000000000 qml/main.qml Charm-1.12.0/Charm/QtQuick/qml/000077500000000000000000000000001331066577000160415ustar00rootroot00000000000000Charm-1.12.0/Charm/QtQuick/qml/main.qml000066400000000000000000000115421331066577000175030ustar00rootroot00000000000000/* main.qml This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import QtQuick 2.2 import QtQuick.Controls 1.1 import QtQuick.Layouts 1.1 ApplicationWindow { visible: true width: 640 height: 480 title: qsTr("Charm") menuBar: MenuBar { Menu { title: qsTr("File") MenuItem { id: importDatabse text: qsTr("Import database from previous export ...") } MenuItem { id: exportDatabse text: qsTr("Export database ...") } MenuItem { id: downloadTaskDefinitions text: qsTr("Download Task Definitions ...") } // do we still need the following two menu entries on a mobile device ? // MenuItem { // id: importTaskDefinitions // text: qsTr("Import and Merge Task Definitions ...") // } // MenuItem { // id: exportTaskDefinitions // text: qsTr("Export and Merge Task Definitions ...") // } MenuItem { text: qsTr("Exit") onTriggered: Qt.quit(); } } Menu { title: qsTr("Edit") MenuItem { id: stopTask text: qsTr("Stop task") } MenuItem { id: editComment text: qsTr("Edit comment") } MenuItem { id: startTask text: qsTr("Start other task") } } } ColumnLayout { anchors.fill: parent spacing: 2 TableView { id: tableView Layout.fillHeight: true Layout.fillWidth: true TableViewColumn{ role: "task" ; title: qsTr("Task"); width: tableView.width - 8 * tableColumn.width; movable: false } TableViewColumn{ role: "monday" ; title: qsTr("Mon"); movable: false; horizontalAlignment: Text.AlignRight; id: tableColumn } TableViewColumn{ role: "tuesday" ; title: qsTr("Tue"); movable: false; horizontalAlignment: Text.AlignRight } TableViewColumn{ role: "wednesday" ; title: qsTr("Wed"); movable: false; horizontalAlignment: Text.AlignRight } TableViewColumn{ role: "thursday" ; title: qsTr("Thu"); movable: false; horizontalAlignment: Text.AlignRight } TableViewColumn{ role: "friday" ; title: qsTr("Fri"); movable: false; horizontalAlignment: Text.AlignRight } TableViewColumn{ role: "saturday" ; title: qsTr("Sat"); movable: false; horizontalAlignment: Text.AlignRight } TableViewColumn{ role: "sunday" ; title: qsTr("Sun"); movable: false; horizontalAlignment: Text.AlignRight } TableViewColumn{ role: "total" ; title: qsTr("Total"); movable: false; horizontalAlignment: Text.AlignRight } model: ListModel { ListElement{ task: "8714 IT Infrastructure/Internal karm/charm development" wednesday: "5.30" } ListElement{ task: "8914 Qt Contributions/Android" monday: "1.50" tuesday: "4.00" wednesday: "2.00" } } } ToolBar { id: toolBar anchors.bottom: parent.bottom Layout.fillWidth: true RowLayout { anchors.fill: parent Button { id: stopTaskButton text: qsTr("Stop task") onClicked: stopTask.trigger() } Button { id: editCommentButton text: qsTr("Edit comment") onClicked: editComment.trigger() } ComboBox { Layout.fillWidth: true model: [ "8714 IT Infrastructure/Internal karm/charm development", "8914 Qt Contributions/Android" ] } } } } } Charm-1.12.0/Charm/Reports/000077500000000000000000000000001331066577000153255ustar00rootroot00000000000000Charm-1.12.0/Charm/Reports/MonthlyTimesheetXmlWriter.cpp000066400000000000000000000045631331066577000232210ustar00rootroot00000000000000/* MonthlyTimesheetXmlWriter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "MonthlyTimesheetXmlWriter.h" #include "TimesheetInfo.h" #include MonthlyTimesheetXmlWriter::MonthlyTimesheetXmlWriter() : TimesheetXmlWriter(QLatin1String("monthly-timesheet")) { } void MonthlyTimesheetXmlWriter::setYearOfMonth(int yearOfMonth) { m_yearOfMonth = yearOfMonth; } void MonthlyTimesheetXmlWriter::setMonthNumber(int monthNumber) { m_monthNumber = monthNumber; } void MonthlyTimesheetXmlWriter::setNumberOfWeeks(int numberOfWeeks) { m_numberOfWeeks = numberOfWeeks; } void MonthlyTimesheetXmlWriter::writeMetadata(QDomDocument &document, QDomElement &metadata) const { QDomElement yearElement = document.createElement(QStringLiteral("year")); metadata.appendChild(yearElement); QDomText text = document.createTextNode(QString::number(m_yearOfMonth)); yearElement.appendChild(text); QDomElement monthElement = document.createElement(QStringLiteral("serial-number")); monthElement.setAttribute(QStringLiteral("semantics"), QStringLiteral("month-number")); metadata.appendChild(monthElement); QDomText monthtext = document.createTextNode(QString::number(m_monthNumber)); monthElement.appendChild(monthtext); } QList MonthlyTimesheetXmlWriter::createTimeSheetInfo() const { return TimeSheetInfo::filteredTaskWithSubTasks( TimeSheetInfo::taskWithSubTasks(dataModel(), m_numberOfWeeks, rootTask(), SecondsMap()), false); // here, we don't care about active or not, because we only report on the tasks } Charm-1.12.0/Charm/Reports/MonthlyTimesheetXmlWriter.h000066400000000000000000000027261331066577000226650ustar00rootroot00000000000000/* MonthlyTimesheetXmlWriter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MONTHLYTIMESHEETXMLWRITER_H #define MONTHLYTIMESHEETXMLWRITER_H #include "TimesheetXmlWriter.h" class MonthlyTimesheetXmlWriter : public TimesheetXmlWriter { public: MonthlyTimesheetXmlWriter(); void setYearOfMonth(int yearOfMonth); void setMonthNumber(int monthNumber); void setNumberOfWeeks(int numberOfWeeks); protected: void writeMetadata(QDomDocument &document, QDomElement &metadata) const override; QList createTimeSheetInfo() const override; private: int m_yearOfMonth = 0; int m_monthNumber = 0; int m_numberOfWeeks = 0; }; #endif Charm-1.12.0/Charm/Reports/TimesheetInfo.cpp000066400000000000000000000071731331066577000206040ustar00rootroot00000000000000/* TimesheetInfo.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TimesheetInfo.h" #include "Core/CharmDataModel.h" TimeSheetInfo::TimeSheetInfo(int segments) : seconds(segments) { seconds.fill(0); } int TimeSheetInfo::total() const { int value = 0; for (int i = 0; i < seconds.size(); ++i) value += seconds[i]; return value; } void TimeSheetInfo::dump() { qDebug() << "TimeSheetInfo: (" << indentation << ")" << formattedTaskIdAndName(6) << ":" << seconds << "-" << total() << "total"; } QString TimeSheetInfo::formattedTaskIdAndName(int taskPaddingLength) const { const QString formattedId = QStringLiteral("%1").arg(taskId, taskPaddingLength, 10, QLatin1Char('0')); return QStringLiteral("%1: %2").arg(formattedId, taskName); } // make the list, aggregate the seconds in the subtask: TimeSheetInfoList TimeSheetInfo::taskWithSubTasks(const CharmDataModel *dataModel, int segments, TaskId id, const SecondsMap &secondsMap, TimeSheetInfo *addTo) { TimeSheetInfoList result; TimeSheetInfoList children; TimeSheetInfo myInformation(segments); const TaskTreeItem &item = dataModel->taskTreeItem(id); // real task or virtual root item Q_ASSERT(item.task().isValid() || id == 0); if (id != 0) { // add totals for task itself: if (secondsMap.contains(id)) myInformation.seconds = secondsMap.value(id); // add name and id: myInformation.taskId = item.task().id(); myInformation.taskName = item.task().name(); if (addTo != 0) myInformation.indentation = addTo->indentation + 1; myInformation.taskId = id; } else { myInformation.indentation = -1; } TaskIdList childIds = item.childIds(); // sort by task id qSort(childIds); // recursively add those to myself: Q_FOREACH (const TaskId i, childIds) children << taskWithSubTasks(dataModel, segments, i, secondsMap, &myInformation); // add to parent: if (addTo != 0) { for (int i = 0; i < segments; ++i) addTo->seconds[i] += myInformation.seconds[i]; addTo->aggregated = true; } result << myInformation << children; return result; } // retrieve events that match the settings (active, ...): TimeSheetInfoList TimeSheetInfo::filteredTaskWithSubTasks( TimeSheetInfoList timeSheetInfo, bool activeTasksOnly) { if (activeTasksOnly) { TimeSheetInfoList nonZero; // FIXME use algorithm (I just hate to lug the fat book around) for (int i = 0; i < timeSheetInfo.size(); ++i) { if (timeSheetInfo[i].total() > 0) nonZero << timeSheetInfo[i]; } timeSheetInfo = nonZero; } return timeSheetInfo; } Charm-1.12.0/Charm/Reports/TimesheetInfo.h000066400000000000000000000037011331066577000202420ustar00rootroot00000000000000/* TimesheetInfo.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TIMESHEETINFO_H #define TIMESHEETINFO_H #include #include #include #include "Core/Task.h" class CharmDataModel; class TimeSheetInfo; typedef QList TimeSheetInfoList; typedef QMap< TaskId, QVector > SecondsMap; class TimeSheetInfo { public: explicit TimeSheetInfo(int segments); int total() const; void dump(); public: static TimeSheetInfoList taskWithSubTasks(const CharmDataModel *dataModel, int segments, TaskId id, const SecondsMap &secondsMap, TimeSheetInfo *addTo = 0); static TimeSheetInfoList filteredTaskWithSubTasks(TimeSheetInfoList timeSheetInfo, bool activeTasksOnly); public: QString formattedTaskIdAndName(int taskPaddingLength) const; // the level of indentation, >0 means the numbers are aggregated for the subtasks: int indentation = 0; QString taskName; QVector seconds; TaskId taskId = {}; bool aggregated = false; }; #endif Charm-1.12.0/Charm/Reports/TimesheetXmlWriter.cpp000066400000000000000000000137461331066577000216510ustar00rootroot00000000000000/* TimesheetXmlWriter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TimesheetXmlWriter.h" #include "TimesheetInfo.h" #include "CharmCMake.h" #include "Core/CharmDataModel.h" #include "Core/CharmConstants.h" #include "Core/XmlSerialization.h" #include TimesheetXmlWriter::TimesheetXmlWriter(const QString &templateName) : m_templateName(templateName) { } TimesheetXmlWriter::~TimesheetXmlWriter() { } const CharmDataModel *TimesheetXmlWriter::dataModel() const { return m_dataModel; } void TimesheetXmlWriter::setDataModel(const CharmDataModel *model) { m_dataModel = model; } EventList TimesheetXmlWriter::events() const { return m_events; } void TimesheetXmlWriter::setEvents(const EventList &events) { m_events = events; } TaskId TimesheetXmlWriter::rootTask() const { return m_rootTask; } void TimesheetXmlWriter::setRootTask(TaskId rootTask) { m_rootTask = rootTask; } bool TimesheetXmlWriter::includeTaskList() const { return m_includeTaskList; } void TimesheetXmlWriter::setIncludeTaskList(bool includeTaskList) { m_includeTaskList = includeTaskList; } QByteArray TimesheetXmlWriter::saveToXml() const { // now create the report: QDomDocument document = XmlSerialization::createXmlTemplate(m_templateName); // find metadata and report element: QDomElement root = document.documentElement(); QDomElement metadata = XmlSerialization::metadataElement(document); QDomElement charmVersion = document.createElement(QStringLiteral("charmversion")); QDomText charmVersionString = document.createTextNode(CharmVersion()); charmVersion.appendChild(charmVersionString); metadata.appendChild(charmVersion); auto installationId = document.createElement(QStringLiteral("installation-id")); const auto installationIdString = document.createTextNode(QString::number(CONFIGURATION.installationId)); installationId.appendChild(installationIdString); metadata.appendChild(installationId); QDomElement report = XmlSerialization::reportElement(document); Q_ASSERT(!root.isNull() && !metadata.isNull() && !report.isNull()); writeMetadata(document, metadata); TimeSheetInfoList timeSheetInfo = createTimeSheetInfo(); // extend report tag: add tasks and effort structure if (m_includeTaskList) { // tasks QDomElement tasks = document.createElement(QStringLiteral("tasks")); report.appendChild(tasks); Q_FOREACH (const TimeSheetInfo &info, timeSheetInfo) { if (info.taskId == 0) // the root task continue; const Task &modelTask = m_dataModel->getTask(info.taskId); tasks.appendChild(modelTask.toXml(document)); } } { // effort // make effort element: QDomElement effort = document.createElement(QStringLiteral("effort")); report.appendChild(effort); // aggregate (group by task and day): typedef QPair Key; QMap< Key, Event> events; Q_FOREACH (const Event &event, m_events) { TimeSheetInfoList::iterator it; for (it = timeSheetInfo.begin(); it != timeSheetInfo.end(); ++it) if ((*it).taskId == event.taskId()) break; if (it == timeSheetInfo.end()) continue; Key key(event.taskId(), event.startDateTime().date()); if (events.contains(key)) { // add to previous events: const Event &oldEvent = events[key]; const int seconds = oldEvent.duration() + event.duration(); const QDateTime start = oldEvent.startDateTime(); const QDateTime end(start.addSecs(seconds)); Q_ASSERT(start.secsTo(end) == seconds); Event newEvent(oldEvent); newEvent.setStartDateTime(start); newEvent.setEndDateTime(end); Q_ASSERT(newEvent.duration() == seconds); QString comment = oldEvent.comment(); if (!event.comment().isEmpty()) { if (!comment.isEmpty()) // make separator comment += QLatin1String(" / "); comment += event.comment(); newEvent.setComment(comment); } events[key] = newEvent; } else { // add this event: events[key] = event; events[key].setId(-events[key].id()); // "synthetic" :-) // move to start at midnight in UTC (for privacy reasons) // never, never, never use setTime() here, it breaks on DST changes! (twice a year) QDateTime start(event.startDateTime().date(), QTime(0, 0, 0, 0), Qt::UTC); QDateTime end(start.addSecs(event.duration())); events[key].setStartDateTime(start); events[key].setEndDateTime(end); Q_ASSERT(events[key].duration() == event.duration()); Q_ASSERT(start.time() == QTime(0, 0, 0, 0)); } } // create elements: Q_FOREACH (const Event &event, events) effort.appendChild(event.toXml(document)); } return document.toByteArray(4); } Charm-1.12.0/Charm/Reports/TimesheetXmlWriter.h000066400000000000000000000037161331066577000213120ustar00rootroot00000000000000/* TimesheetXmlWriter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TIMESHEETXMLWRITER_H #define TIMESHEETXMLWRITER_H #include #include "Core/Event.h" #include "Core/Task.h" class QByteArray; class QDomDocument; class QDomElement; class CharmDataModel; class TimeSheetInfo; class TimesheetXmlWriter { public: explicit TimesheetXmlWriter(const QString &templateName); virtual ~TimesheetXmlWriter(); const CharmDataModel *dataModel() const; void setDataModel(const CharmDataModel *model); TaskId rootTask() const; void setRootTask(TaskId rootTask); bool includeTaskList() const; void setIncludeTaskList(bool); /** * @throws XmlSerializationException */ QByteArray saveToXml() const; EventList events() const; void setEvents(const EventList &events); protected: virtual void writeMetadata(QDomDocument &document, QDomElement &metadata) const = 0; virtual QList createTimeSheetInfo() const = 0; private: const CharmDataModel *m_dataModel = nullptr; EventList m_events; TaskId m_rootTask = {}; QString m_templateName; bool m_includeTaskList = true; }; #endif Charm-1.12.0/Charm/Reports/WeeklyTimesheetXmlWriter.cpp000066400000000000000000000044661331066577000230310ustar00rootroot00000000000000/* WeeklyTimesheetXmlWriter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "WeeklyTimesheetXmlWriter.h" #include "TimesheetInfo.h" #include WeeklyTimesheetXmlWriter::WeeklyTimesheetXmlWriter() : TimesheetXmlWriter(QLatin1String("weekly-timesheet")) { } void WeeklyTimesheetXmlWriter::setYear(int year) { m_year = year; } void WeeklyTimesheetXmlWriter::setWeekNumber(int weekNumber) { m_weekNumber = weekNumber; } void WeeklyTimesheetXmlWriter::writeMetadata(QDomDocument &document, QDomElement &metadata) const { // extend metadata tag: add year, and serial (week) number: QDomElement yearElement = document.createElement(QStringLiteral("year")); metadata.appendChild(yearElement); QDomText text = document.createTextNode(QString::number(m_year)); yearElement.appendChild(text); QDomElement weekElement = document.createElement(QStringLiteral("serial-number")); weekElement.setAttribute(QStringLiteral("semantics"), QStringLiteral("week-number")); metadata.appendChild(weekElement); QDomText weektext = document.createTextNode(QString::number(m_weekNumber)); weekElement.appendChild(weektext); } QList WeeklyTimesheetXmlWriter::createTimeSheetInfo() const { static const int DaysInWeek = 7; return TimeSheetInfo::filteredTaskWithSubTasks( TimeSheetInfo::taskWithSubTasks(dataModel(), DaysInWeek, rootTask(), SecondsMap()), false); // here, we don't care about active or not, because we only report on the tasks } Charm-1.12.0/Charm/Reports/WeeklyTimesheetXmlWriter.h000066400000000000000000000025561331066577000224740ustar00rootroot00000000000000/* WeeklyTimesheetXmlWriter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef WEEKLYTIMESHEETXMLWRITER_H #define WEEKLYTIMESHEETXMLWRITER_H #include "TimesheetXmlWriter.h" class WeeklyTimesheetXmlWriter : public TimesheetXmlWriter { public: WeeklyTimesheetXmlWriter(); void setYear(int year); void setWeekNumber(int weekNumber); protected: void writeMetadata(QDomDocument &document, QDomElement &metadata) const override; QList createTimeSheetInfo() const override; private: int m_year = 0; int m_weekNumber = 0; }; #endif Charm-1.12.0/Charm/TaskModelAdapter.cpp000066400000000000000000000264441331066577000175710ustar00rootroot00000000000000/* TaskModelAdapter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: David Faure Author: Mike McQuaid This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TaskModelAdapter.h" #include "Data.h" #include "ViewHelpers.h" #include "Core/CharmConstants.h" #include "Core/Configuration.h" #include "Commands/CommandModifyTask.h" #include "Commands/CommandModifyEvent.h" #include #include TaskModelAdapter::TaskModelAdapter(CharmDataModel *parent) : QAbstractItemModel() , m_dataModel(parent) { m_dataModel->registerAdapter(this); } TaskModelAdapter::~TaskModelAdapter() { if (m_dataModel) m_dataModel->unregisterAdapter(this); } // reimplement QAbstractItemModel: int TaskModelAdapter::columnCount(const QModelIndex &) const { return Column_TaskColumnCount; } int TaskModelAdapter::rowCount(const QModelIndex &parent) const { if (parent.column() > 0) return 0; const TaskTreeItem *item = itemFor(parent); // every index has an item, the invalid index // has the root item Q_ASSERT(item); return item->childCount(); } QVariant TaskModelAdapter::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); const TaskTreeItem *item = itemFor(index); const TaskId id = item->task().id(); const Event &activeEvent = m_dataModel->activeEventFor(id); const bool isActive = activeEvent.isValid(); const QApplication *application = static_cast(QApplication::instance()); Q_ASSERT(application); // we assume this code is executed in a GUI app // handle roles that are treated all the same, everywhere: switch (role) { // problem: foreground role is never queried for case Qt::ForegroundRole: if (item->task().isCurrentlyValid()) { return application->palette().color(QPalette::Active, QPalette::Text); } else { return application->palette().color(QPalette::Disabled, QPalette::Text); } break; case Qt::BackgroundRole: if (item->task().isCurrentlyValid()) { return QVariant(); } else { QColor color("crimson"); color.setAlphaF(0.25); return color; } break; case Qt::DisplayRole: return DATAMODEL->taskIdAndNameString(item->task().id()); case Qt::DecorationRole: if (isActive) { return Data::activePixmap(); } else { return QVariant(); } break; case Qt::CheckStateRole: if (item->task().subscribed()) { return Qt::Checked; } else { return Qt::Unchecked; } break; case TasksViewRole_TaskDescription: return item->task().comment(); case TasksViewRole_Name: // now unused return item->task().name(); case TasksViewRole_RunningTime: return hoursAndMinutes(activeEvent.duration()); case TasksViewRole_TaskId: return id; case Qt::EditRole: // we edit the comment case TasksViewRole_UserComment: return activeEvent.comment(); case TasksViewRole_Filter: return DATAMODEL->taskIdAndFullNameString(item->task().id()); default: return QVariant(); } } QModelIndex TaskModelAdapter::index(int row, int column, const QModelIndex &parent) const { // sanity check: if (row < 0 || column < 0 || column >= Column_TaskColumnCount) return QModelIndex(); const TaskTreeItem *parentItem = itemFor(parent); Q_ASSERT(parentItem != 0); // more sanity checks: if (row >= parentItem->childCount()) return QModelIndex(); const TaskTreeItem &item = parentItem->child(row); if (item.isValid()) { return indexForTaskTreeItem(item, column); } else { return QModelIndex(); } } QModelIndex TaskModelAdapter::parent(const QModelIndex &index) const { if (!index.isValid()) return QModelIndex(); const TaskTreeItem *item = itemFor(index); const TaskTreeItem &parent = m_dataModel->parentItem(item->task()); if (!parent.isValid()) return QModelIndex(); // top level item return indexForTaskTreeItem(parent, 0); } Qt::ItemFlags TaskModelAdapter::flags(const QModelIndex &index) const { Qt::ItemFlags flags = 0; if (index.isValid()) { const TaskTreeItem *item = itemFor(index); flags = Qt::ItemIsUserCheckable|Qt::ItemIsSelectable|Qt::ItemIsEnabled; const bool isCurrent = item->task().isCurrentlyValid(); if (isCurrent) { const TaskId id = item->task().id(); const Event &activeEvent = m_dataModel->activeEventFor(id); const bool isActive = activeEvent.isValid(); if (isActive) flags |= Qt::ItemIsEditable; } } return flags; } bool TaskModelAdapter::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) return false; const TaskTreeItem *item = itemFor(index); Q_ASSERT(item != 0); Task task(item->task()); // make a copy, so that we can modify it if (role == Qt::EditRole) { Q_ASSERT(m_dataModel->isTaskActive(task.id())); const Event &old = m_dataModel->activeEventFor(task.id()); QString comment = value.toString(); Event event(old); event.setComment(comment); auto command = new CommandModifyEvent(event, old, this); VIEW.sendCommand(command); return true; } else if (role == Qt::CheckStateRole) { task.setSubscribed(!task.subscribed()); auto command = new CommandModifyTask(task, this); VIEW.sendCommand(command); return true; } return false; } void TaskModelAdapter::resetTasks() { beginResetModel(); endResetModel(); } void TaskModelAdapter::taskAboutToBeAdded(TaskId parentId, int pos) { const TaskTreeItem &parent = m_dataModel->taskTreeItem(parentId); beginInsertRows(indexForTaskTreeItem(parent, 0), pos, pos); } void TaskModelAdapter::taskAdded(TaskId) { endInsertRows(); } void TaskModelAdapter::taskParentChanged(TaskId task, TaskId oldParent, TaskId newParent) { // remove task from old parent: const TaskTreeItem &item = m_dataModel->taskTreeItem(task); const int row = item.row(); const TaskTreeItem &oldParentItem = m_dataModel->taskTreeItem(oldParent); beginRemoveRows(indexForTaskTreeItem(oldParentItem, 0), row, row); // the actual remove happens in the data model endRemoveRows(); // add task to new patent: const TaskTreeItem &newParentItem = m_dataModel->taskTreeItem(newParent); const int pos = newParentItem.childCount(); beginInsertRows(indexForTaskTreeItem(newParentItem, 0), pos, pos); // the actual insert happens in the data model endInsertRows(); } void TaskModelAdapter::taskModified(TaskId id) { const TaskTreeItem &item = m_dataModel->taskTreeItem(id); if (item.isValid()) { QModelIndex startIndex = indexForTaskTreeItem(item, 0); QModelIndex endIndex = indexForTaskTreeItem(item, Column_TaskId); emit dataChanged(startIndex, endIndex); } } void TaskModelAdapter::taskAboutToBeDeleted(TaskId id) { const TaskTreeItem &item = m_dataModel->taskTreeItem(id); const TaskTreeItem &parent = m_dataModel->parentItem(item.task()); int row = item.row(); Q_ASSERT(row != -1); beginRemoveRows(indexForTaskTreeItem(parent, 0), row, row); } void TaskModelAdapter::taskDeleted(TaskId) { endRemoveRows(); } void TaskModelAdapter::eventAdded(EventId id) { const Event &event = m_dataModel->eventForId(id); taskModified(event.taskId()); } void TaskModelAdapter::eventModified(EventId id, Event) { const Event &event = m_dataModel->eventForId(id); const TaskTreeItem &item = m_dataModel->taskTreeItem(event.taskId()); if (item.isValid()) { // find out about what fields have actually changed, so that no // ongoing edits are overridden (to fix till' s bug report) // -- DF: we can't do that anymore, with a single column. // see TasksViewDelegate::setEditorData for the fix. QModelIndex startIndex = indexForTaskTreeItem(item, 0); QModelIndex endIndex = indexForTaskTreeItem(item, Column_TaskId); emit dataChanged(startIndex, endIndex); } } void TaskModelAdapter::eventDeleted(EventId id) { eventAdded(id); } void TaskModelAdapter::eventActivated(EventId id) { // query the model to find out the task: const Event &event = m_dataModel->eventForId(id); if (event.isValid()) { taskModified(event.taskId()); emit eventActivationNotice(id); } } void TaskModelAdapter::eventDeactivated(EventId id) { // query the model to find out the task: const Event &event = m_dataModel->eventForId(id); if (event.isValid()) { taskModified(event.taskId()); emit eventDeactivationNotice(id); } } const TaskTreeItem *TaskModelAdapter::itemFor(const QModelIndex &index) const { if (index.isValid()) { return static_cast(index.internalPointer()); } else { return &m_dataModel->taskTreeItem(0); } } QModelIndex TaskModelAdapter::indexForTaskTreeItem(const TaskTreeItem &item, int column) const { if (item.isValid()) { // argl UUUUGGGLLYYYY // DF: how about reinterpret_cast(&item) ? const void *constVoidPointer = static_cast(&item); void *voidPointer = const_cast(constVoidPointer); return createIndex(item.row(), column, voidPointer); } else { return QModelIndex(); } } QModelIndex TaskModelAdapter::indexForTaskId(TaskId id) const { return indexForTaskTreeItem(m_dataModel->taskTreeItem(id)); } Task TaskModelAdapter::taskForIndex(const QModelIndex &index) const { const TaskTreeItem *item = itemFor(index); return item->task(); } bool TaskModelAdapter::taskIsActive(const Task &task) const { Q_ASSERT(m_dataModel != 0); return m_dataModel->isTaskActive(task.id()); } bool TaskModelAdapter::taskHasChildren(const Task &task) const { const TaskTreeItem &item = m_dataModel->taskTreeItem(task.id()); return item.childCount() > 0; } TaskList TaskModelAdapter::children(const Task &task) const { const TaskTreeItem &item = m_dataModel->taskTreeItem(task.id()); return item.children(); } bool TaskModelAdapter::taskIdExists(TaskId taskId) const { return m_dataModel->taskExists(taskId); } void TaskModelAdapter::commitCommand(CharmCommand *command) { Q_ASSERT(command->owner() == this); command->finalize(); } Charm-1.12.0/Charm/TaskModelAdapter.h000066400000000000000000000105161331066577000172270ustar00rootroot00000000000000/* TaskModelAdapter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TASKMODELADAPTER_H #define TASKMODELADAPTER_H #include #include #include "Core/TaskModelInterface.h" #include "Core/CharmDataModel.h" #include "Core/CharmDataModelAdapterInterface.h" #include "Core/CommandEmitterInterface.h" enum ViewColumns { Column_TaskId, // FIXME rename Column_TaskColumnCount }; enum TasksViewRoles { TasksViewRole_Name = 0x1045F132, TasksViewRole_RunningTime, TasksViewRole_UserComment, TasksViewRole_TaskId, TasksViewRole_Filter, ///< Role for search/filter TasksViewRole_TaskDescription = Qt::ToolTipRole }; typedef ViewColumns ViewColumn; /** TaskModelAdapter adapts the CharmDataModel to be used in the task view (in main view and "select task" dialog). It is a QAbstractItemModel, and stores the TaskTreeItem pointer of the respective address in the model indexes internal pointer. */ class TaskModelAdapter : public QAbstractItemModel, public TaskModelInterface, public CommandEmitterInterface, public CharmDataModelAdapterInterface { Q_OBJECT public: explicit TaskModelAdapter(CharmDataModel *parent); ~TaskModelAdapter() override; // reimplement QAbstractItemModel: int columnCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; // QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const; Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; // reimplement CharmDataModelAdapterInterface: void resetTasks() override; void taskAboutToBeAdded(TaskId parent, int pos) override; void taskAdded(TaskId id) override; void taskModified(TaskId id) override; void taskParentChanged(TaskId task, TaskId oldParent, TaskId newParent) override; void taskAboutToBeDeleted(TaskId) override; void taskDeleted(TaskId id) override; void resetEvents() override { } void eventAboutToBeAdded(EventId) override { } void eventAdded(EventId) override; void eventModified(EventId, Event) override; void eventAboutToBeDeleted(EventId) override { } void eventDeleted(EventId) override; void eventActivated(EventId id) override; void eventDeactivated(EventId id) override; // reimplement TaskModelInterface: Task taskForIndex(const QModelIndex &) const override; QModelIndex indexForTaskId(TaskId) const override; bool taskIsActive(const Task &task) const override; bool taskHasChildren(const Task &task) const override; bool taskIdExists(TaskId taskId) const override; TaskList children(const Task &task) const; // reimplement CommandEmitterInterface: void commitCommand(CharmCommand *) override; Q_SIGNALS: void eventActivationNotice(EventId id) override; void eventDeactivationNotice(EventId id) override; private: const TaskTreeItem *itemFor(const QModelIndex &) const; QModelIndex indexForTaskTreeItem(const TaskTreeItem &item, int column = 0) const; QPointer m_dataModel; }; #endif Charm-1.12.0/Charm/TemporaryValue.h000066400000000000000000000022431331066577000170200ustar00rootroot00000000000000/* This file is part of Charm, a task-based time tracking application. Copyright (C) 2009-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TEMPORARYVALUE_H #define TEMPORARYVALUE_H template struct TemporaryValue { explicit TemporaryValue(T &x, const T &value) : m_x(x) , m_oldValue(x) { m_x = value; } ~TemporaryValue() { m_x = m_oldValue; } T &m_x; const T m_oldValue; }; #endif Charm-1.12.0/Charm/UndoCharmCommandWrapper.cpp000066400000000000000000000025501331066577000211150ustar00rootroot00000000000000/* UndoCharmCommandWrapper.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Nicholas Van Sickle This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "UndoCharmCommandWrapper.h" UndoCharmCommandWrapper::UndoCharmCommandWrapper(CharmCommand *command) : m_command(command) { setText(command->description()); } UndoCharmCommandWrapper::~UndoCharmCommandWrapper() { delete m_command; } void UndoCharmCommandWrapper::undo() { m_command->requestRollback(); } void UndoCharmCommandWrapper::redo() { m_command->requestExecute(); } CharmCommand *UndoCharmCommandWrapper::command() const { return m_command; } Charm-1.12.0/Charm/UndoCharmCommandWrapper.h000066400000000000000000000026631331066577000205670ustar00rootroot00000000000000/* UndoCharmCommandWrapper.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Nicholas Van Sickle This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef UNDOCHARMCOMMANDWRAPPER_H #define UNDOCHARMCOMMANDWRAPPER_H #include #include "Core/CharmCommand.h" /** Thin wrapper for CharmCommand -> QUndoCommand It simply forwards the command text and emits signals for commit/rollback on undo/redo **/ class UndoCharmCommandWrapper : public QUndoCommand { public: explicit UndoCharmCommandWrapper(CharmCommand *command); ~UndoCharmCommandWrapper(); void undo() override; void redo() override; CharmCommand *command() const; private: CharmCommand *m_command; }; #endif Charm-1.12.0/Charm/ViewFilter.cpp000066400000000000000000000112061331066577000164530ustar00rootroot00000000000000/* ViewFilter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ViewFilter.h" #include "Core/CharmDataModel.h" #include "ViewHelpers.h" ViewFilter::ViewFilter(CharmDataModel *model, QObject *parent) : QSortFilterProxyModel(parent) , m_model(model) { setSourceModel(&m_model); // we filter for the task name column setFilterKeyColumn(Column_TaskId); // setFilterKeyColumn( -1 ); setFilterCaseSensitivity(Qt::CaseInsensitive); // relay signals to the view: connect(&m_model, &TaskModelAdapter::eventActivationNotice, this, &ViewFilter::eventActivationNotice); connect(&m_model, &TaskModelAdapter::eventDeactivationNotice, this, &ViewFilter::eventDeactivationNotice); sort(Column_TaskId); } ViewFilter::~ViewFilter() { } Task ViewFilter::taskForIndex(const QModelIndex &index) const { return m_model.taskForIndex(mapToSource(index)); } QModelIndex ViewFilter::indexForTaskId(TaskId id) const { return mapFromSource(m_model.indexForTaskId(id)); } bool ViewFilter::taskIsActive(const Task &task) const { return m_model.taskIsActive(task); } bool ViewFilter::taskHasChildren(const Task &task) const { return m_model.taskHasChildren(task); } void ViewFilter::prefilteringModeChanged() { invalidate(); } bool ViewFilter::filterAcceptsRow(int source_row, const QModelIndex &parent) const { // by default, QSortFilterProxyModel only accepts row where already the parents where accepted bool acceptedByFilter = QSortFilterProxyModel::filterAcceptsRow(source_row, parent); // in our case, this is not what we want, we want parents to be // accepted if any of their children are accepted (this is a // recursive call, and could possibly be slow): const QModelIndex index(m_model.index(source_row, 0, parent)); if (!index.isValid()) return acceptedByFilter; int rowCount = m_model.rowCount(index); for (int i = 0; i < rowCount; ++i) { if (filterAcceptsRow(i, index)) { acceptedByFilter = true; break; } } bool accepted = acceptedByFilter; const Task task = m_model.taskForIndex(index); switch (Configuration::instance().taskPrefilteringMode) { case Configuration::TaskPrefilter_ShowAll: break; case Configuration::TaskPrefilter_CurrentOnly: { const bool ok = (task.isCurrentlyValid() || checkChildren(task, HaveValidChild)); accepted &= ok; break; } case Configuration::TaskPrefilter_SubscribedOnly: { const bool ok = (task.subscribed() || checkChildren(task, HaveSubscribedChild)); accepted &= ok; break; } case Configuration::TaskPrefilter_SubscribedAndCurrentOnly: accepted &= ((task.subscribed() || checkChildren(task, HaveSubscribedChild)) && (task.isCurrentlyValid() || checkChildren(task, HaveValidChild))); break; default: break; } return accepted; } bool ViewFilter::filterAcceptsColumn(int, const QModelIndex &) const { return true; } bool ViewFilter::taskIdExists(TaskId taskId) const { return m_model.taskIdExists(taskId); } bool ViewFilter::checkChildren(Task task, CheckFor checkFor) const { if (taskHasChildren(task)) { const TaskList taskList = m_model.children(task); for (const Task &taskChild : taskList) { if (checkFor == HaveSubscribedChild && taskChild.subscribed()) { return true; } else if (checkFor == HaveValidChild && taskChild.isCurrentlyValid()) { return true; } } } return false; } void ViewFilter::commitCommand(CharmCommand *command) { // we do not emit signals, we are the relay (since we are a proxy): m_model.commitCommand(command); } Charm-1.12.0/Charm/ViewFilter.h000066400000000000000000000046401331066577000161240ustar00rootroot00000000000000/* ViewFilter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef VIEWFILTER_H #define VIEWFILTER_H #include #include "Core/Configuration.h" #include "TaskModelAdapter.h" #include "Core/TaskModelInterface.h" #include "Core/CommandEmitterInterface.h" class CharmDataModel; class CharmCommand; // ViewFilter is implemented as a decorator to avoid accidental direct // access to the task model with indexes of the proxy class ViewFilter : public QSortFilterProxyModel, public TaskModelInterface, public CommandEmitterInterface { Q_OBJECT public: explicit ViewFilter(CharmDataModel *, QObject *parent = nullptr); ~ViewFilter() override; // implement TaskModelInterface Task taskForIndex(const QModelIndex &) const override; QModelIndex indexForTaskId(TaskId) const override; bool taskIsActive(const Task &task) const override; bool taskHasChildren(const Task &task) const override; // filter for subscriptions: void prefilteringModeChanged(); bool taskIdExists(TaskId taskId) const override; void commitCommand(CharmCommand *) override; bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const override; bool filterAcceptsRow(int row, const QModelIndex &parent) const override; Q_SIGNALS: void eventActivationNotice(EventId id) override; void eventDeactivationNotice(EventId id) override; private: enum CheckFor { HaveValidChild, HaveSubscribedChild }; bool checkChildren(Task task, CheckFor checkFor) const; TaskModelAdapter m_model; }; #endif Charm-1.12.0/Charm/ViewHelpers.cpp000066400000000000000000000136261331066577000166400ustar00rootroot00000000000000/* ViewHelpers.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ViewHelpers.h" #include #include #include namespace { static QCollator collator() { QCollator c; c.setCaseSensitivity(Qt::CaseInsensitive); c.setNumericMode(true); return c; } } void Charm::connectControllerAndView(Controller *controller, CharmWindow *view) { // connect view and controller: // make controller process commands send by the view: QObject::connect(view, SIGNAL(emitCommand(CharmCommand*)), controller, SLOT(executeCommand(CharmCommand*))); QObject::connect(view, SIGNAL(emitCommandRollback(CharmCommand*)), controller, SLOT(rollbackCommand(CharmCommand*))); // make view receive done commands from the controller: QObject::connect(controller, SIGNAL(commandCompleted(CharmCommand*)), view, SLOT(commitCommand(CharmCommand*))); } class EventSorter { public: EventSorter(const Charm::SortOrderList &orders) : m_orders(orders) { Q_ASSERT(!m_orders.contains(Charm::SortOrder::None)); Q_ASSERT(!m_orders.isEmpty()); } template int compare(const T &left, const T &right) const { if (left < right) { return -1; } else if (left > right) { return 1; } return 0; } bool operator()(const EventId &leftId, const EventId &rightId) const { const Event &left = DATAMODEL->eventForId(leftId); const Event &right = DATAMODEL->eventForId(rightId); int result = -1; foreach (const auto order, m_orders) { switch (order) { case Charm::SortOrder::None: Q_UNREACHABLE(); case Charm::SortOrder::StartTime: result = compare(left.startDateTime(), right.startDateTime()); break; case Charm::SortOrder::EndTime: result = compare(left.endDateTime(), right.endDateTime()); break; case Charm::SortOrder::TaskId: result = compare(left.taskId(), right.taskId()); break; case Charm::SortOrder::Comment: result = Charm::collatorCompare(left.comment(), right.comment()); break; } if (result != 0) break; result = -1; } return result < 0; } private: const Charm::SortOrderList &m_orders; }; int Charm::collatorCompare(const QString &left, const QString &right) { static const auto collator(::collator()); return collator.compare(left, right); } EventIdList Charm::eventIdsSortedBy(EventIdList ids, const Charm::SortOrderList &orders) { if (!orders.isEmpty()) qStableSort(ids.begin(), ids.end(), EventSorter(orders)); return ids; } EventIdList Charm::eventIdsSortedBy(EventIdList ids, SortOrder order) { return eventIdsSortedBy(ids, SortOrderList(1) << order); } EventIdList Charm::filteredBySubtree(EventIdList ids, TaskId parent, bool exclude) { EventIdList result; bool isParent = false; Q_FOREACH (EventId id, ids) { const Event &event = DATAMODEL->eventForId(id); isParent = (parent == event.taskId() || DATAMODEL->isParentOf(parent, event.taskId())); if (isParent != exclude) result << id; } return result; } QString Charm::elidedTaskName(const QString &text, const QFont &font, int width) { QFontMetrics metrics(font); const QString &projectCode = text.section(QLatin1Char(' '), 0, 0, QString::SectionIncludeTrailingSep); const int projectCodeWidth = metrics.width(projectCode); if (width > projectCodeWidth) { const QString &taskName = text.section(QLatin1Char(' '), 1); const int taskNameWidth = width - projectCodeWidth; const QString &taskNameElided = metrics.elidedText(taskName, Qt::ElideLeft, taskNameWidth); return projectCode + taskNameElided; } return metrics.elidedText(text, Qt::ElideMiddle, width); } QString Charm::reportStylesheet(const QPalette &palette) { QString style; QFile stylesheet(QStringLiteral(":/Charm/report_stylesheet.sty")); if (stylesheet.open(QIODevice::ReadOnly | QIODevice::Text)) { style = QString::fromUtf8(stylesheet.readAll()); style.replace(QLatin1String("@header_row_background_color@"), palette.highlight().color().name()); style.replace(QLatin1String("@header_row_foreground_color@"), palette.highlightedText().color().name()); style.replace(QLatin1String("@alternate_row_background_color@"), palette.alternateBase().color().name()); style.replace(QLatin1String("@event_attributes_row_background_color@"), palette.midlight().color().name()); if (style.isEmpty()) qWarning() << "reportStylesheet: default style sheet is empty, too bad"; } else { qCritical() << "reportStylesheet: cannot load report style sheet:" << stylesheet.errorString(); } return style; } Charm-1.12.0/Charm/ViewHelpers.h000066400000000000000000000041001331066577000162700ustar00rootroot00000000000000/* ViewHelpers.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef VIEWHELPERS_H #define VIEWHELPERS_H #include "Core/Event.h" #include "Core/CharmConstants.h" #include "ApplicationCore.h" #include "ModelConnector.h" #define MODEL (ApplicationCore::instance().model()) #define DATAMODEL (MODEL.charmDataModel()) #define VIEW (ApplicationCore::instance().mainView()) #define TRAY (ApplicationCore::instance().trayIcon()) namespace Charm { enum class SortOrder { None = 0, StartTime, EndTime, TaskId, Comment }; typedef QVarLengthArray SortOrderList; void connectControllerAndView(Controller *, CharmWindow *); int collatorCompare(const QString &left, const QString &right); EventIdList eventIdsSortedBy(EventIdList, const SortOrderList &orders); EventIdList eventIdsSortedBy(EventIdList, SortOrder order); /** Return those ids in the input list that elements of the subtree * under the parent task, which includes the parent task. */ EventIdList filteredBySubtree(EventIdList, TaskId parent, bool exclude = false); QString elidedTaskName(const QString &text, const QFont &font, int width); QString reportStylesheet(const QPalette &palette); } Q_DECLARE_TYPEINFO(Charm::SortOrder, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(Charm::SortOrderList, Q_MOVABLE_TYPE); #endif Charm-1.12.0/Charm/WeeklySummary.cpp000066400000000000000000000053371331066577000172210ustar00rootroot00000000000000/* WeeklySummary.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "WeeklySummary.h" #include "Core/CharmDataModel.h" #include "Core/Event.h" #include "Core/Task.h" static const int DAYS_IN_WEEK = 7; WeeklySummary::WeeklySummary() : durations(DAYS_IN_WEEK, 0) { } QVector WeeklySummary::summariesForTimespan(CharmDataModel *dataModel, const TimeSpan ×pan) { const EventIdList eventIds = dataModel->eventsThatStartInTimeFrame(timespan); // prepare a list of unique task ids used within the time span: TaskIdList taskIds, uniqueTaskIds; // the list of tasks to show EventList events; Q_FOREACH (EventId id, eventIds) { Event event = dataModel->eventForId(id); events << event; taskIds << event.taskId(); } qSort(taskIds); std::unique_copy(taskIds.begin(), taskIds.end(), std::back_inserter(uniqueTaskIds)); Q_ASSERT(events.size() == eventIds.size()); // retrieve task information QVector summaries(uniqueTaskIds.size()); for (int i = 0; i < uniqueTaskIds.size(); ++i) { summaries[i].task = uniqueTaskIds.at(i); const Task &task = dataModel->getTask(uniqueTaskIds[i]); summaries[i].taskname = dataModel->fullTaskName(task); } // now add the times to the tasks: Q_FOREACH (const Event &event, events) { // find the index for this event: auto it = std::find(uniqueTaskIds.begin(), uniqueTaskIds.end(), event.taskId()); if (it != uniqueTaskIds.end()) { const int index = std::distance(uniqueTaskIds.begin(), it); Q_ASSERT(index >= 0 && index < summaries.size()); const int dayOfWeek = event.startDateTime().date().dayOfWeek() - 1; Q_ASSERT(dayOfWeek >= 0 && dayOfWeek < 7); summaries[index].durations[dayOfWeek] += event.duration(); } } return summaries; } Charm-1.12.0/Charm/WeeklySummary.h000066400000000000000000000025171331066577000166630ustar00rootroot00000000000000/* WeeklySummary.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef WEEKLYSUMMARY_H #define WEEKLYSUMMARY_H #include #include #include "Core/Task.h" #include "Core/TimeSpans.h" class CharmDataModel; class WeeklySummary { public: static QVector summariesForTimespan(CharmDataModel *dataModel, const TimeSpan ×pan); WeeklySummary(); TaskId task = {}; QString taskname; QVector durations; }; #endif // WEEKLYSUMMARY_H Charm-1.12.0/Charm/Widgets/000077500000000000000000000000001331066577000152755ustar00rootroot00000000000000Charm-1.12.0/Charm/Widgets/ActivityReport.cpp000066400000000000000000000522261331066577000210000ustar00rootroot00000000000000/* ActivityReport.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ActivityReport.h" #include "ApplicationCore.h" #include "DateEntrySyncer.h" #include "Data.h" #include "SelectTaskDialog.h" #include "Core/Configuration.h" #include "Core/Dates.h" #include #include #include #include #include #include #include #include #include "ui_ActivityReportConfigurationDialog.h" ActivityReportConfigurationDialog::ActivityReportConfigurationDialog(QWidget *parent) : ReportConfigurationDialog(parent) , m_ui(new Ui::ActivityReportConfigurationDialog) { setWindowTitle(tr("Activity Report")); m_ui->setupUi(this); m_ui->dateEditEnd->calendarWidget()->setFirstDayOfWeek(Qt::Monday); m_ui->dateEditEnd->calendarWidget()->setVerticalHeaderFormat(QCalendarWidget::ISOWeekNumbers); m_ui->dateEditStart->calendarWidget()->setFirstDayOfWeek(Qt::Monday); m_ui->dateEditStart->calendarWidget()->setVerticalHeaderFormat(QCalendarWidget::ISOWeekNumbers); m_ui->tabWidget->setCurrentIndex(0); connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &ActivityReportConfigurationDialog::accept); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &ActivityReportConfigurationDialog::reject); connect(m_ui->comboBox, static_cast(&QComboBox::currentIndexChanged), this, &ActivityReportConfigurationDialog::slotTimeSpanSelected ); connect(m_ui->addExcludeTaskButton, &QToolButton::clicked, this, &ActivityReportConfigurationDialog::slotExcludeTask); connect(m_ui->removeExcludeTaskButton, &QToolButton::clicked, this, &ActivityReportConfigurationDialog::slotRemoveExcludedTask); connect(m_ui->addIncludeTaskButton, &QToolButton::clicked, this, &ActivityReportConfigurationDialog::slotSelectTask); connect(m_ui->removeIncludeTaskButton, &QToolButton::clicked, this, &ActivityReportConfigurationDialog::slotRemoveIncludeTask); connect(m_ui->checkBoxGroupTasks, &QCheckBox::clicked, this, &ActivityReportConfigurationDialog::slotGroupTasks); connect(m_ui->checkBoxGroupTasksComments, &QCheckBox::clicked, this, &ActivityReportConfigurationDialog::slotGroupTasksComments); new DateEntrySyncer(m_ui->spinBoxStartWeek, m_ui->spinBoxStartYear, m_ui->dateEditStart, 1, this); new DateEntrySyncer(m_ui->spinBoxEndWeek, m_ui->spinBoxEndYear, m_ui->dateEditEnd, 7, this); QTimer::singleShot(0, this, SLOT(slotDelayedInitialization())); } ActivityReportConfigurationDialog::~ActivityReportConfigurationDialog() { } void ActivityReportConfigurationDialog::slotDelayedInitialization() { slotStandardTimeSpansChanged(); connect(ApplicationCore::instance().dateChangeWatcher(), &DateChangeWatcher::dateChanged, this, &ActivityReportConfigurationDialog::slotStandardTimeSpansChanged); // FIXME load settings } void ActivityReportConfigurationDialog::slotStandardTimeSpansChanged() { const TimeSpans timeSpans; m_timespans = timeSpans.standardTimeSpans(); NamedTimeSpan customRange = { tr("Select Range"), timeSpans.thisWeek().timespan, Range }; m_timespans << customRange; m_ui->comboBox->clear(); for (int i = 0; i < m_timespans.size(); ++i) m_ui->comboBox->addItem(m_timespans[i].name); } void ActivityReportConfigurationDialog::slotTimeSpanSelected(int index) { if (m_ui->comboBox->count() == 0 || index == -1) return; Q_ASSERT(m_ui->comboBox->count() > index); if (index == m_timespans.size() - 1) { // manual selection m_ui->groupBox->setEnabled(true); } else { m_ui->spinBoxStartYear->setValue(m_timespans[index].timespan.first.year()); m_ui->spinBoxStartWeek->setValue(m_timespans[index].timespan.first.weekNumber()); m_ui->spinBoxEndYear->setValue(m_timespans[index].timespan.second.year()); m_ui->spinBoxEndWeek->setValue(m_timespans[index].timespan.second.weekNumber()); m_ui->dateEditStart->setDate(m_timespans[index].timespan.first); m_ui->dateEditEnd->setDate(m_timespans[index].timespan.second); m_ui->groupBox->setEnabled(false); } } void ActivityReportConfigurationDialog::slotSelectTask() { TaskId taskId; if (selectTask(taskId) && !m_properties.rootTasks.contains(taskId)) { addListItem(taskId, m_ui->listWidgetIncludeTask); m_properties.rootTasks << taskId; } m_ui->removeIncludeTaskButton->setEnabled(!m_properties.rootTasks.isEmpty()); m_ui->listWidgetIncludeTask->setEnabled(!m_properties.rootTasks.isEmpty()); } void ActivityReportConfigurationDialog::slotExcludeTask() { TaskId taskId; if (selectTask(taskId) && !m_properties.rootExcludeTasks.contains(taskId)) { addListItem(taskId, m_ui->listWidgetExclude); m_properties.rootExcludeTasks << taskId; } m_ui->removeExcludeTaskButton->setEnabled(!m_properties.rootExcludeTasks.isEmpty()); m_ui->listWidgetExclude->setEnabled(!m_properties.rootExcludeTasks.isEmpty()); } void ActivityReportConfigurationDialog::slotRemoveExcludedTask() { QListWidgetItem *item = m_ui->listWidgetExclude->currentItem(); if (item) { m_properties.rootExcludeTasks.remove(item->data(Qt::UserRole).toInt()); delete item; } m_ui->removeExcludeTaskButton->setEnabled(!m_properties.rootExcludeTasks.isEmpty()); m_ui->listWidgetExclude->setEnabled(!m_properties.rootExcludeTasks.isEmpty()); } void ActivityReportConfigurationDialog::slotRemoveIncludeTask() { QListWidgetItem *item = m_ui->listWidgetIncludeTask->currentItem(); if (item) { m_properties.rootTasks.remove(item->data(Qt::UserRole).toInt()); delete item; } m_ui->removeIncludeTaskButton->setEnabled(!m_properties.rootTasks.isEmpty()); m_ui->listWidgetIncludeTask->setEnabled(!m_properties.rootTasks.isEmpty()); } void ActivityReportConfigurationDialog::slotGroupTasks(bool checked) { m_ui->checkBoxGroupTasksComments->setEnabled(!checked); } void ActivityReportConfigurationDialog::slotGroupTasksComments(bool checked) { m_ui->checkBoxGroupTasks->setEnabled(!checked); } bool ActivityReportConfigurationDialog::selectTask(TaskId &task) { SelectTaskDialog dialog(this); dialog.setNonTrackableSelectable(); const bool taskSelected = dialog.exec(); if (taskSelected) task = dialog.selectedTask(); return taskSelected; } QListWidgetItem *ActivityReportConfigurationDialog::addListItem(TaskId taskId, QListWidget *list) const { const TaskTreeItem &item = DATAMODEL->taskTreeItem(taskId); QListWidgetItem *listItem = new QListWidgetItem(DATAMODEL->smartTaskName(item.task()), list); listItem->setToolTip(DATAMODEL->fullTaskName(item.task())); listItem->setData(Qt::UserRole, taskId); return listItem; } void ActivityReportConfigurationDialog::accept() { // FIXME save settings QDialog::accept(); } void ActivityReportConfigurationDialog::reject() { QDialog::reject(); } void ActivityReportConfigurationDialog::showReportPreviewDialog() { const int index = m_ui->comboBox->currentIndex(); m_properties.timeSpanSelection = m_timespans[index]; if (index == m_timespans.size() - 1) { //Range m_properties.start = m_ui->dateEditStart->date(); m_properties.end = m_ui->dateEditEnd->date().addDays(1); } else { m_properties.start = m_timespans[index].timespan.first; m_properties.end = m_timespans[index].timespan.second; } m_properties.showFullDescription = m_ui->checkBoxFullDescription->isChecked(); m_properties.groupByTaskId = m_ui->checkBoxGroupTasks->isChecked(); m_properties.groupByTaskIdAndComments = m_ui->checkBoxGroupTasksComments->isChecked(); auto report = new ActivityReport(); report->setReportProperties(m_properties); report->show(); } ActivityReport::ActivityReport(QWidget *parent) : ReportPreviewWindow(parent) { saveToXmlButton()->hide(); saveToTextButton()->hide(); uploadButton()->hide(); connect(this, &ReportPreviewWindow::anchorClicked, this, &ActivityReport::slotLinkClicked); } ActivityReport::~ActivityReport() { } void ActivityReport::setReportProperties( const ActivityReportConfigurationDialog::Properties &properties) { m_properties = properties; slotUpdate(); } void ActivityReport::slotUpdate() { // retrieve matching events: EventIdList matchingEvents = DATAMODEL->eventsThatStartInTimeFrame(m_properties.start, m_properties.end); if (!m_properties.rootTasks.isEmpty()) { QSet filteredEvents; Q_FOREACH (TaskId include, m_properties.rootTasks) filteredEvents |= Charm::filteredBySubtree(matchingEvents, include).toSet(); matchingEvents = filteredEvents.toList(); } if (m_properties.groupByTaskId) { matchingEvents = Charm::eventIdsSortedBy(matchingEvents, Charm::SortOrderList() << Charm::SortOrder::TaskId << Charm::SortOrder::StartTime); } else if (m_properties.groupByTaskIdAndComments) { matchingEvents = Charm::eventIdsSortedBy(matchingEvents, Charm::SortOrderList() << Charm::SortOrder::TaskId << Charm::SortOrder::Comment << Charm::SortOrder::StartTime); } else { matchingEvents = Charm::eventIdsSortedBy(matchingEvents, Charm::SortOrderList() << Charm::SortOrder::StartTime); } // filter unproductive events: Q_FOREACH (TaskId exclude, m_properties.rootExcludeTasks) matchingEvents = Charm::filteredBySubtree(matchingEvents, exclude, true); // calculate total: int totalSeconds = 0; Q_FOREACH (EventId id, matchingEvents) { const Event &event = DATAMODEL->eventForId(id); Q_ASSERT(event.isValid()); totalSeconds += event.duration(); } // which TimeSpan type QString timeSpanTypeName; switch (m_properties.timeSpanSelection.timeSpanType) { case Day: timeSpanTypeName = tr("Day"); break; case Week: timeSpanTypeName = tr("Week"); break; case Month: timeSpanTypeName = tr("Month"); break; case Year: timeSpanTypeName = tr("Year"); break; case Range: timeSpanTypeName = tr("Range"); break; default: Q_ASSERT(false); // should not happen } auto report = new QTextDocument(this); QDomDocument doc = createReportTemplate(); QDomElement root = doc.documentElement(); QDomElement body = root.firstChildElement(QStringLiteral("body")); // create the caption: { QDomElement headline = doc.createElement(QStringLiteral("h1")); QDomText text = doc.createTextNode(tr("Activity Report")); headline.appendChild(text); body.appendChild(headline); } { QDomElement headline = doc.createElement(QStringLiteral("h3")); QString content = tr("Report for %1, from %2 to %3") .arg(CONFIGURATION.user.name(), m_properties.start.toString(Qt::TextDate), m_properties.end.toString(Qt::TextDate)); QDomText text = doc.createTextNode(content); headline.appendChild(text); body.appendChild(headline); QDomElement previousLink = doc.createElement(QStringLiteral("a")); previousLink.setAttribute(QStringLiteral("href"), QStringLiteral("Previous")); QDomText previousLinkText = doc.createTextNode(tr("").arg(timeSpanTypeName)); previousLink.appendChild(previousLinkText); body.appendChild(previousLink); QDomElement nextLink = doc.createElement(QStringLiteral("a")); nextLink.setAttribute(QStringLiteral("href"), QStringLiteral("Next")); QDomText nextLinkText = doc.createTextNode(tr("").arg(timeSpanTypeName)); nextLink.appendChild(nextLinkText); body.appendChild(nextLink); { QDomElement paragraph = doc.createElement(QStringLiteral("h4")); QString totalsText = tr("Total: %1").arg(hoursAndMinutes(totalSeconds)); QDomText totalsElement = doc.createTextNode(totalsText); paragraph.appendChild(totalsElement); body.appendChild(paragraph); } if (!m_properties.rootTasks.isEmpty()) { QDomElement paragraph = doc.createElement(QStringLiteral("p")); QString rootTaskText = tr("Activity under tasks:"); Q_FOREACH (TaskId taskId, m_properties.rootTasks) { const Task &task = DATAMODEL->getTask(taskId); rootTaskText.append(QStringLiteral(" ( %1 ),").arg(DATAMODEL->fullTaskName(task))); } rootTaskText = rootTaskText.mid(0, rootTaskText.length() - 1); QDomText rootText = doc.createTextNode(rootTaskText); paragraph.appendChild(rootText); body.appendChild(paragraph); } QDomElement paragraph = doc.createElement(QStringLiteral("br")); body.appendChild(paragraph); } { const QString Headlines[] = { tr("Date and Time, Task, Description") }; const int NumberOfColumns = sizeof Headlines / sizeof Headlines[0]; // now for a table QDomElement table = doc.createElement(QStringLiteral("table")); table.setAttribute(QStringLiteral("width"), QStringLiteral("100%")); table.setAttribute(QStringLiteral("align"), QStringLiteral("left")); table.setAttribute(QStringLiteral("cellpadding"), QStringLiteral("3")); table.setAttribute(QStringLiteral("cellspacing"), QStringLiteral("0")); body.appendChild(table); // table header QDomElement tableHead = doc.createElement(QStringLiteral("thead")); table.appendChild(tableHead); QDomElement headerRow = doc.createElement(QStringLiteral("tr")); headerRow.setAttribute(QStringLiteral("class"), QStringLiteral("header_row")); tableHead.appendChild(headerRow); // column headers for (int i = 0; i < NumberOfColumns; ++i) { QDomElement header = doc.createElement(QStringLiteral("th")); QDomText text = doc.createTextNode(Headlines[i]); header.appendChild(text); headerRow.appendChild(header); } QDomElement tableBody = doc.createElement(QStringLiteral("tbody")); table.appendChild(tableBody); // rows const bool groupTasks = m_properties.groupByTaskId || m_properties.groupByTaskIdAndComments; int groupTotalSeconds = 0; for (auto it = matchingEvents.constBegin(), end = matchingEvents.constEnd(); it != end; ++it) { const EventId id(*it); const Event &event = DATAMODEL->eventForId(id); Q_ASSERT(event.isValid()); bool nextMatch = false; if (groupTasks) { const auto next(it + 1); const EventId nextId(next != end ? *next : 0); const Event &nextEvent(DATAMODEL->eventForId(nextId)); nextMatch = event.taskId() == nextEvent.taskId(); if (nextMatch && m_properties.groupByTaskIdAndComments) nextMatch = Charm::collatorCompare(event.comment(), nextEvent.comment()) == 0; groupTotalSeconds += event.duration(); if (nextMatch) continue; } const TaskTreeItem &item = DATAMODEL->taskTreeItem(event.taskId()); const Task &task = item.task(); Q_ASSERT(task.isValid()); const auto paddedId = QStringLiteral("%1").arg(QString::number( task.id()).trimmed(), Configuration::instance().taskPaddingLength, QLatin1Char('0')); QStringList row1Texts; if (groupTasks) { row1Texts = QStringList { tr("%1 -- [%2] %3") .arg(hoursAndMinutes(groupTotalSeconds), paddedId, m_properties.showFullDescription ? DATAMODEL->fullTaskName( task) : task.name().trimmed()) }; } else { row1Texts = QStringList { tr("%1 %2-%3 (%4) -- [%5] %6") .arg(event.startDateTime().date().toString(Qt::SystemLocaleShortDate).trimmed(), event.startDateTime().time().toString(Qt::SystemLocaleShortDate).trimmed(), event.endDateTime().time().toString(Qt::SystemLocaleShortDate).trimmed(), hoursAndMinutes(event.duration()), paddedId, m_properties.showFullDescription ? DATAMODEL->fullTaskName( task) : task.name().trimmed()) }; } QDomElement row1 = doc.createElement(QStringLiteral("tr")); row1.setAttribute(QStringLiteral("class"), QStringLiteral("event_attributes_row")); QDomElement row2 = doc.createElement(QStringLiteral("tr")); for (int index = 0; index < NumberOfColumns; ++index) { QDomElement cell = doc.createElement(QStringLiteral("td")); cell.setAttribute(QStringLiteral("class"), QStringLiteral("event_attributes")); QDomText text = doc.createTextNode(row1Texts[index]); cell.appendChild(text); row1.appendChild(cell); } QDomElement cell2 = doc.createElement(QStringLiteral("td")); cell2.setAttribute(QStringLiteral("class"), QStringLiteral("event_description")); cell2.setAttribute(QStringLiteral("align"), QStringLiteral("left")); QDomElement preElement = doc.createElement(QStringLiteral("pre")); QDomText preText = doc.createTextNode( m_properties.groupByTaskId ? QString() : event.comment()); preElement.appendChild(preText); cell2.appendChild(preElement); row2.appendChild(cell2); tableBody.appendChild(row1); tableBody.appendChild(row2); if (groupTasks) { if (!nextMatch) groupTotalSeconds = 0; } } } // NOTE: seems like the style sheet has to be set before the html // code is pushed into the QTextDocument report->setDefaultStyleSheet(Charm::reportStylesheet(palette())); report->setHtml(doc.toString()); setDocument(report); } void ActivityReport::slotLinkClicked(const QUrl &which) { const int direction = which.toString() == QLatin1String("Previous") ? -1 : 1; switch (m_properties.timeSpanSelection.timeSpanType) { case Day: m_properties.start = m_properties.start.addDays(1 * direction); m_properties.end = m_properties.end.addDays(1 * direction); break; case Week: m_properties.start = m_properties.start.addDays(7 * direction); m_properties.end = m_properties.end.addDays(7 * direction); break; case Month: m_properties.start = m_properties.start.addMonths(1 * direction); m_properties.end = m_properties.end.addMonths(1 * direction); break; case Year: m_properties.start = m_properties.start.addYears(1 * direction); m_properties.end = m_properties.end.addYears(1 * direction); break; case Range: { const int spanRange = m_properties.start.daysTo(m_properties.end); m_properties.start = m_properties.start.addDays(spanRange * direction); m_properties.end = m_properties.end.addDays(spanRange * direction); break; } default: Q_ASSERT(false); // should not happen } setReportProperties(m_properties); } Charm-1.12.0/Charm/Widgets/ActivityReport.h000066400000000000000000000055741331066577000204510ustar00rootroot00000000000000/* ActivityReport.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef ACTIVITYREPORT_H #define ACTIVITYREPORT_H #include #include #include "ReportConfigurationDialog.h" #include "ReportPreviewWindow.h" #include "ViewHelpers.h" #include namespace Ui { class ActivityReportConfigurationDialog; } class QUrl; class QListWidgetItem; class QListWidget; class ActivityReportConfigurationDialog : public ReportConfigurationDialog { Q_OBJECT public: struct Properties { NamedTimeSpan timeSpanSelection; QDate start; QDate end; QSet rootTasks; QSet rootExcludeTasks; bool showFullDescription = false; bool groupByTaskId = false; bool groupByTaskIdAndComments = false; }; explicit ActivityReportConfigurationDialog(QWidget *parent); ~ActivityReportConfigurationDialog() override; void showReportPreviewDialog() override; public Q_SLOTS: void accept() override; void reject() override; private Q_SLOTS: void slotDelayedInitialization(); void slotStandardTimeSpansChanged(); void slotTimeSpanSelected(int); void slotSelectTask(); void slotExcludeTask(); void slotRemoveExcludedTask(); void slotRemoveIncludeTask(); void slotGroupTasks(bool checked); void slotGroupTasksComments(bool checked); private: bool selectTask(TaskId &task); QListWidgetItem *addListItem(TaskId id, QListWidget *list) const; QScopedPointer m_ui; QList m_timespans; ActivityReportConfigurationDialog::Properties m_properties; }; class ActivityReport : public ReportPreviewWindow { Q_OBJECT public: explicit ActivityReport(QWidget *parent = nullptr); ~ActivityReport() override; void setReportProperties(const ActivityReportConfigurationDialog::Properties &properties); private Q_SLOTS: void slotLinkClicked(const QUrl &which); private: void slotUpdate() override; private: ActivityReportConfigurationDialog::Properties m_properties; }; #endif Charm-1.12.0/Charm/Widgets/ActivityReportConfigurationDialog.ui000066400000000000000000000277251331066577000245110ustar00rootroot00000000000000 ActivityReportConfigurationDialog 0 0 386 296 0 0 Activity Report Configuration Page Qt::Horizontal 40 20 Time frame: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter QComboBox::AdjustToContents Qt::Horizontal 40 20 false Manual selection Start week Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 53 1970 10000 End week Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 53 1970 10000 (events that start at or after...) Start date Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter true (events that start before...) End date Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter true 0 0 0 Tasks Include tasks and subtasks (all tasks, otherwise). false Qt::ScrollBarAlwaysOff QAbstractScrollArea::AdjustToContents + - Qt::Horizontal 40 20 Exclude tasks and subtasks (no tasks, otherwise). false Qt::ScrollBarAlwaysOff QAbstractScrollArea::AdjustToContents + - Qt::Horizontal 40 20 Advanced 0 0 0 0 0 0 Advanced options for the report. Group by task id Show full task description Group by task id and comment Qt::Vertical 20 40 QDialogButtonBox::Cancel|QDialogButtonBox::Ok Charm-1.12.0/Charm/Widgets/BillDialog.cpp000066400000000000000000000052011331066577000200010ustar00rootroot00000000000000/* BillDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "BillDialog.h" #include #include #include BillDialog::BillDialog(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) { setResult(Later); QPalette p = palette(); QImage billImage(QStringLiteral(":/Charm/bill.jpg")); QBrush billBrush(billImage); p.setBrush(QPalette::Window, billBrush); setPalette(p); setAutoFillBackground(true); setMinimumSize(billImage.size()); setMaximumSize(billImage.size()); setWindowTitle(QStringLiteral("Yeah... about those timesheets...")); m_asYouWish = new QPushButton(QStringLiteral("As you wish")); connect(m_asYouWish, &QPushButton::clicked, this, &BillDialog::slotAsYouWish); m_alreadyDone = new QPushButton(QStringLiteral("Already done")); connect(m_alreadyDone, &QPushButton::clicked, this, &BillDialog::slotAlreadyDone); m_later = new QPushButton(QStringLiteral("Later")); connect(m_later, &QPushButton::clicked, this, &BillDialog::slotLater); auto layout = new QVBoxLayout(this); auto buttonBox = new QDialogButtonBox(); buttonBox->addButton(m_asYouWish, QDialogButtonBox::YesRole); buttonBox->addButton(m_alreadyDone, QDialogButtonBox::NoRole); buttonBox->addButton(m_later, QDialogButtonBox::RejectRole); layout->addWidget(buttonBox, 0, Qt::AlignBottom); } void BillDialog::setReport(int year, int week) { m_year = year; m_week = week; m_alreadyDone->setText(QStringLiteral("Already sent Week %1 (%2)").arg(week).arg(year)); } int BillDialog::year() const { return m_year; } int BillDialog::week() const { return m_week; } void BillDialog::slotAsYouWish() { done(AsYouWish); } void BillDialog::slotAlreadyDone() { done(AlreadyDone); } void BillDialog::slotLater() { done(Later); } Charm-1.12.0/Charm/Widgets/BillDialog.h000066400000000000000000000027141331066577000174540ustar00rootroot00000000000000/* BillDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef BillDialog_H #define BillDialog_H #include class BillDialog : public QDialog { Q_OBJECT public: enum BillResponse { Later, AsYouWish, AlreadyDone, }; explicit BillDialog(QWidget *parent = nullptr, Qt::WindowFlags f = 0); void setReport(int year, int week); int year() const; int week() const; private Q_SLOTS: void slotAsYouWish(); void slotAlreadyDone(); void slotLater(); private: QPushButton *m_asYouWish; QPushButton *m_alreadyDone; QPushButton *m_later; int m_year = 0; int m_week = 0; }; #endif Charm-1.12.0/Charm/Widgets/CharmAboutDialog.cpp000066400000000000000000000024571331066577000211560ustar00rootroot00000000000000/* CharmAboutDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmAboutDialog.h" #include "ui_CharmAboutDialog.h" #include CharmAboutDialog::CharmAboutDialog(QWidget *parent) : QDialog(parent) , m_ui(new Ui::CharmAboutDialog) { m_ui->setupUi(this); QString versionText = m_ui->versionLabel->text(); versionText.replace(QLatin1String("CHARM_VERSION"), CharmVersion()); m_ui->versionLabel->setText(versionText); } CharmAboutDialog::~CharmAboutDialog() { } Charm-1.12.0/Charm/Widgets/CharmAboutDialog.h000066400000000000000000000023371331066577000206200ustar00rootroot00000000000000/* CharmAboutDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARMABOUTDIALOG_H #define CHARMABOUTDIALOG_H #include #include namespace Ui { class CharmAboutDialog; } class CharmAboutDialog : public QDialog { Q_OBJECT public: explicit CharmAboutDialog(QWidget *parent = nullptr); ~CharmAboutDialog() override; private: QScopedPointer m_ui; }; #endif Charm-1.12.0/Charm/Widgets/CharmAboutDialog.ui000066400000000000000000000150751331066577000210110ustar00rootroot00000000000000 CharmAboutDialog 0 0 408 456 0 0 :/Charm/charmabout.png Qt::AlignCenter 18 75 true Charm Qt::AlignCenter 10 Version CHARM_VERSION Qt::AlignCenter true Qt::ScrollBarAlwaysOff false <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'.Lucida Grande UI'; font-size:13pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Sans Serif'; font-size:9pt;">Copyright © 2006-2018 The Charm Authors</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Sans Serif'; font-size:9pt;">Licensed under the terms of the GPL.</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;"><br /></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Lucida Grande'; font-size:9pt;">Charm is supported and maintained by </span><a href="http://www.kdab.com"><span style=" font-family:'Sans Serif'; font-size:9pt; text-decoration: underline; color:#0057ae;">KDAB</span></a></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Sans Serif'; font-size:9pt;">Qt Experts - Platform-independent software solutions</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Lucida Grande'; font-size:9pt;"><br /></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Lucida Grande'; font-size:9pt;">Original Author:</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Lucida Grande'; font-size:9pt;">Mirko Boehm</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Lucida Grande'; font-size:9pt;"><br /></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Lucida Grande'; font-size:9pt;">Current Maintainers:</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Lucida Grande'; font-size:9pt;">Frank Osterfeld, Guillermo Amaral</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Lucida Grande'; font-size:9pt;"><br /></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Lucida Grande'; font-size:9pt;">Contributors:</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Lucida Grande'; font-size:9pt;">Mike McQuaid (former maintainer), Till Adam, David Faure, Nuno Pinheiro, Pradeepto Batthacharya, Katrina Niolet, Nicholas Van Sickle, Jesper K. Pedersen, Sebastian Sauer, Michel Boyer de la Giroday, Hannah von Reth</span></p></body></html> Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse Charm-1.12.0/Charm/Widgets/CharmNewReleaseDialog.cpp000066400000000000000000000063411331066577000221320ustar00rootroot00000000000000/* CharmNewReleaseDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Michel Boyer de la Giroday This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmNewReleaseDialog.h" #include "ui_CharmNewReleaseDialog.h" #include #include #include #include CharmNewReleaseDialog::CharmNewReleaseDialog(QWidget *parent) : QDialog(parent) , m_ui(new Ui::CharmNewReleaseDialog) { m_ui->setupUi(this); m_skipUpdate = new QPushButton(tr("Skip Update")); connect(m_skipUpdate, &QPushButton::clicked, this, &CharmNewReleaseDialog::slotSkipVersion); m_remindMeLater = new QPushButton(tr("Remind Me Later")); connect(m_remindMeLater, &QPushButton::clicked, this, &CharmNewReleaseDialog::slotRemindMe); m_update = new QPushButton(tr("Update")); connect(m_update, &QPushButton::clicked, this, &CharmNewReleaseDialog::slotLaunchBrowser); m_ui->buttonBox->addButton(m_skipUpdate, QDialogButtonBox::NoRole); m_ui->buttonBox->addButton(m_remindMeLater, QDialogButtonBox::RejectRole); m_ui->buttonBox->addButton(m_update, QDialogButtonBox::AcceptRole); } void CharmNewReleaseDialog::setVersion(const QString &newVersion, const QString &localVersion) { QString versionText = m_ui->infoLB->text(); versionText.replace(QLatin1String("NEW"), newVersion); versionText.replace(QLatin1String("CURRENT"), localVersion); m_ui->infoLB->setText(versionText); m_version = newVersion; } void CharmNewReleaseDialog::setDownloadLink(const QUrl &link) { m_link = link; } void CharmNewReleaseDialog::setReleaseInformationLink(const QString &link) { QString hyperlink = m_ui->releaseInfoLabel->text(); hyperlink.replace(QLatin1String("LINK"), link); m_ui->releaseInfoLabel->setText(hyperlink); } void CharmNewReleaseDialog::slotLaunchBrowser() { if (!QDesktopServices::openUrl(m_link)) { QMessageBox::warning(this, tr("Warning"), tr( "Could not open url: %1 in your browser, please go to the Charm download page manually!").arg( m_link.toString())); } accept(); } void CharmNewReleaseDialog::slotSkipVersion() { QSettings settings; settings.beginGroup(QStringLiteral("UpdateChecker")); settings.setValue(QStringLiteral("skip-version"), m_version); settings.endGroup(); accept(); } void CharmNewReleaseDialog::slotRemindMe() { reject(); } CharmNewReleaseDialog::~CharmNewReleaseDialog() { } Charm-1.12.0/Charm/Widgets/CharmNewReleaseDialog.h000066400000000000000000000033221331066577000215730ustar00rootroot00000000000000/* CharmNewReleaseDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Michel Boyer de la Giroday This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARMNEWRELEASEDIALOG #define CHARMNEWRELEASEDIALOG #include #include #include namespace Ui { class CharmNewReleaseDialog; } class CharmNewReleaseDialog : public QDialog { Q_OBJECT public: explicit CharmNewReleaseDialog(QWidget *parent = nullptr); ~CharmNewReleaseDialog() override; void setVersion(const QString &newVersion, const QString &localVersion); void setDownloadLink(const QUrl &link); void setReleaseInformationLink(const QString &link); private Q_SLOTS: void slotLaunchBrowser(); void slotSkipVersion(); void slotRemindMe(); private: QUrl m_link; QString m_version; QPushButton *m_skipUpdate; QPushButton *m_remindMeLater; QPushButton *m_update; QScopedPointer m_ui; }; #endif // CHARMNEWRELEASEDIALOG Charm-1.12.0/Charm/Widgets/CharmNewReleaseDialog.ui000066400000000000000000000057551331066577000217750ustar00rootroot00000000000000 CharmNewReleaseDialog 0 0 424 226 New Update Available :/Charm/charmabout.png Qt::AlignCenter 16777215 16 <html><head/><body><p align="center"><span style=" font-size:11pt;">A new Charm release is available (NEW, you have: CURRENT)</span></p><p align="center"><br/></p></body></html> Qt::Horizontal 40 20 <a href="LINK">Release information</a> Qt::RichText true Qt::Horizontal 40 20 QDialogButtonBox::NoButton Charm-1.12.0/Charm/Widgets/CharmPreferences.cpp000066400000000000000000000156301331066577000212220ustar00rootroot00000000000000/* CharmPreferences.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld Author: Mathias Hasselmann This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmPreferences.h" #include "ApplicationCore.h" #include "MessageBox.h" #include "Core/Configuration.h" #include "Idle/IdleDetector.h" #include "Lotsofcake/Configuration.h" #include #include #include #include CharmPreferences::CharmPreferences(const Configuration &config, QWidget *parent_) : QDialog(parent_) { m_ui.setupUi(this); const bool haveIdleDetection = ApplicationCore::instance().idleDetector()->available(); const bool haveCommandInterface = (ApplicationCore::instance().commandInterface() != nullptr); const bool httpJobPossible = Lotsofcake::Configuration().isConfigured(); m_ui.lbWarnUnuploadedTimesheets->setVisible(httpJobPossible); m_ui.cbWarnUnuploadedTimesheets->setVisible(httpJobPossible); m_ui.cbIdleDetection->setEnabled(haveIdleDetection); m_ui.lbIdleDetection->setEnabled(haveIdleDetection); m_ui.cbIdleDetection->setChecked(config.detectIdling && m_ui.cbIdleDetection->isEnabled()); m_ui.cbWarnUnuploadedTimesheets->setChecked(config.warnUnuploadedTimesheets); m_ui.cbRequestEventComment->setChecked(config.requestEventComment); m_ui.lbCommandInterface->setVisible(haveCommandInterface); m_ui.cbEnableCommandInterface->setEnabled(haveCommandInterface); m_ui.cbEnableCommandInterface->setVisible(haveCommandInterface); m_ui.cbEnableCommandInterface->setChecked(haveCommandInterface && config.enableCommandInterface); connect(m_ui.cbWarnUnuploadedTimesheets, &QCheckBox::toggled, this, &CharmPreferences::slotWarnUnuploadedChanged); // this would not need a switch, but i hate casting enums to int: switch (config.timeTrackerFontSize) { case Configuration::TimeTrackerFont_Small: m_ui.cbTimeTrackerFontSize->setCurrentIndex(0); break; case Configuration::TimeTrackerFont_Regular: m_ui.cbTimeTrackerFontSize->setCurrentIndex(1); break; case Configuration::TimeTrackerFont_Large: m_ui.cbTimeTrackerFontSize->setCurrentIndex(2); break; } switch (config.durationFormat) { case Configuration::Minutes: m_ui.cbDurationFormat->setCurrentIndex(0); break; case Configuration::Decimal: m_ui.cbDurationFormat->setCurrentIndex(1); break; } switch (config.toolButtonStyle) { case Qt::ToolButtonIconOnly: m_ui.cbToolButtonStyle->setCurrentIndex(0); break; case Qt::ToolButtonTextOnly: m_ui.cbToolButtonStyle->setCurrentIndex(1); break; case Qt::ToolButtonTextUnderIcon: m_ui.cbToolButtonStyle->setCurrentIndex(2); break; case Qt::ToolButtonTextBesideIcon: m_ui.cbToolButtonStyle->setCurrentIndex(3); break; case Qt::ToolButtonFollowStyle: m_ui.cbToolButtonStyle->setCurrentIndex(4); break; } m_ui.sbNumberOfTaskSelectorEntries->setValue(config.numberOfTaskSelectorEntries); // resize( minimumSize() ); } CharmPreferences::~CharmPreferences() { } bool CharmPreferences::detectIdling() const { return m_ui.cbIdleDetection->isChecked(); } bool CharmPreferences::warnUnuploadedTimesheets() const { return m_ui.cbWarnUnuploadedTimesheets->isChecked(); } bool CharmPreferences::requestEventComment() const { return m_ui.cbRequestEventComment->isChecked(); } bool CharmPreferences::enableCommandInterface() const { return m_ui.cbEnableCommandInterface->isChecked(); } int CharmPreferences::numberOfTaskSelectorEntries() const { return m_ui.sbNumberOfTaskSelectorEntries->value(); } Configuration::DurationFormat CharmPreferences::durationFormat() const { switch (m_ui.cbDurationFormat->currentIndex()) { case 0: return Configuration::Minutes; case 1: return Configuration::Decimal; default: Q_ASSERT(!"Unexpected combobox item for DurationFormat"); } return Configuration::Minutes; } Configuration::TimeTrackerFontSize CharmPreferences::timeTrackerFontSize() const { switch (m_ui.cbTimeTrackerFontSize->currentIndex()) { case 0: return Configuration::TimeTrackerFont_Small; break; case 1: return Configuration::TimeTrackerFont_Regular; break; case 2: return Configuration::TimeTrackerFont_Large; break; default: Q_ASSERT(false); // somebody added an item } // always return something, to avoid compiler warning: return Configuration::TimeTrackerFont_Regular; } Qt::ToolButtonStyle CharmPreferences::toolButtonStyle() const { switch (m_ui.cbToolButtonStyle->currentIndex()) { case 0: return Qt::ToolButtonIconOnly; break; case 1: return Qt::ToolButtonTextOnly; break; case 2: return Qt::ToolButtonTextUnderIcon; break; case 3: return Qt::ToolButtonTextBesideIcon; break; case 4: return Qt::ToolButtonFollowStyle; break; default: Q_ASSERT(false); // somebody added an item } // always return something, to avoid compiler warning: return Qt::ToolButtonIconOnly; } void CharmPreferences::slotWarnUnuploadedChanged(bool enabled) { if (!Lotsofcake::Configuration().isConfigured()) return; if (!enabled) { const int response = MessageBox::question(this, tr("Bill is sad :(."), tr( "Bill has always been misunderstood. All he really wants is your reports, and even when he doesn't get them you only have to evade him once per hour. I'm sure you want to keep Bill's gentle reminders?"), tr("Mmmmkay"), tr("No, Stop Bill"), QMessageBox::Yes); if (response == QMessageBox::Yes) m_ui.cbWarnUnuploadedTimesheets->setCheckState(Qt::Checked); } } Charm-1.12.0/Charm/Widgets/CharmPreferences.h000066400000000000000000000032351331066577000206650ustar00rootroot00000000000000/* CharmPreferences.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARMPREFERENCES_H #define CHARMPREFERENCES_H #include #include "Core/Configuration.h" #include "ui_CharmPreferences.h" class CharmPreferences : public QDialog { Q_OBJECT public: explicit CharmPreferences(const Configuration &config, QWidget *parent = nullptr); ~CharmPreferences() override; Configuration::DurationFormat durationFormat() const; bool detectIdling() const; bool warnUnuploadedTimesheets() const; bool requestEventComment() const; bool enableCommandInterface() const; int numberOfTaskSelectorEntries() const; Qt::ToolButtonStyle toolButtonStyle() const; Configuration::TimeTrackerFontSize timeTrackerFontSize() const; private Q_SLOTS: void slotWarnUnuploadedChanged(bool); private: Ui::CharmPreferences m_ui; }; #endif Charm-1.12.0/Charm/Widgets/CharmPreferences.ui000066400000000000000000000226071331066577000210570ustar00rootroot00000000000000 CharmPreferences 0 0 505 311 Charm Configuration true Icons Text Text under icon Text beside icon System default 0 0 Enable Bill Lumbergh cbWarnUnuploadedTimesheets Minutes (hh:mm) Decimal (h.xx) true true true 0 0 Time tracker window font size cbTimeTrackerFontSize 0 0 Enable command interface Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter cbEnableCommandInterface 0 0 Duration format (requires restart) false cbDurationFormat 0 0 Enable idle detection cbIdleDetection Small Regular (Application Font) Large 0 0 Comment finished events cbRequestEventComment 0 0 Buttons show cbToolButtonStyle 0 0 Limit recently used tasks to sbNumberOfTaskSelectorEntries 5 Qt::Vertical 20 5 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok cbTimeTrackerFontSize cbToolButtonStyle cbDurationFormat cbIdleDetection cbWarnUnuploadedTimesheets buttonBox buttonBox accepted() CharmPreferences accept() 248 254 157 274 buttonBox rejected() CharmPreferences reject() 316 260 286 274 Charm-1.12.0/Charm/Widgets/CharmWindow.cpp000066400000000000000000000153141331066577000202270ustar00rootroot00000000000000/* CharmWindow.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmWindow.h" #include "ApplicationCore.h" #include "Data.h" #include "ViewHelpers.h" #include "WidgetUtils.h" #include "Commands/CommandRelayCommand.h" #include "Core/CharmCommand.h" #include "Core/CharmConstants.h" #include #include #include #include #include #include #include #include CharmWindow::CharmWindow(const QString &name, QWidget *parent) : QMainWindow(parent) , m_openCharmAction(new QAction(tr("Open Charm"), this)) , m_showAction(new QAction(this)) { setWindowName(name); connect(m_openCharmAction, SIGNAL(triggered(bool)), SLOT(showView())); connect(m_showAction, SIGNAL(triggered(bool)), SLOT(showView())); connect(this, &CharmWindow::visibilityChanged, this, &CharmWindow::handleShow); m_toolBar = addToolBar(QStringLiteral("Toolbar")); m_toolBar->setMovable(false); emit visibilityChanged(false); } void CharmWindow::stateChanged(State) { switch (ApplicationCore::instance().state()) { case Connecting: setEnabled(false); restoreGuiState(); configurationChanged(); break; case Connected: configurationChanged(); ApplicationCore::instance().createFileMenu(menuBar()); insertEditMenu(); ApplicationCore::instance().createWindowMenu(menuBar()); ApplicationCore::instance().createHelpMenu(menuBar()); setEnabled(true); break; case Disconnecting: setEnabled(false); saveGuiState(); break; case ShuttingDown: case Dead: default: break; } } void CharmWindow::setWindowName(const QString &text) { m_windowName = text; setWindowTitle(text); } QString CharmWindow::windowName() const { return m_windowName; } void CharmWindow::setWindowIdentifier(const QString &id) { m_windowIdentifier = id; } QString CharmWindow::windowIdentifier() const { return m_windowIdentifier; } void CharmWindow::setWindowNumber(int number) { m_windowNumber = number; delete m_shortcut; m_shortcut = new QShortcut(this); QKeySequence sequence(tr("Ctrl+%1").arg(number)); #ifdef Q_OS_OSX m_shortcut->setKey(sequence); #endif m_shortcut->setContext(Qt::ApplicationShortcut); m_showAction->setShortcut(sequence); connect(m_shortcut, SIGNAL(activated()), SLOT(showHideView())); connect(m_shortcut, SIGNAL(activated()), SLOT(showView())); } int CharmWindow::windowNumber() const { return m_windowNumber; } QToolBar *CharmWindow::toolBar() const { return m_toolBar; } QAction *CharmWindow::openCharmAction() { return m_openCharmAction; } QAction *CharmWindow::showAction() { return m_showAction; } void CharmWindow::restore() { show(); } void CharmWindow::checkVisibility() { const auto visibility = isVisible(); if (m_isVisibility != visibility) { m_isVisibility = visibility; emit visibilityChanged(m_isVisibility); } } void CharmWindow::showEvent(QShowEvent *e) { checkVisibility(); QMainWindow::showEvent(e); } void CharmWindow::hideEvent(QHideEvent *e) { checkVisibility(); QMainWindow::hideEvent(e); } void CharmWindow::sendCommand(CharmCommand *cmd) { cmd->prepare(); auto relay = new CommandRelayCommand(this); relay->setCommand(cmd); emit emitCommand(relay); } void CharmWindow::sendCommandRollback(CharmCommand *cmd) { cmd->prepare(); auto relay = new CommandRelayCommand(this); relay->setCommand(cmd); emit emitCommandRollback(relay); } void CharmWindow::handleShow(bool visible) { const QString text = tr("Show %1").arg(m_windowName); m_showAction->setText(text); m_showAction->setEnabled(!visible); } void CharmWindow::commitCommand(CharmCommand *command) { command->finalize(); } void CharmWindow::keyPressEvent(QKeyEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent->modifiers() & Qt::ControlModifier && keyEvent->key() == Qt::Key_W && isVisible()) { showHideView(); return; } } QMainWindow::keyPressEvent(event); } void CharmWindow::showView(QWidget *w) { w->show(); w->setWindowState(w->windowState() & ~Qt::WindowMinimized); w->raise(); w->activateWindow(); } bool CharmWindow::showHideView(QWidget *w) { // hide or restore the view if (w->isVisible()) { w->hide(); return false; } else { showView(w); return true; } } void CharmWindow::showView() { showView(this); } void CharmWindow::showHideView() { showHideView(this); } void CharmWindow::configurationChanged() { WidgetUtils::updateToolButtonStyle(this); } void CharmWindow::saveGuiState() { Q_ASSERT(!windowIdentifier().isEmpty()); QSettings settings; settings.beginGroup(windowIdentifier()); // save geometry WidgetUtils::saveGeometry(this, MetaKey_MainWindowGeometry); settings.setValue(MetaKey_MainWindowVisible, isVisible()); } void CharmWindow::restoreGuiState() { const QString identifier = windowIdentifier(); Q_ASSERT(!identifier.isEmpty()); // restore geometry QSettings settings; settings.beginGroup(identifier); WidgetUtils::restoreGeometry(this, MetaKey_MainWindowGeometry); // restore visibility bool visible = true; if (m_hideAtStartUp) { visible = false; } else { // Time Tracking Window should always be visible, except when Charm is started with --hide-at-start visible = (identifier == QLatin1String("window_tracking")) ? true : settings.value( MetaKey_MainWindowVisible).toBool(); } setVisible(visible); } void CharmWindow::setHideAtStartup(const bool &value) { m_hideAtStartUp = value; } Charm-1.12.0/Charm/Widgets/CharmWindow.h000066400000000000000000000062401331066577000176720ustar00rootroot00000000000000/* CharmWindow.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARMWINDOW_H #define CHARMWINDOW_H #include #include "Core/UIStateInterface.h" #include "Core/CommandEmitterInterface.h" class QAction; class QShortcut; class CharmWindow : public QMainWindow, public UIStateInterface { Q_OBJECT public: explicit CharmWindow(const QString &name, QWidget *parent = nullptr); QAction *showAction(); QAction *openCharmAction(); QString windowName() const; QString windowIdentifier() const; int windowNumber() const; virtual QToolBar *toolBar() const; protected: /** The window name is the human readable name the application uses to reference the window. */ void setWindowName(const QString &name); /** The window identifier is used to reference window specific configuration groups, et cetera. * It is generally not recommend to change it once the application is in use. */ void setWindowIdentifier(const QString &id); /** The window number is a Mac concept that allows to pull up application windows by entering CMD+. */ void setWindowNumber(int number); /** Insert the Edit menu. Empty by default. */ virtual void insertEditMenu() { } void checkVisibility(); public: void stateChanged(State previous) override; void showEvent(QShowEvent *) override; void hideEvent(QHideEvent *) override; void keyPressEvent(QKeyEvent *event) override; void saveGuiState() override; void restoreGuiState() override; static void showView(QWidget *w); static bool showHideView(QWidget *w); void setHideAtStartup(const bool &); Q_SIGNALS: void visibilityChanged(bool); void saveConfiguration(); public Q_SLOTS: void sendCommandRollback(CharmCommand *); void sendCommand(CharmCommand *); void commitCommand(CharmCommand *) override; virtual void restore(); void showView(); void showHideView(); void configurationChanged() override; private Q_SLOTS: void handleShow(bool visible); private: QString m_windowName; QAction *m_openCharmAction; QAction *m_showAction; int m_windowNumber = -1; // Mac numerical window number, used for shortcut etc QString m_windowIdentifier; QShortcut *m_shortcut = nullptr; QToolBar *m_toolBar; bool m_isVisibility = false; bool m_hideAtStartUp = false; }; #endif Charm-1.12.0/Charm/Widgets/CommentEditorPopup.cpp000066400000000000000000000046331331066577000216040ustar00rootroot00000000000000/* CommentEditorPopup.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mathias Hasselmann This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommentEditorPopup.h" #include "ui_CommentEditorPopup.h" #include #include #include #include #include "ViewHelpers.h" CommentEditorPopup::CommentEditorPopup(QWidget *parent) : QDialog(parent) , ui(new Ui::CommentEditorPopup) { ui->setupUi(this); ui->buttonBox->button(QDialogButtonBox::Ok)->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return)); ui->textEdit->setFocus(Qt::TabFocusReason); } CommentEditorPopup::~CommentEditorPopup() { delete ui; } void CommentEditorPopup::loadEvent(EventId id) { Event event = DATAMODEL->eventForId(id); if (!event.isValid()) { m_id = EventId(); return; } m_id = id; ui->textEdit->setPlainText(event.comment()); } void CommentEditorPopup::accept() { const QString t = ui->textEdit->toPlainText(); Event event = DATAMODEL->eventForId(m_id); if (event.isValid()) { event.setComment(t); DATAMODEL->modifyEvent(event); } else { // event already gone? should never happen, but you never know QPointer that(this); QMessageBox::critical(this, tr("Error"), tr( "Could not save the comment, the edited event was deleted in the meantime."), QMessageBox::Ok); if (!that) // in case the popup was deleted while the msg box was open return; } QDialog::accept(); } Charm-1.12.0/Charm/Widgets/CommentEditorPopup.h000066400000000000000000000025371331066577000212520ustar00rootroot00000000000000/* CommentEditorPopup.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mathias Hasselmann This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMENTEDITORPOPUP_H #define COMMENTEDITORPOPUP_H #include #include "Core/Event.h" namespace Ui { class CommentEditorPopup; } class CommentEditorPopup : public QDialog { Q_OBJECT public: explicit CommentEditorPopup(QWidget *parent = nullptr); ~CommentEditorPopup() override; public Q_SLOTS: void loadEvent(EventId id); void accept() override; private: Ui::CommentEditorPopup *ui; EventId m_id = {}; }; #endif // COMMENTEDITORPOPUP_H Charm-1.12.0/Charm/Widgets/CommentEditorPopup.ui000066400000000000000000000034161331066577000214350ustar00rootroot00000000000000 CommentEditorPopup 0 0 274 243 Comment Event true true false Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() CommentEditorPopup accept() 248 254 157 274 buttonBox rejected() CommentEditorPopup reject() 316 260 286 274 Charm-1.12.0/Charm/Widgets/ConfigurationDialog.cpp000066400000000000000000000052221331066577000217310ustar00rootroot00000000000000/* ConfigurationDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ConfigurationDialog.h" #include "Core/CharmConstants.h" #include ConfigurationDialog::ConfigurationDialog(const Configuration &config, QWidget *parent) : QDialog(parent) , m_config(config) { m_ui.setupUi(this); m_ui.nameLineEdit->setText(config.user.name()); m_ui.databaseLocation->setText(config.localStorageDatabase); connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &ConfigurationDialog::reject); connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &ConfigurationDialog::accept); #ifdef Q_OS_ANDROID setWindowState(windowState() | Qt::WindowMaximized); #endif } Configuration ConfigurationDialog::configuration() const { return m_config; } void ConfigurationDialog::on_databaseLocation_textChanged(const QString &) { checkInput(); } void ConfigurationDialog::accept() { m_config.installationId = m_config.createInstallationId(); m_config.user.setId(1); m_config.user.setName(m_ui.nameLineEdit->text()); m_config.localStorageType = CHARM_SQLITE_BACKEND_DESCRIPTOR; m_config.localStorageDatabase = m_ui.databaseLocation->text(); m_config.newDatabase = true; // m_config.failure = false; currently set by application QDialog::accept(); } void ConfigurationDialog::on_databaseLocationButton_clicked() { QString filename = QFileDialog::getSaveFileName(this, tr("Choose Database Location...")); if (!filename.isNull()) m_ui.databaseLocation->setText(filename); } void ConfigurationDialog::on_nameLineEdit_textChanged(const QString &) { checkInput(); } void ConfigurationDialog::checkInput() { const bool ok = !m_ui.databaseLocation->text().isEmpty() && !m_ui.nameLineEdit->text().isEmpty(); m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(ok); } Charm-1.12.0/Charm/Widgets/ConfigurationDialog.h000066400000000000000000000030351331066577000213760ustar00rootroot00000000000000/* ConfigurationDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CONFIGURATIONDIALOG_H #define CONFIGURATIONDIALOG_H #include #include "ApplicationCore.h" #include "Core/Configuration.h" #include "ui_ConfigurationDialog.h" class ConfigurationDialog : public QDialog { Q_OBJECT public: explicit ConfigurationDialog(const Configuration &, QWidget *parent); Configuration configuration() const; private Q_SLOTS: void on_databaseLocationButton_clicked(); void on_databaseLocation_textChanged(const QString &text); void on_nameLineEdit_textChanged(const QString &text); void accept() override; private: void checkInput(); Configuration m_config; Ui::ConfigurationDialog m_ui; }; #endif Charm-1.12.0/Charm/Widgets/ConfigurationDialog.ui000066400000000000000000000064161331066577000215720ustar00rootroot00000000000000 ConfigurationDialog 0 0 415 254 Database Configuration Full name: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Database: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Choose... 0 0 Select the location where the Charm database should be stored. The location is relative to your home directory. If in doubt, please use the predefined location. Your full name is used to create nicer reports and is stored within the database. Qt::PlainText false Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true Qt::Vertical 20 40 QDialogButtonBox::Cancel|QDialogButtonBox::Ok Charm-1.12.0/Charm/Widgets/DateEntrySyncer.cpp000066400000000000000000000055671331066577000211010ustar00rootroot00000000000000/* DateEntrySyncer.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "DateEntrySyncer.h" #include "Core/Dates.h" #include #include DateEntrySyncer::DateEntrySyncer(QSpinBox *week, QSpinBox *year, QDateEdit *date, int weekDay, QObject *parent) : QObject(parent) , m_week(week) , m_year(year) , m_date(date) , m_weekDay(weekDay) { connect(m_week, SIGNAL(valueChanged(int)), this, SLOT(dateSelectionChanged())); connect(m_year, SIGNAL(valueChanged(int)), this, SLOT(dateSelectionChanged())); if (m_date) connect(m_date, SIGNAL(dateChanged(QDate)), this, SLOT(dateSelectionChanged())); } // number of weeks per year differs between 52 and 53, so we need to set the maximum value accordingly, and fix the value if the user flips through years static void fixWeek(QSpinBox *yearSb, QSpinBox *weekSb) { const int year = yearSb->value(); const int week = weekSb->value(); const int maxWeek = Charm::numberOfWeeksInYear(year); Q_ASSERT(maxWeek >= 52); const int newWeek = qMin(maxWeek, week); weekSb->blockSignals(true); weekSb->setMaximum(maxWeek); weekSb->setValue(newWeek); weekSb->blockSignals(false); } void DateEntrySyncer::dateSelectionChanged() { if (sender() == m_week || sender() == m_year) { //spinboxes changed, update date edit fixWeek(m_year, m_week); const int week = m_week->value(); const int year = m_year->value(); if (m_date) { m_date->blockSignals(true); m_date->setDate(Charm::dateByWeekNumberAndWeekDay(year, week, m_weekDay)); m_date->blockSignals(false); } } else { Q_ASSERT(m_date); //date edit changed, update spinboxes const QDate date = m_date->date(); int year = 0; const int week = date.weekNumber(&year); m_year->blockSignals(true); m_week->blockSignals(true); m_year->setValue(year); m_week->setValue(week); m_week->blockSignals(false); m_year->blockSignals(false); } } Charm-1.12.0/Charm/Widgets/DateEntrySyncer.h000066400000000000000000000025361331066577000205370ustar00rootroot00000000000000/* DateEntrySyncer.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef DATEENTRYSYNCER_H #define DATEENTRYSYNCER_H #include class QSpinBox; class QDateEdit; class DateEntrySyncer : public QObject { Q_OBJECT public: DateEntrySyncer(QSpinBox *weekNumberSB, QSpinBox *yearSB, QDateEdit *dateedit, int weekDay = 1, QObject *parent = nullptr); private Q_SLOTS: void dateSelectionChanged(); private: QSpinBox *m_week; QSpinBox *m_year; QDateEdit *m_date; int m_weekDay; }; #endif //DATEENTRYSYNCER_H Charm-1.12.0/Charm/Widgets/EnterVacationDialog.cpp000066400000000000000000000212701331066577000216650ustar00rootroot00000000000000/* EnterVacationDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "EnterVacationDialog.h" #include "SelectTaskDialog.h" #include "ViewHelpers.h" #include "Commands/CommandMakeEvent.h" #include "Core/Dates.h" #include "Core/Event.h" #include #include #include #include #include "ui_EnterVacationDialog.h" static bool isWorkDay(const QDate &date) { return date.dayOfWeek() != Qt::Saturday && date.dayOfWeek() != Qt::Sunday; } static QString formatDuration(const QDateTime &start, const QDateTime &end) { Q_ASSERT(start <= end); const int secs = start.secsTo(end); Q_ASSERT(secs % 60 == 0); const int totalMinutes = secs / 60; const int hours = totalMinutes / 60; const int minutes = totalMinutes % 60; if (minutes == 0) { return QObject::tr("%1 hours", "hours", hours).arg(hours); } else { return QObject::tr("%1 hours %2 minutes").arg(QString::number(hours), QString::number(minutes)); } } static EventList createEventList(const QDate &start, const QDate &end, int minutes, const TaskId &taskId) { Q_ASSERT(start < end); EventList events; const int days = start.daysTo(end); events.reserve(days); for (int i = 0; i < days; ++i) { const QDate date = start.addDays(i); //for each work day, create an event starting at 8 am if (isWorkDay(date)) { const QDateTime startTime = QDateTime(date, QTime(8, 0)); const QDateTime endTime = startTime.addSecs(minutes * 60); Event event; event.setTaskId(taskId); event.setStartDateTime(startTime); event.setEndDateTime(endTime); event.setComment(QObject::tr("(created by vacation dialog)")); events.append(event); } } return events; } EnterVacationDialog::EnterVacationDialog(QWidget *parent) : QDialog(parent) , m_ui(new Ui::EnterVacationDialog) { setWindowTitle(tr("Enter Vacation")); m_ui->setupUi(this); m_ui->startDate->calendarWidget()->setFirstDayOfWeek(Qt::Monday); m_ui->startDate->calendarWidget()->setVerticalHeaderFormat(QCalendarWidget::ISOWeekNumbers); m_ui->endDate->calendarWidget()->setFirstDayOfWeek(Qt::Monday); m_ui->endDate->calendarWidget()->setVerticalHeaderFormat(QCalendarWidget::ISOWeekNumbers); //set next week as default range const QDate referenceDate = QDate::currentDate().addDays(7); m_ui->startDate->setDate(Charm::weekDayInWeekOf(Qt::Monday, referenceDate)); m_ui->endDate->setDate(Charm::weekDayInWeekOf(Qt::Friday, referenceDate)); connect(m_ui->startDate, &QDateEdit::dateChanged, this, &EnterVacationDialog::updateButtonStates); connect(m_ui->endDate, &QDateEdit::dateChanged, this, &EnterVacationDialog::updateButtonStates); connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &EnterVacationDialog::okClicked); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &EnterVacationDialog::reject); connect(m_ui->selectTaskButton, &QPushButton::clicked, this, &EnterVacationDialog::selectTask); QSettings settings; settings.beginGroup(QStringLiteral("EnterVacation")); m_ui->hoursSpinBox->setValue(settings.value(QStringLiteral("workHours"), 8).toInt()); m_ui->minutesSpinBox->setValue(settings.value(QStringLiteral("workMinutes"), 0).toInt()); m_selectedTaskId = settings.value(QStringLiteral("selectedTaskId"), -1).toInt(); //reset stored ID if task does not exist anymore: if (!DATAMODEL->taskExists(m_selectedTaskId)) m_selectedTaskId = -1; updateButtonStates(); updateTaskLabel(); } EnterVacationDialog::~EnterVacationDialog() { } void EnterVacationDialog::createEvents() { const EventList events = createEventList(m_ui->startDate->date(), m_ui->endDate->date().addDays(1), m_ui->hoursSpinBox->value() * 60 + m_ui->minutesSpinBox->value(), m_selectedTaskId); QDialog confirmationDialog(this); auto layout = new QVBoxLayout(&confirmationDialog); auto label = new QLabel(tr("The following vacation events will be created.")); label->setWordWrap(true); layout->addWidget(label); auto textBrowser = new QTextBrowser; layout->addWidget(textBrowser); auto box = new QDialogButtonBox; box->setStandardButtons(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); box->button(QDialogButtonBox::Ok)->setText(tr("Create")); connect(box, &QDialogButtonBox::accepted, &confirmationDialog, &QDialog::accept); connect(box, &QDialogButtonBox::rejected, &confirmationDialog, &QDialog::reject); layout->addWidget(box); const QString startDate = m_ui->startDate->date().toString(Qt::TextDate); const QString endDate = m_ui->endDate->date().toString(Qt::TextDate); const Task task = DATAMODEL->getTask(m_selectedTaskId); const QString htmlStartDate = startDate.toHtmlEscaped(); const QString htmlEndDate = endDate.toHtmlEscaped(); const QString htmlTaskName = task.name().toHtmlEscaped(); QString html = QStringLiteral(""); html += QStringLiteral("

    %1

    ").arg(tr("Vacation")); html += QStringLiteral("

    %1

    ").arg(tr("From %1 to %2").arg(htmlStartDate, htmlEndDate)); html += QStringLiteral("

    %1

    ").arg(tr("Task used: %1").arg(htmlTaskName)); html += QLatin1String("

    "); Q_FOREACH (const Event &event, events) { const QDate eventStart = event.startDateTime().date(); const QDate eventEnd = event.endDateTime().date(); Q_ASSERT(eventStart == eventEnd); Q_UNUSED(eventEnd) //release mode const QString shortDate = eventStart.toString(Qt::DefaultLocaleShortDate); const QString duration = formatDuration(event.startDateTime(), event.endDateTime()); const QString htmlShortDate = shortDate.toHtmlEscaped(); const QString htmlDuration = duration.toHtmlEscaped(); html += QStringLiteral("%1").arg(tr("%1: %3", "short date, duration").arg(htmlShortDate, htmlDuration)); html += QLatin1String("

    "); } html += QLatin1String("

    "); html += QLatin1String(""); textBrowser->setHtml(html); confirmationDialog.resize(400, 600); if (confirmationDialog.exec() == QDialog::Accepted) m_events = events; } void EnterVacationDialog::okClicked() { QSettings settings; settings.beginGroup(QStringLiteral("EnterVacation")); settings.setValue(QStringLiteral("workHours"), m_ui->hoursSpinBox->value()); settings.setValue(QStringLiteral("workMinutes"), m_ui->minutesSpinBox->value()); settings.setValue(QStringLiteral("selectedTaskId"), m_selectedTaskId); createEvents(); QDialog::accept(); } void EnterVacationDialog::updateButtonStates() { const bool validTask = DATAMODEL->taskExists(m_selectedTaskId); const bool validDates = m_ui->startDate->date() <= m_ui->endDate->date(); const bool validDuration = m_ui->hoursSpinBox->value() > 0 || m_ui->minutesSpinBox->value() > 0; m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled( validTask && validDates && validDuration); } void EnterVacationDialog::updateTaskLabel() { const Task task = DATAMODEL->getTask(m_selectedTaskId); if (!task.isValid()) { m_ui->taskLabel->setText(tr("(No task selected)")); } else { m_ui->taskLabel->setText(task.name()); } updateGeometry(); } void EnterVacationDialog::selectTask() { SelectTaskDialog dialog(this); if (dialog.exec() != QDialog::Accepted) return; m_selectedTaskId = dialog.selectedTask(); updateTaskLabel(); updateButtonStates(); } EventList EnterVacationDialog::events() const { return m_events; } Charm-1.12.0/Charm/Widgets/EnterVacationDialog.h000066400000000000000000000030351331066577000213310ustar00rootroot00000000000000/* EnterVacationDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef ENTERVACATIONDIALOG_H #define ENTERVACATIONDIALOG_H #include #include "Core/Event.h" #include "Core/Task.h" #include namespace Ui { class EnterVacationDialog; } class EnterVacationDialog : public QDialog { Q_OBJECT public: explicit EnterVacationDialog(QWidget *parent = nullptr); ~EnterVacationDialog() override; EventList events() const; private: void updateTaskLabel(); void createEvents(); private Q_SLOTS: void selectTask(); void okClicked(); void updateButtonStates(); private: QScopedPointer m_ui; TaskId m_selectedTaskId = -1; EventList m_events; }; #endif Charm-1.12.0/Charm/Widgets/EnterVacationDialog.ui000066400000000000000000000105411331066577000215170ustar00rootroot00000000000000 EnterVacationDialog 0 0 474 171 Start date: true End date: true Task: TASK Qt::Horizontal 40 20 Select Task... Day: 24 hours 59 10 minutes Qt::Horizontal 40 20 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() EnterVacationDialog accept() 248 254 157 274 buttonBox rejected() EnterVacationDialog reject() 316 260 286 274 Charm-1.12.0/Charm/Widgets/EventEditor.cpp000066400000000000000000000205521331066577000202350ustar00rootroot00000000000000/* EventEditor.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "EventEditor.h" #include "SelectTaskDialog.h" #include "ViewHelpers.h" #include "Commands/CommandMakeEvent.h" #include "Core/CharmConstants.h" #include "Core/CharmDataModel.h" #include "Core/TaskTreeItem.h" #include #include #include "ui_EventEditor.h" EventEditor::EventEditor(const Event &event, QWidget *parent) : QDialog(parent) , m_ui(new Ui::EventEditor) , m_event(event) { m_ui->setupUi(this); m_ui->dateEditEnd->calendarWidget()->setFirstDayOfWeek(Qt::Monday); m_ui->dateEditEnd->calendarWidget()->setVerticalHeaderFormat(QCalendarWidget::ISOWeekNumbers); m_ui->dateEditStart->calendarWidget()->setFirstDayOfWeek(Qt::Monday); m_ui->dateEditStart->calendarWidget()->setVerticalHeaderFormat(QCalendarWidget::ISOWeekNumbers); // Ctrl+Return for OK m_ui->buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL + Qt::Key_Return); // connect stuff: connect(m_ui->spinBoxHours, SIGNAL(valueChanged(int)), SLOT(durationHoursEdited(int))); connect(m_ui->spinBoxMinutes, SIGNAL(valueChanged(int)), SLOT(durationMinutesEdited(int))); connect(m_ui->dateEditStart, &QDateEdit::dateChanged, this, &EventEditor::startDateChanged); connect(m_ui->timeEditStart, &QTimeEdit::timeChanged, this, &EventEditor::startTimeChanged); connect(m_ui->dateEditEnd, &QDateEdit::dateChanged, this, &EventEditor::endDateChanged); connect(m_ui->timeEditEnd, &QTimeEdit::timeChanged, this, &EventEditor::endTimeChanged); connect(m_ui->pushButtonSelectTask, &QPushButton::clicked, this, &EventEditor::selectTaskClicked); connect(m_ui->textEditComment, &QTextEdit::textChanged, this, &EventEditor::commentChanged); connect(m_ui->startToNowButton, &QPushButton::clicked, this, &EventEditor::startToNowButtonClicked); connect(m_ui->endToNowButton, &QPushButton::clicked, this, &EventEditor::endToNowButtonClicked); // what a fricking hack - but QDateTimeEdit does not seem to have // a simple function to toggle 12h and 24h mode: // yeah, I know, this will survive changes in the user prefs, but // only for this instance of the edit dialog QString originalDateTimeFormat = m_ui->timeEditStart->displayFormat(); QString format = originalDateTimeFormat .remove(QStringLiteral("ap")) .remove(QStringLiteral("AP")) .simplified(); m_ui->timeEditStart->setDisplayFormat(format); m_ui->timeEditEnd->setDisplayFormat(format); // initialize to some sensible values, unless we got something valid passed in if (!m_event.isValid()) { QSettings settings; QDateTime start = settings.value(MetaKey_LastEventEditorDateTime, QDateTime::currentDateTime()).toDateTime(); m_event.setStartDateTime(start); m_event.setEndDateTime(start); m_endDateChanged = false; } updateValues(true); } EventEditor::~EventEditor() { } void EventEditor::accept() { QSettings settings; settings.setValue(MetaKey_LastEventEditorDateTime, m_event.endDateTime()); QDialog::accept(); } Event EventEditor::eventResult() const { return m_event; } void EventEditor::durationHoursEdited(int) { updateEndTime(); updateValues(); } void EventEditor::durationMinutesEdited(int) { updateEndTime(); updateValues(); } void EventEditor::updateEndTime() { int duration = 3600 * m_ui->spinBoxHours->value() + 60 * m_ui->spinBoxMinutes->value(); QDateTime endTime = m_event.startDateTime().addSecs(duration); m_event.setEndDateTime(endTime); } void EventEditor::startDateChanged(const QDate &date) { QDateTime start = m_event.startDateTime(); start.setDate(date); int delta = m_event.startDateTime().secsTo(m_event.endDateTime()); m_event.setStartDateTime(start); if (!m_endDateChanged) m_event.setEndDateTime(start.addSecs(delta)); updateValues(); } void EventEditor::startTimeChanged(const QTime &time) { QDateTime start = m_event.startDateTime(); start.setTime(time); m_event.setStartDateTime(start); updateValues(); } void EventEditor::endDateChanged(const QDate &date) { QDateTime end = m_event.endDateTime(); end.setDate(date); m_event.setEndDateTime(end); updateValues(); if (!m_updating) m_endDateChanged = true; } void EventEditor::endTimeChanged(const QTime &time) { QDateTime end = m_event.endDateTime(); end.setTime(time); m_event.setEndDateTime(end); updateValues(); } void EventEditor::selectTaskClicked() { SelectTaskDialog dialog(this); if (dialog.exec()) { m_event.setTaskId(dialog.selectedTask()); updateValues(); } } void EventEditor::commentChanged() { m_event.setComment(m_ui->textEditComment->toPlainText()); updateValues(); } struct SignalBlocker { explicit SignalBlocker(QObject *o) : m_object(o) , m_previous(o->signalsBlocked()) { o->blockSignals(true); } ~SignalBlocker() { m_object->blockSignals(m_previous); } QObject *m_object; bool m_previous; }; void EventEditor::updateValues(bool all) { if (m_updating) return; m_updating = true; m_ui->buttonBox->button(QDialogButtonBox::Ok) ->setEnabled(m_event.endDateTime() >= m_event.startDateTime()); const TaskTreeItem &taskTreeItem = MODEL.charmDataModel()->taskTreeItem(m_event.taskId()); m_ui->dateEditStart->setDate(m_event.startDateTime().date()); m_ui->timeEditStart->setTime(m_event.startDateTime().time()); bool active = MODEL.charmDataModel()->isEventActive(m_event.id()); m_ui->dateEditEnd->setEnabled(!active); m_ui->timeEditEnd->setEnabled(!active); m_ui->spinBoxHours->setEnabled(!active); m_ui->spinBoxMinutes->setEnabled(!active); m_ui->pushButtonSelectTask->setEnabled(!active); m_ui->startToNowButton->setEnabled(!active); m_ui->endToNowButton->setEnabled(!active); if (!active) { m_ui->dateEditEnd->setDate(m_event.endDateTime().date()); m_ui->timeEditEnd->setTime(m_event.endDateTime().time()); } else { m_ui->dateEditEnd->setDate(QDate::currentDate()); m_ui->timeEditEnd->setTime(QTime::currentTime()); } if (all) m_ui->textEditComment->setText(m_event.comment()); int durationHours = qMax(m_event.duration() / 3600, 0); int durationMinutes = qMax((m_event.duration() % 3600) / 60, 0); { // block signals to prevent updates of the start/end edits const SignalBlocker blocker1(m_ui->spinBoxHours); const SignalBlocker blocker2(m_ui->spinBoxMinutes); m_ui->spinBoxHours->setValue(durationHours); m_ui->spinBoxMinutes->setValue(durationMinutes); } QString name = MODEL.charmDataModel()->fullTaskName(taskTreeItem.task()); m_ui->labelTaskName->setText(name); QString format = m_ui->dateEditStart->displayFormat() .remove(QStringLiteral("ap")) .remove(QStringLiteral("AP")) .simplified(); m_ui->dateEditStart->setDisplayFormat(format); m_ui->dateEditEnd->setDisplayFormat(format); m_updating = false; } void EventEditor::startToNowButtonClicked() { m_event.setStartDateTime(QDateTime::currentDateTime()); updateValues(); } void EventEditor::endToNowButtonClicked() { m_event.setEndDateTime(QDateTime::currentDateTime()); updateValues(); } Charm-1.12.0/Charm/Widgets/EventEditor.h000066400000000000000000000037041331066577000177020ustar00rootroot00000000000000/* EventEditor.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef EVENTEDITOR_H #define EVENTEDITOR_H #include #include #include "Core/Event.h" namespace Ui { class EventEditor; } class EventEditor : public QDialog { Q_OBJECT public: explicit EventEditor(const Event &event, QWidget *parent = nullptr); ~EventEditor() override; // return the result after the dialog has been accepted Event eventResult() const; protected Q_SLOTS: void accept() override; private Q_SLOTS: void durationHoursEdited(int); void durationMinutesEdited(int); void startDateChanged(const QDate &); void startTimeChanged(const QTime &); void endDateChanged(const QDate &); void endTimeChanged(const QTime &); void selectTaskClicked(); void commentChanged(); void startToNowButtonClicked(); void endToNowButtonClicked(); private: void updateEndTime(); void updateValues(bool all = false); QScopedPointer m_ui; Event m_event; bool m_updating = false; bool m_endDateChanged = true; }; #endif /* EVENTEDITOR_H */ Charm-1.12.0/Charm/Widgets/EventEditor.ui000066400000000000000000000475651331066577000201050ustar00rootroot00000000000000 EventEditor 0 0 471 381 Edit Event 0 1 255 255 255 66 66 66 99 99 99 82 82 82 33 33 33 44 44 44 255 255 255 255 255 255 255 255 255 0 0 0 66 66 66 0 0 0 33 33 33 255 255 220 0 0 0 255 255 255 66 66 66 99 99 99 82 82 82 33 33 33 44 44 44 255 255 255 255 255 255 255 255 255 0 0 0 66 66 66 0 0 0 33 33 33 255 255 220 0 0 0 33 33 33 66 66 66 99 99 99 82 82 82 33 33 33 44 44 44 33 33 33 255 255 255 33 33 33 66 66 66 66 66 66 0 0 0 66 66 66 255 255 220 0 0 0 true (Task name) Qt::PlainText Qt::AlignCenter true 12 Select task... Start: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter dateEditStart QAbstractSpinBox::UpDownArrows true h:mm AP Now QAbstractSpinBox::UpDownArrows true h:mm AP Now End: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter dateEditEnd Duration: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter spinBoxHours 0 0 hour 0 999999999 0 0 true min 59 Qt::Horizontal 180 24 Comment: Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing textEditComment 0 3 true false Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok false pushButtonSelectTask dateEditStart timeEditStart dateEditEnd timeEditEnd spinBoxHours spinBoxMinutes textEditComment buttonBox buttonBox accepted() EventEditor accept() 429 445 391 2 buttonBox rejected() EventEditor reject() 345 454 452 5 Charm-1.12.0/Charm/Widgets/EventEditorDelegate.cpp000066400000000000000000000162551331066577000216750ustar00rootroot00000000000000/* EventEditorDelegate.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "EventEditorDelegate.h" #include "Data.h" #include "EventModelFilter.h" #include "ViewHelpers.h" #include "Core/CharmConstants.h" #include "Core/Event.h" #include #include EventEditorDelegate::EventEditorDelegate(EventModelFilter *model, QObject *parent) : QItemDelegate(parent) , m_model(model) { } QSize EventEditorDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { // to have the size hint recalculated, simply set m_cachedSizeHint // to an invalid value (m_cachedSizeHint = QSize();) if (!m_cachedSizeHint.isValid()) { const Event &event = m_model->eventForIndex(index); Q_ASSERT(event.isValid()); const TaskTreeItem &item = DATAMODEL->taskTreeItem(event.taskId()); QPixmap pixmap(option.rect.size()); // temp QPainter painter(&pixmap); m_cachedSizeHint = paint(&painter, option, taskName(item), dateAndDuration(event), 42, EventState_Locked).size(); } return m_cachedSizeHint; } void EventEditorDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { const Event &event = m_model->eventForIndex(index); Q_ASSERT(event.isValid()); const TaskTreeItem &item = DATAMODEL->taskTreeItem(event.taskId()); if (event.isValid()) { bool locked = DATAMODEL->isEventActive(event.id()); paint(painter, option, taskName(item), dateAndDuration(event), logDuration(event.duration()), locked ? EventState_Locked : EventState_Default); } } QString EventEditorDelegate::taskName(const TaskTreeItem &item) const { QString taskName; QTextStream taskStream(&taskName); // print leading zeroes for the TaskId const int taskIdLength = CONFIGURATION.taskPaddingLength; taskStream << QStringLiteral("%1").arg(item.task().id(), taskIdLength, 10, QLatin1Char('0')) << " " << DATAMODEL->smartTaskName(item.task()); return taskName; } QString EventEditorDelegate::dateAndDuration(const Event &event) const { QString dateAndDuration; QTextStream dateStream(&dateAndDuration); QDate date = event.startDateTime().date(); QTime time = event.startDateTime().time(); QTime endTime = event.endDateTime().time(); dateStream << date.toString(Qt::SystemLocaleDate) << " " << time.toString(QStringLiteral("h:mm")) << " - " << endTime.toString(QStringLiteral("h:mm")) << " (" << hoursAndMinutes(event.duration()) << ") Week " << date.weekNumber(); return dateAndDuration; } QRect EventEditorDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QString &taskName, const QString ×pan, double logDuration, EventState state) const { painter->save(); const QPalette &palette = option.palette; const QFont &mainFont = option.font; painter->setFont(mainFont); QFont detailFont(mainFont); detailFont.setPointSizeF(mainFont.pointSizeF() * 0.8); QPixmap decoration; QColor foreground; QColor background; switch (state) { case EventState_Locked: decoration = Data::editorLockedPixmap(); foreground = palette.color(QPalette::Disabled, QPalette::WindowText); background = palette.color(QPalette::Disabled, QPalette::Window); break; case EventState_Dirty: decoration = Data::editorDirtyPixmap(); foreground = palette.color(QPalette::Active, QPalette::WindowText); background = palette.color(QPalette::Active, QPalette::Window); break; case EventState_Default: foreground = palette.color(QPalette::Active, QPalette::WindowText); background = palette.color(QPalette::Active, QPalette::Window); break; } if (option.state & QStyle::State_Selected) { QBrush brush(palette.color(QPalette::Active, QPalette::Highlight)); painter->fillRect(option.rect, brush); if (state != EventState_Locked) { foreground = palette.color( QPalette::Active, QPalette::HighlightedText); } } painter->setPen(foreground); // draw line 1 and decoration: painter->setFont(mainFont); QRect taskRect(option.rect); taskRect.setWidth(option.rect.width() - decoration.width()); QPoint decorationPoint(option.rect.width() - decoration.width(), option.rect.center().y() - decoration.height() / 2); QRect boundingRect; QString elidedTask = Charm::elidedTaskName(taskName, mainFont, taskRect.width()); painter->drawText(taskRect, Qt::AlignLeft | Qt::AlignTop, elidedTask, &boundingRect); taskRect = boundingRect; taskRect.setHeight(qMax(taskRect.height(), decoration.height())); // now taskRect tells us where to start line 2 painter->drawPixmap(decorationPoint, decoration); // draw line 2 (timespan and comment, partly): painter->setFont(detailFont); QRect detailsRect(option.rect); detailsRect.setTop(taskRect.bottom()); detailsRect.setHeight(option.rect.height() - taskRect.height()); painter->drawText(detailsRect, Qt::AlignLeft | Qt::AlignTop, timespan, &boundingRect); detailsRect = boundingRect; // draw the duration line: const int Margin = 2; QRect durationRect(option.rect.left() + 1, detailsRect.bottom(), static_cast(logDuration * (option.rect.width() - 2)), Margin); painter->fillRect(durationRect, palette.dark()); painter->restore(); return QRect(0, 0, qMax(taskRect.width(), detailsRect.width()), durationRect.bottom() + 1 - option.rect.top()); } double EventEditorDelegate::logDuration(int duration) const { // we rely on the compiler to optimize at compile time :-) if (duration <= 0) return 0; if (duration <= 3600) { return 0.2 * 1.0 / 3600.0 * duration; } else { const double log2 = std::log(2.0); const double hours = 1.0 / 3600 * duration; const double value = log(hours) / log2; return 0.2 * (1.0 + value); } } Charm-1.12.0/Charm/Widgets/EventEditorDelegate.h000066400000000000000000000040071331066577000213320ustar00rootroot00000000000000/* EventEditorDelegate.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef EVENTEDITORDELEGATE_H #define EVENTEDITORDELEGATE_H #include #include class QPainter; class QStyleOptionViewItem; class QModelIndex; class EventModelFilter; class TaskTreeItem; class Event; class EventEditorDelegate : public QItemDelegate { Q_OBJECT public: enum EventState { EventState_Default, EventState_Locked, EventState_Dirty }; explicit EventEditorDelegate(EventModelFilter *model, QObject *parent = nullptr); QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override; void paint(QPainter *, const QStyleOptionViewItem &, const QModelIndex &) const override; private: EventModelFilter *m_model; mutable QSize m_cachedSizeHint; QString taskName(const TaskTreeItem &item) const; QString dateAndDuration(const Event &event) const; QRect paint(QPainter *, const QStyleOptionViewItem &option, const QString &taskName, const QString ×pan, double logDuration, EventState state) const; // calculate the length for a visual representation of the event duration double logDuration(int seconds) const; }; #endif Charm-1.12.0/Charm/Widgets/EventView.cpp000066400000000000000000000455211331066577000177240ustar00rootroot00000000000000/* EventView.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "EventView.h" #include "ApplicationCore.h" #include "Data.h" #include "EventEditor.h" #include "EventEditorDelegate.h" #include "EventModelFilter.h" #include "FindAndReplaceEventsDialog.h" #include "MessageBox.h" #include "SelectTaskDialog.h" #include "TasksView.h" #include "ViewHelpers.h" #include "WeeklyTimesheet.h" #include "WidgetUtils.h" #include "Commands/CommandDeleteEvent.h" #include "Commands/CommandMakeEvent.h" #include "Commands/CommandModifyEvent.h" #include "Core/CharmConstants.h" #include "Core/CharmDataModel.h" #include "Core/Configuration.h" #include "Core/Event.h" #include "Core/TaskTreeItem.h" #include #include #include #include #include #include #include #include #include EventView::EventView(QWidget *parent) : QDialog(parent) , m_toolBar(new QToolBar(this)) , m_actionUndo(this) , m_actionRedo(this) , m_actionNewEvent(this) , m_actionEditEvent(this) , m_actionDeleteEvent(this) , m_actionCreateTimeSheet(this) , m_actionFindAndReplace(this) , m_comboBox(new QComboBox(this)) , m_labelTotal(new QLabel(this)) , m_listView(new QListView(this)) { setWindowTitle(tr("Event Editor")); auto layout = new QVBoxLayout(this); layout->setMargin(0); layout->setSpacing(0); layout->addWidget(m_toolBar); layout->addWidget(m_listView); m_listView->setAlternatingRowColors(true); m_listView->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_listView, &QListView::customContextMenuRequested, this, &EventView::slotContextMenuRequested); connect(m_listView, &QListView::doubleClicked, this, &EventView::slotEventDoubleClicked); connect(&m_actionNewEvent, &QAction::triggered, this, &EventView::slotNewEvent); connect(&m_actionEditEvent, SIGNAL(triggered()), SLOT(slotEditEvent())); connect(&m_actionDeleteEvent, &QAction::triggered, this, &EventView::slotDeleteEvent); // connect( &m_commitTimer, SIGNAL(timeout()), // SLOT(slotCommitTimeout()) ); // m_commitTimer.setSingleShot( true ); m_actionUndo.setText(tr("Undo")); m_actionUndo.setToolTip(tr("Undo the latest change")); m_actionUndo.setShortcut(QKeySequence::Undo); m_actionUndo.setEnabled(false); m_actionRedo.setText(tr("Redo")); m_actionRedo.setToolTip(tr("Redo the last undone change.")); m_actionRedo.setShortcut(QKeySequence::Redo); m_actionRedo.setEnabled(false); m_undoStack = new QUndoStack(this); connect(m_undoStack, &QUndoStack::canUndoChanged, &m_actionUndo, &QAction::setEnabled); connect(m_undoStack, &QUndoStack::undoTextChanged, this, &EventView::slotUndoTextChanged); connect(&m_actionUndo, &QAction::triggered, m_undoStack, &QUndoStack::undo); connect(m_undoStack, &QUndoStack::canRedoChanged, &m_actionRedo, &QAction::setEnabled); connect(m_undoStack, &QUndoStack::redoTextChanged, this, &EventView::slotRedoTextChanged); connect(&m_actionRedo, &QAction::triggered, m_undoStack, &QUndoStack::redo); m_actionNewEvent.setText(tr("New Event...")); m_actionNewEvent.setToolTip(tr("Create a new Event")); m_actionNewEvent.setIcon(Data::newTaskIcon()); m_actionNewEvent.setShortcut(QKeySequence::New); m_toolBar->addAction(&m_actionNewEvent); m_actionEditEvent.setText(tr("Edit Event...")); m_actionEditEvent.setShortcut(Qt::CTRL + Qt::Key_E); m_actionEditEvent.setIcon(Data::editEventIcon()); m_toolBar->addAction(&m_actionEditEvent); m_actionFindAndReplace.setText(tr("Search/Replace Events...")); m_actionFindAndReplace.setToolTip(tr("Change the task events belong to")); m_actionFindAndReplace.setIcon(Data::searchIcon()); m_toolBar->addAction(&m_actionFindAndReplace); connect(&m_actionFindAndReplace, &QAction::triggered, this, &EventView::slotFindAndReplace); m_actionDeleteEvent.setText(tr("Delete Event...")); QList deleteShortcuts; deleteShortcuts << QKeySequence::Delete; #ifdef Q_OS_OSX deleteShortcuts << Qt::Key_Backspace; #endif m_actionDeleteEvent.setShortcuts(deleteShortcuts); m_actionDeleteEvent.setIcon(Data::deleteTaskIcon()); m_toolBar->addAction(&m_actionDeleteEvent); // disable all actions, action state will be set when the current // item changes: m_actionNewEvent.setEnabled(true); // always on m_actionEditEvent.setEnabled(false); m_actionDeleteEvent.setEnabled(false); m_toolBar->addWidget(m_comboBox); connect(m_comboBox, SIGNAL(currentIndexChanged(int)), SLOT(timeFrameChanged(int))); auto spacer = new QWidget(this); QSizePolicy spacerSizePolicy = spacer->sizePolicy(); spacerSizePolicy.setHorizontalPolicy(QSizePolicy::Expanding); spacer->setSizePolicy(spacerSizePolicy); m_toolBar->addWidget(spacer); m_toolBar->addWidget(m_labelTotal); QTimer::singleShot(0, this, SLOT(delayedInitialization())); // I hate doing this but the stupid default view sizeHints suck badly. setMinimumHeight(200); } EventView::~EventView() { // Prevents a crash on exit, with the stack emitting undoTextChanged on destruction m_undoStack->disconnect(this); } void EventView::delayedInitialization() { timeSpansChanged(); connect(ApplicationCore::instance().dateChangeWatcher(), &DateChangeWatcher::dateChanged, this, &EventView::timeSpansChanged); } void EventView::populateEditMenu(QMenu *menu) { menu->addAction(&m_actionUndo); menu->addAction(&m_actionRedo); menu->addSeparator(); menu->addAction(&m_actionNewEvent); menu->addAction(&m_actionEditEvent); menu->addAction(&m_actionDeleteEvent); } void EventView::timeSpansChanged() { m_timeSpans = TimeSpans(QDate::currentDate()).standardTimeSpans(); // close enough to "ever" for our purposes: NamedTimeSpan allEvents = { tr("Ever"), TimeSpan(QDate::currentDate().addYears(-200), QDate::currentDate().addYears(+200)), Range }; m_timeSpans << allEvents; const int currentIndex = m_comboBox->currentIndex(); m_comboBox->clear(); for (int i = 0; i < m_timeSpans.size(); ++i) m_comboBox->addItem(m_timeSpans[i].name); if (currentIndex >= 0 && currentIndex <= m_timeSpans.size()) { m_comboBox->setCurrentIndex(currentIndex); } else { m_comboBox->setCurrentIndex(0); } } void EventView::commitCommand(CharmCommand *command) { command->finalize(); } void EventView::slotCurrentItemChanged(const QModelIndex &start, const QModelIndex &) { if (!start.isValid()) { m_event = Event(); m_actionDeleteEvent.setEnabled(false); m_actionEditEvent.setEnabled(false); } else { m_actionDeleteEvent.setEnabled(true); m_actionEditEvent.setEnabled(true); Event event = m_model->eventForIndex(start); Q_ASSERT(event.isValid()); // index is valid, so... setCurrentEvent(event); } slotConfigureUi(); } void EventView::setCurrentEvent(const Event &event) { m_event = event; } void EventView::stageCommand(CharmCommand *command) { auto undoCommand = new UndoCharmCommandWrapper(command); connect(command, &CharmCommand::emitExecute, this, &EventView::emitCommand); connect(command, &CharmCommand::emitRollback, this, &EventView::emitCommandRollback); connect(command, &CharmCommand::emitSlotEventIdChanged, this, &EventView::slotEventIdChanged); m_undoStack->push(undoCommand); } void EventView::slotNewEvent() { SelectTaskDialog dialog(this); if (dialog.exec()) { const TaskTreeItem &item = MODEL.charmDataModel()->taskTreeItem(dialog.selectedTask()); if (item.task().isValid()) { Event e; e.setTaskId(dialog.selectedTask()); slotEditEvent(e); } } } void EventView::slotDeleteEvent() { const TaskTreeItem &taskTreeItem = MODEL.charmDataModel()->taskTreeItem(m_event.taskId()); const QString name = MODEL.charmDataModel()->fullTaskName(taskTreeItem.task()); const QDate startDate = m_event.startDateTime().date(); const QTime startTime = m_event.startDateTime().time(); const QDate endDate = m_event.endDateTime().date(); const QTime endTime = m_event.endDateTime().time(); const bool sameDates = (startDate == endDate); QString message; if (sameDates) { message = tr("%1%2: %3 - %4 (Duration: %5)") .arg(name, QLocale::system().toString(startDate, QLocale::ShortFormat), QLocale::system().toString(startTime, QLocale::ShortFormat), QLocale::system().toString(endTime, QLocale::ShortFormat), hoursAndMinutes(m_event.duration())); } else { message = tr("%1" "") .arg(name, QLocale::system().toString(startDate, QLocale::ShortFormat), QLocale::system().toString(startTime, QLocale::ShortFormat), QLocale::system().toString(endDate, QLocale::ShortFormat), QLocale::system().toString(endTime, QLocale::ShortFormat), hoursAndMinutes(m_event.duration())); } if (MessageBox::question(this, tr("Delete Event?"), message, tr("Delete"), tr("Cancel")) == QMessageBox::Yes) { auto command = new CommandDeleteEvent(m_event, this); command->prepare(); stageCommand(command); } } void EventView::slotPreviousEvent() { const QModelIndex &index = m_model->indexForEvent(m_event); Q_ASSERT(index.isValid() && index.row() > 0 && index.row() < m_model->rowCount()); const QModelIndex &previousIndex = m_model->index(index.row() - 1, 0, QModelIndex()); m_listView->selectionModel()->setCurrentIndex (previousIndex, QItemSelectionModel::ClearAndSelect); } void EventView::slotNextEvent() { const QModelIndex &index = m_model->indexForEvent(m_event); Q_ASSERT(index.isValid() && index.row() >= 0 && index.row() < m_model->rowCount() - 1); const QModelIndex &nextIndex = m_model->index(index.row() + 1, 0, QModelIndex()); m_listView->selectionModel()->setCurrentIndex (nextIndex, QItemSelectionModel::ClearAndSelect); } void EventView::slotContextMenuRequested(const QPoint &point) { // prepare the menu: QMenu menu(m_listView); menu.addAction(&m_actionNewEvent); menu.addAction(&m_actionEditEvent); menu.addAction(&m_actionDeleteEvent); // all actions are handled in their own slots: menu.exec(m_listView->mapToGlobal(point)); } void EventView::makeVisibleAndCurrent(const Event &event) { // make sure the event filter time span includes the events start // time (otherwise it is not visible): // (how?: if the event is not in the timespan, expand the timespan // as much as needed) const int CurrentTimeSpan = m_comboBox->currentIndex(); if (!m_timeSpans[CurrentTimeSpan].contains(event.startDateTime().date())) { for (int i = 0; i < m_timeSpans.size(); ++i) { // at least "ever" should contain it if (m_timeSpans[i].contains(event.startDateTime().date())) { m_comboBox->setCurrentIndex(i); break; } } } // get an index for the event, and make it the current index: const QModelIndex &index = m_model->indexForEvent(event); Q_ASSERT(index.isValid()); m_listView->selectionModel()->setCurrentIndex (index, QItemSelectionModel::ClearAndSelect); } void EventView::timeFrameChanged(int index) { // wait for the next update, in this case: if (m_comboBox->count() == 0) return; if (!m_model) return; if (index >= 0 && index < m_timeSpans.size()) { m_model->setFilterStartDate(m_timeSpans[index].timespan.first); m_model->setFilterEndDate(m_timeSpans[index].timespan.second); } else { Q_ASSERT(false); } } void EventView::slotEventActivated(EventId) { slotConfigureUi(); } void EventView::slotEventDeactivated(EventId) { slotConfigureUi(); } void EventView::slotConfigureUi() { // what a fricking hack - but QDateTimeEdit does not seem to have // a simple function to toggle 12h and 24h mode: static QString OriginalDateTimeFormat; if (OriginalDateTimeFormat.isEmpty()) { QDateTimeEdit edit(this); OriginalDateTimeFormat = edit.displayFormat(); } // yeah, I know, this will survive changes in the user prefs bool active = MODEL.charmDataModel()->isEventActive(m_event.id()); m_actionNewEvent.setEnabled(true); // always on m_actionEditEvent.setEnabled(m_event.isValid()); m_actionDeleteEvent.setEnabled(m_event.isValid() && !active); // m_ui->frame->setEnabled( ! active ); } void EventView::slotUpdateCurrent() { Event event = DATAMODEL->eventForId(m_event.id()); if (event != m_event) setCurrentEvent(event); slotUpdateTotal(); } void EventView::slotUndoTextChanged(const QString &text) { m_actionUndo.setText(tr("Undo %1").arg(text)); } void EventView::slotRedoTextChanged(const QString &text) { m_actionRedo.setText(tr("Redo %1").arg(text)); } void EventView::slotEventIdChanged(int oldId, int newId) { foreach (QObject *o, m_undoStack->children()) { UndoCharmCommandWrapper *wrapper = dynamic_cast(o); Q_ASSERT(wrapper); wrapper->command()->eventIdChanged(oldId, newId); } } void EventView::slotUpdateTotal() { // what matching signal does the proxy emit? int seconds = m_model->totalDuration(); if (seconds == 0) { m_labelTotal->clear(); } else { QString total; QTextStream stream(&total); stream << "(" << hoursAndMinutes(seconds) << " total)"; m_labelTotal->setText(total); } } void EventView::slotFindAndReplace() { FindAndReplaceEventsDialog findAndReplace(this); if (findAndReplace.exec() != QDialog::Accepted) return; QList events = findAndReplace.modifiedEvents(); for (int i = 0; i < events.count(); ++i) slotEventChangesCompleted(events[i]); } void EventView::slotReset() { timeFrameChanged(m_comboBox->currentIndex()); } void EventView::stateChanged(State) { switch (ApplicationCore::instance().state()) { case Connecting: setModel(&MODEL); connect(MODEL.charmDataModel(), &CharmDataModel::resetGUIState, this, &EventView::restoreGuiState); break; case Connected: //the model is populated when entering Connected, so delay state restore QMetaObject::invokeMethod(this, "restoreGuiState", Qt::QueuedConnection); configurationChanged(); break; case Disconnecting: saveGuiState(); break; default: break; } } void EventView::configurationChanged() { WidgetUtils::updateToolButtonStyle(this); slotConfigureUi(); } void EventView::saveGuiState() { WidgetUtils::saveGeometry(this, MetaKey_EventEditorGeometry); } void EventView::restoreGuiState() { WidgetUtils::restoreGeometry(this, MetaKey_EventEditorGeometry); } void EventView::setModel(ModelConnector *connector) { EventModelFilter *model = connector->eventModel(); m_listView->setModel(model); m_listView->setSelectionBehavior(QAbstractItemView::SelectRows); m_listView->setSelectionMode(QAbstractItemView::SingleSelection); connect(m_listView->selectionModel(),&QItemSelectionModel::currentChanged, this, &EventView::slotCurrentItemChanged); connect(model, &EventModelFilter::eventActivationNotice, this, &EventView::slotEventActivated); connect(model, &EventModelFilter::eventDeactivationNotice, this, &EventView::slotEventDeactivated); connect(model, &EventModelFilter::dataChanged, this, &EventView::slotUpdateCurrent); connect(model, &EventModelFilter::rowsInserted, this, &EventView::slotUpdateTotal); connect(model, &EventModelFilter::rowsRemoved, this, &EventView::slotUpdateTotal); connect(model, &EventModelFilter::rowsInserted, this, &EventView::slotConfigureUi); connect(model, &EventModelFilter::rowsRemoved, this, &EventView::slotConfigureUi); connect(model, &EventModelFilter::layoutChanged, this, &EventView::slotUpdateCurrent); connect(model, &EventModelFilter::modelReset, this, &EventView::slotUpdateTotal); m_model = model; // normally, the model is set only once, so this should be no problem: auto delegate = new EventEditorDelegate(model, m_listView); m_listView->setItemDelegate(delegate); timeSpansChanged(); } void EventView::slotEventDoubleClicked(const QModelIndex &index) { Q_ASSERT(m_model); // otherwise, how can we get a doubleclick on an item? const Event &event = m_model->eventForIndex(index); slotEditEvent(event); } void EventView::slotEditEvent() { slotEditEvent(m_event); } void EventView::slotEditEvent(const Event &event) { EventEditor editor(event, this); if (editor.exec()) { Event newEvent = editor.eventResult(); if (!newEvent.isValid()) { auto command = new CommandMakeEvent(newEvent, this); connect(command, &CommandMakeEvent::finishedOk, this, &EventView::slotEventChangesCompleted, Qt::QueuedConnection); stageCommand(command); return; } else { auto command = new CommandModifyEvent(newEvent, event, this); stageCommand(command); } } } void EventView::slotEventChangesCompleted(const Event &event) { // make event editor finished, bypass the undo stack to set its contents // undo will just target CommandMakeEvent instead auto command = new CommandModifyEvent(event, event, this); emitCommand(command); delete command; } Charm-1.12.0/Charm/Widgets/EventView.h000066400000000000000000000064751331066577000173760ustar00rootroot00000000000000/* EventView.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef EVENTVIEW_H #define EVENTVIEW_H #include #include #include #include #include "Core/UIStateInterface.h" #include "Core/Event.h" #include "Core/TimeSpans.h" #include "Core/CommandEmitterInterface.h" #include "Charm/ModelConnector.h" #include "Charm/UndoCharmCommandWrapper.h" class QModelIndex; class CharmCommand; class EventModelFilter; class QToolBar; class QComboBox; class QLabel; class QListView; class EventView : public QDialog, public UIStateInterface { Q_OBJECT public: explicit EventView(QWidget *parent = nullptr); ~EventView() override; void makeVisibleAndCurrent(const Event &); void setModel(ModelConnector *); void populateEditMenu(QMenu *); Q_SIGNALS: void emitCommand(CharmCommand *) override; void emitCommandRollback(CharmCommand *) override; public Q_SLOTS: void commitCommand(CharmCommand *) override; void delayedInitialization(); void timeSpansChanged(); void timeFrameChanged(int); void slotConfigureUi(); void saveGuiState() override; void restoreGuiState() override; void stateChanged(State previous) override; void configurationChanged() override; private Q_SLOTS: void slotEventDoubleClicked(const QModelIndex &); void slotEditEvent(); void slotEditEvent(const Event &); void slotEventChangesCompleted(const Event &); void slotCurrentItemChanged(const QModelIndex &, const QModelIndex &); void slotContextMenuRequested(const QPoint &); void slotNextEvent(); void slotPreviousEvent(); void slotNewEvent(); void slotDeleteEvent(); void slotEventActivated(EventId); void slotEventDeactivated(EventId); void slotUpdateTotal(); void slotUpdateCurrent(); void slotUndoTextChanged(const QString &); void slotRedoTextChanged(const QString &); void slotEventIdChanged(int oldId, int newId); void slotFindAndReplace(); void slotReset(); private: void setCurrentEvent(const Event &); void stageCommand(CharmCommand *); QToolBar *m_toolBar; QUndoStack *m_undoStack; QList m_timeSpans; Event m_event; EventModelFilter *m_model = nullptr; QAction m_actionUndo; QAction m_actionRedo; QAction m_actionNewEvent; QAction m_actionEditEvent; QAction m_actionDeleteEvent; QAction m_actionCreateTimeSheet; QAction m_actionFindAndReplace; QComboBox *m_comboBox; QLabel *m_labelTotal; QListView *m_listView; }; #endif Charm-1.12.0/Charm/Widgets/ExpandStatesHelper.cpp000066400000000000000000000055011331066577000215450ustar00rootroot00000000000000/* ExpandStatesHelper.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ExpandStatesHelper.h" #include "TaskModelAdapter.h" #include static void saveChildExpandStates(const QModelIndex &idx, QTreeView *tv, QHash *map) { const int rc = idx.model()->rowCount(idx); for (int i = 0; i < rc; ++i) { const QModelIndex c = idx.child(i, 0); if (c.model()->rowCount(c) > 0) { const TaskId id = c.data(TasksViewRole_TaskId).toInt(); const bool expanded = tv->isExpanded(c); map->insert(id, expanded); if (expanded) saveChildExpandStates(c, tv, map); } } } static void restoreChildExpandStates(const QModelIndex &idx, QTreeView *tv, QHash *map) { const int rc = idx.model()->rowCount(idx); for (int i = 0; i < rc; ++i) { const QModelIndex c = idx.child(i, 0); if (c.model()->rowCount(c) > 0) { const TaskId id = c.data(TasksViewRole_TaskId).toInt(); const bool expanded = map->value(id); tv->setExpanded(c, expanded); restoreChildExpandStates(c, tv, map); } } } void Charm::saveExpandStates(QTreeView *tv, QHash *map) { if (!tv->model()) return; if (tv->model()->rowCount() == 0) return; const QModelIndex root = tv->model()->index(0, 0, QModelIndex()); const TaskId id = root.data(TasksViewRole_TaskId).toInt(); const bool expanded = tv->isExpanded(root); map->insert(id, expanded); saveChildExpandStates(root, tv, map); } void Charm::restoreExpandStates(QTreeView *tv, QHash *map) { if (!tv->model()) return; if (tv->model()->rowCount() == 0) return; const QModelIndex root = tv->model()->index(0, 0, QModelIndex()); const TaskId id = root.data(TasksViewRole_TaskId).toInt(); const bool expand = map->value(id); tv->setExpanded(root, expand); restoreChildExpandStates(root, tv, map); } Charm-1.12.0/Charm/Widgets/ExpandStatesHelper.h000066400000000000000000000023241331066577000212120ustar00rootroot00000000000000/* ExpandStatesHelper.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef EXPANDSTATESHELPER_H #define EXPANDSTATESHELPER_H #include "Core/Task.h" #include class QTreeView; namespace Charm { //helper functions for saving and restoring expansion states when filtering void saveExpandStates(QTreeView *tv, QHash *map); void restoreExpandStates(QTreeView *tv, QHash *map); } #endif Charm-1.12.0/Charm/Widgets/FindAndReplaceEventsDialog.cpp000066400000000000000000000126251331066577000231130ustar00rootroot00000000000000/* FindAndReplaceEventsDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Michel Boyer de la Giroday This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "FindAndReplaceEventsDialog.h" #include "ui_FindAndReplaceEventsDialog.h" #include "ApplicationCore.h" #include "EventEditorDelegate.h" #include "SelectTaskDialog.h" #include "ViewHelpers.h" #include "Commands/CommandModifyEvent.h" #include #include FindAndReplaceEventsDialog::FindAndReplaceEventsDialog(QWidget *parent) : QDialog(parent) , m_ui(new Ui::FindAndReplaceEventsDialog) { m_ui->setupUi(this); m_replace = new QPushButton(tr("Replace")); m_replace->setEnabled(false); connect(m_replace, &QPushButton::clicked, this, &FindAndReplaceEventsDialog::slotReplaceProjectCode); m_cancel = new QPushButton(tr("Cancel")); connect(m_cancel, &QPushButton::clicked, this, &QDialog::reject); m_ui->buttonBox->addButton(m_cancel, QDialogButtonBox::RejectRole); m_ui->buttonBox->addButton(m_replace, QDialogButtonBox::AcceptRole); m_ui->dateEditEnd->calendarWidget()->setFirstDayOfWeek(Qt::Monday); m_ui->dateEditEnd->calendarWidget()->setVerticalHeaderFormat(QCalendarWidget::ISOWeekNumbers); m_ui->dateEditStart->calendarWidget()->setFirstDayOfWeek(Qt::Monday); m_ui->dateEditStart->calendarWidget()->setVerticalHeaderFormat(QCalendarWidget::ISOWeekNumbers); const TimeSpans timeSpans; m_timeSpan = timeSpans.thisWeek().timespan; m_ui->dateEditStart->setDate(m_timeSpan.first); // less 1 day as the timespan logic in charm excludes events on the end date // and we add a day when filtering. m_ui->dateEditEnd->setDate(m_timeSpan.second.addDays(-1)); connect(m_ui->dateEditStart->calendarWidget(), &QCalendarWidget::selectionChanged, this, &FindAndReplaceEventsDialog::slotTimeSpansChanged); connect(m_ui->dateEditEnd->calendarWidget(), &QCalendarWidget::selectionChanged, this, &FindAndReplaceEventsDialog::slotTimeSpansChanged); connect(ApplicationCore::instance().dateChangeWatcher(), &DateChangeWatcher::dateChanged, this, &FindAndReplaceEventsDialog::slotTimeSpansChanged); connect(m_ui->selectSearchTaskPB, &QPushButton::clicked, this, &FindAndReplaceEventsDialog::slotSelectTaskToSearch); connect(m_ui->selectReplaceWithTaskPB, &QPushButton::clicked, this, &FindAndReplaceEventsDialog::slotSelectTaskToReplaceWith); m_model.reset(new EventModelFilter(DATAMODEL)); m_ui->findAndReplaceLV->setModel(m_model.data()); m_model->setFilterStartDate(m_timeSpan.first); m_model->setFilterEndDate(m_timeSpan.second); auto delegate = new EventEditorDelegate(m_model.data(), m_ui->findAndReplaceLV); m_ui->findAndReplaceLV->setItemDelegate(delegate); } FindAndReplaceEventsDialog::~FindAndReplaceEventsDialog() { } void FindAndReplaceEventsDialog::searchProjectCode() { m_foundEvents = m_model->events(); } void FindAndReplaceEventsDialog::slotReplaceProjectCode() { const QList events = m_model->events(); for (int i = 0; i < events.count(); ++i) { if (m_foundEvents.contains(events[i])) { Event event = events[i]; event.setTaskId(m_taskToReplaceWith); m_modifiedEvents << event; } } accept(); } QList FindAndReplaceEventsDialog::modifiedEvents() const { return m_modifiedEvents; } void FindAndReplaceEventsDialog::slotTimeSpansChanged() { m_timeSpan.first = m_ui->dateEditStart->date(); m_timeSpan.second = m_ui->dateEditEnd->date(); m_model->setFilterStartDate(m_timeSpan.first); // add a day as the timespan logic in charm excludes events on the end date. m_model->setFilterEndDate(m_timeSpan.second.addDays(1)); if (m_taskToSearch > 0) searchProjectCode(); } void FindAndReplaceEventsDialog::slotSelectTaskToSearch() { selectTask(TaskToSearch); } void FindAndReplaceEventsDialog::slotSelectTaskToReplaceWith() { selectTask(TaskToReplaceWith); } void FindAndReplaceEventsDialog::selectTask(SelectTaskType type) { SelectTaskDialog dialog(this); if (type == TaskToSearch) dialog.setNonValidSelectable(); if (!dialog.exec()) return; const int taskId = dialog.selectedTask(); if (type == TaskToSearch) { m_taskToSearch = taskId; m_ui->findTaskLB->setText(DATAMODEL->taskIdAndSmartNameString(taskId)); m_model->setFilterTaskId(taskId); } else { m_taskToReplaceWith = taskId; m_ui->replaceWithTaskLB->setText(DATAMODEL->taskIdAndSmartNameString(taskId)); } m_replace->setEnabled(m_taskToReplaceWith > 0 && m_taskToSearch > 0); searchProjectCode(); } Charm-1.12.0/Charm/Widgets/FindAndReplaceEventsDialog.h000066400000000000000000000041411331066577000225520ustar00rootroot00000000000000/* FindAndReplaceEventsDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Michel Boyer de la Giroday This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef FINDANDREPLACEEVENTSDIALOG_H #define FINDANDREPLACEEVENTSDIALOG_H #include #include #include "Core/Task.h" #include class Event; class EventModelFilter; class Task; namespace Ui { class FindAndReplaceEventsDialog; } class FindAndReplaceEventsDialog : public QDialog { Q_OBJECT public: explicit FindAndReplaceEventsDialog(QWidget *parent = nullptr); ~FindAndReplaceEventsDialog() override; QList modifiedEvents() const; private Q_SLOTS: void slotSelectTaskToSearch(); void slotSelectTaskToReplaceWith(); void slotTimeSpansChanged(); void slotReplaceProjectCode(); private: enum SelectTaskType { TaskToSearch, TaskToReplaceWith }; void searchProjectCode(); void selectTask(SelectTaskType type); void eventChangesCompleted(const Event &event); TaskId m_taskToSearch = {}; TaskId m_taskToReplaceWith = {}; TimeSpan m_timeSpan; QPushButton *m_replace; QPushButton *m_cancel; QList m_foundEvents; QList m_modifiedEvents; QScopedPointer m_model; QScopedPointer m_ui; }; #endif // FINDANDREPLACEEVENTSDIALOG_H Charm-1.12.0/Charm/Widgets/FindAndReplaceEventsDialog.ui000066400000000000000000000125061331066577000227440ustar00rootroot00000000000000 FindAndReplaceEventsDialog 0 0 332 396 Find Event true Date selection false false (events that start at or after...) Start date Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter true (events that start before...) End date Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter true Find and Replace Find: Replace with: Select Task Select Task Qt::Horizontal 40 20 true QDialogButtonBox::NoButton Charm-1.12.0/Charm/Widgets/HttpJobProgressDialog.cpp000066400000000000000000000044001331066577000222160ustar00rootroot00000000000000/* HttpJobProgressDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "HttpJobProgressDialog.h" #include #include HttpJobProgressDialog::HttpJobProgressDialog(HttpJob *job, QWidget *parent) : QProgressDialog(parent) , m_job(job) { setLabelText(tr("Wait...")); Q_ASSERT(job); connect(job, &HttpJob::finished, this, &HttpJobProgressDialog::jobFinished); connect(job, &HttpJob::transferStarted, this, &HttpJobProgressDialog::jobTransferStarted); connect(job, &HttpJob::passwordRequested, this, &HttpJobProgressDialog::jobPasswordRequested); } void HttpJobProgressDialog::jobTransferStarted() { show(); } void HttpJobProgressDialog::jobFinished(HttpJob *) { deleteLater(); } void HttpJobProgressDialog::jobPasswordRequested(HttpJob::PasswordRequestReason reason) { bool ok; QPointer that(this); //guard against destruction while dialog is open const auto title = reason == HttpJob::PasswordIncorrect ? tr("Authentication Failed") : tr("Password"); const auto message = reason == HttpJob::PasswordIncorrect ? tr("Please re-enter your lotsofcake password") : tr("Please enter your lotsofcake password"); const auto newpass = QInputDialog::getText(parentWidget(), title, message, QLineEdit::Password, m_job->password(), &ok); if (!that) return; if (ok) { m_job->provideRequestedPassword(newpass); } else { m_job->passwordRequestCanceled(); } } Charm-1.12.0/Charm/Widgets/HttpJobProgressDialog.h000066400000000000000000000025511331066577000216700ustar00rootroot00000000000000/* HttpJobProgressDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef HTTPJOBPROGRESSDIALOG_H #define HTTPJOBPROGRESSDIALOG_H #include #include #include "HttpClient/HttpJob.h" class HttpJobProgressDialog : public QProgressDialog { Q_OBJECT public: explicit HttpJobProgressDialog(HttpJob *job, QWidget *parent = nullptr); private Q_SLOTS: void jobFinished(HttpJob *); void jobTransferStarted(); void jobPasswordRequested(HttpJob::PasswordRequestReason reason); private: QPointer m_job; }; #endif Charm-1.12.0/Charm/Widgets/IdleCorrectionDialog.cpp000066400000000000000000000042561331066577000220350ustar00rootroot00000000000000/* IdleCorrectionDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "IdleCorrectionDialog.h" #include "ui_IdleCorrectionDialog.h" #include #include IdleCorrectionDialog::IdleCorrectionDialog(const IdleDetector::IdlePeriod &idlePeriod, QWidget *parent) : QDialog(parent) , m_ui(new Ui::IdleCorrectionDialog) , m_start(idlePeriod.first) { m_ui->setupUi(this); updateDuration(); auto timer = new QTimer(this); timer->setInterval(60000); connect(timer, &QTimer::timeout, this, &IdleCorrectionDialog::updateDuration); timer->start(); } IdleCorrectionDialog::~IdleCorrectionDialog() { } IdleCorrectionDialog::Result IdleCorrectionDialog::result() const { if (m_ui->ignore->isChecked()) { return Idle_Ignore; } else if (m_ui->endEvent->isChecked()) { return Idle_EndEvent; } else if (m_ui->restartEvent->isChecked()) { return Idle_RestartEvent; } else { Q_ASSERT(false); // unhandled whatever? } return Idle_NoResult; } void IdleCorrectionDialog::updateDuration() { const auto secs = m_start.secsTo(QDateTime::currentDateTime()); m_ui->idleLabel->setText( tr( "Charm detected that the computer became idle for %1 hours, while an event was in progress.") .arg(hoursAndMinutes(secs))); } Charm-1.12.0/Charm/Widgets/IdleCorrectionDialog.h000066400000000000000000000030631331066577000214750ustar00rootroot00000000000000/* IdleCorrectionDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef IDLECORRECTIONDIALOG_H #define IDLECORRECTIONDIALOG_H #include "Idle/IdleDetector.h" #include #include namespace Ui { class IdleCorrectionDialog; } class IdleCorrectionDialog : public QDialog { Q_OBJECT public: enum Result { Idle_NoResult, Idle_Ignore, Idle_EndEvent, Idle_RestartEvent }; explicit IdleCorrectionDialog(const IdleDetector::IdlePeriod &idlePeriod, QWidget *parent = nullptr); ~IdleCorrectionDialog() override; Result result() const; private: void updateDuration(); QScopedPointer m_ui; QDateTime m_start; }; #endif Charm-1.12.0/Charm/Widgets/IdleCorrectionDialog.ui000066400000000000000000000052621331066577000216660ustar00rootroot00000000000000 IdleCorrectionDialog 0 0 378 168 Spacing out in front of the computer? Charm detected that the computer became idle while an event was in progress. true Ignore idle detection this time, continue event. true End event(s) at the time the computer went idle End event(s) at idle time and restart it from now on Qt::Vertical 20 14 Qt::Horizontal QDialogButtonBox::Ok buttonBox accepted() IdleCorrectionDialog accept() 248 254 157 274 buttonBox rejected() IdleCorrectionDialog reject() 316 260 286 274 Charm-1.12.0/Charm/Widgets/MakeTemporarilyVisible.h000066400000000000000000000025631331066577000220770ustar00rootroot00000000000000/* MakeTemporarilyVisible.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MAKETEMPORARILYVISIBLE_H #define MAKETEMPORARILYVISIBLE_H class MakeTemporarilyVisible { public: explicit MakeTemporarilyVisible(QWidget *widget) : m_widget(widget) { Q_ASSERT(m_widget); m_wasVisible = m_widget->isVisible(); if (!m_wasVisible) m_widget->show(); } ~MakeTemporarilyVisible() { if (!m_wasVisible) m_widget->hide(); } private: QWidget *m_widget = {}; bool m_wasVisible = false; }; #endif Charm-1.12.0/Charm/Widgets/MessageBox.cpp000066400000000000000000000047421331066577000200450ustar00rootroot00000000000000/* MessageBox.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "MessageBox.h" #include #include static int messageBox(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButton yesButton, const QString &yesLabel, QMessageBox::StandardButton noButton, const QString &noLabel, QMessageBox::StandardButton defaultButton, QMessageBox::Icon icon) { QPointer messageBox(new QMessageBox(parent)); messageBox->setWindowTitle(title); messageBox->setIcon(icon); messageBox->setText(text); messageBox->setStandardButtons(yesButton|noButton); messageBox->button(yesButton)->setText(yesLabel); messageBox->button(noButton)->setText(noLabel); messageBox->setDefaultButton(defaultButton); const int result = messageBox->exec(); delete messageBox; return result; } int MessageBox::question(QWidget *parent, const QString &title, const QString &text, const QString &yesLabel, const QString &noLabel, QMessageBox::StandardButton defaultButton) { return messageBox(parent, title, text, QMessageBox::Yes, yesLabel, QMessageBox::No, noLabel, defaultButton, QMessageBox::Question); } int MessageBox::warning(QWidget *parent, const QString &title, const QString &text, const QString &yesLabel, const QString &noLabel, QMessageBox::StandardButton defaultButton) { return messageBox(parent, title, text, QMessageBox::Yes, yesLabel, QMessageBox::No, noLabel, defaultButton, QMessageBox::Warning); } Charm-1.12.0/Charm/Widgets/MessageBox.h000066400000000000000000000026071331066577000175100ustar00rootroot00000000000000/* MessageBox.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_MESSAGEBOX_H #define CHARM_MESSAGEBOX_H #include namespace MessageBox { int question(QWidget *parent, const QString &title, const QString &text, const QString &yesLabel, const QString &noLabel, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); int warning(QWidget *parent, const QString &title, const QString &text, const QString &yesLabel, const QString &noLabel, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); } #endif Charm-1.12.0/Charm/Widgets/MonthlyTimesheet.cpp000066400000000000000000000315001331066577000213020ustar00rootroot00000000000000/* MonthlyTimesheet.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "MonthlyTimesheet.h" #include "Reports/MonthlyTimesheetXmlWriter.h" #include #include #include #include #include #include #include "ViewHelpers.h" #include "CharmCMake.h" namespace { typedef QHash > WeeksByYear; static float SecondsInDay = 60. * 60. * 8. /* eight hour work day */; } MonthlyTimeSheetReport::MonthlyTimeSheetReport(QWidget *parent) : TimeSheetReport(parent) { QSettings settings; settings.beginGroup(QStringLiteral("users")); m_weeklyhours = settings.value(QStringLiteral("weeklyhours")).toString().trimmed(); settings.endGroup(); m_dailyhours = m_weeklyhours.toInt() / 5; if (m_dailyhours > 0 && m_dailyhours <= 8) { SecondsInDay = 60. * 60. * m_dailyhours; } else { m_dailyhours = 8; } connect(this, &MonthlyTimeSheetReport::anchorClicked, this, &MonthlyTimeSheetReport::slotLinkClicked); } MonthlyTimeSheetReport::~MonthlyTimeSheetReport() { } void MonthlyTimeSheetReport::setReportProperties( const QDate &start, const QDate &end, TaskId rootTask, bool activeTasksOnly) { m_numberOfWeeks = Charm::weekDifference(start, end.addDays(-1)) + 1; m_monthNumber = start.month(); m_yearOfMonth = start.year(); TimeSheetReport::setReportProperties(start, end, rootTask, activeTasksOnly); } QString MonthlyTimeSheetReport::suggestedFileName() const { return tr("MonthlyTimeSheet-%1-%2").arg(m_yearOfMonth).arg(m_monthNumber, 2, 10, QLatin1Char( '0')); } QByteArray MonthlyTimeSheetReport::saveToText() { QByteArray output; QTextStream stream(&output); QString content = tr("Report for %1, %2 %3 (%4 to %5)") .arg(CONFIGURATION.user.name(), QDate::longMonthName(m_monthNumber), QString::number(startDate().year()), startDate().toString(Qt::TextDate), endDate().addDays(-1).toString(Qt::TextDate)); stream << content << '\n'; stream << '\n'; TimeSheetInfoList timeSheetInfo = TimeSheetInfo::filteredTaskWithSubTasks( TimeSheetInfo::taskWithSubTasks(DATAMODEL, m_numberOfWeeks, rootTask(), secondsMap()), activeTasksOnly()); TimeSheetInfo totalsLine(m_numberOfWeeks); if (!timeSheetInfo.isEmpty()) { totalsLine = timeSheetInfo.first(); if (rootTask() == 0) timeSheetInfo.removeAt(0); // there is always one, because there is always the root item } for (int i = 0; i < timeSheetInfo.size(); ++i) stream << timeSheetInfo[i].formattedTaskIdAndName(CONFIGURATION.taskPaddingLength) << "\t" << hoursAndMinutes(timeSheetInfo[i].total()) << '\n'; stream << '\n'; stream << "Month total: " << hoursAndMinutes(totalsLine.total()) << '\n'; stream.flush(); return output; } QByteArray MonthlyTimeSheetReport::saveToXml(SaveToXmlMode mode) { try { MonthlyTimesheetXmlWriter timesheet; timesheet.setDataModel(DATAMODEL); timesheet.setMonthNumber(m_monthNumber); timesheet.setYearOfMonth(m_yearOfMonth); timesheet.setNumberOfWeeks(m_numberOfWeeks); timesheet.setRootTask(rootTask()); timesheet.setIncludeTaskList(mode == IncludeTaskList); const EventIdList matchingEventIds = DATAMODEL->eventsThatStartInTimeFrame( startDate(), endDate()); EventList events; events.reserve(matchingEventIds.size()); Q_FOREACH (const EventId &eventId, matchingEventIds) events.append(DATAMODEL->eventForId(eventId)); timesheet.setEvents(events); return timesheet.saveToXml(); } catch (const XmlSerializationException &e) { QMessageBox::critical(this, tr("Error exporting the report"), e.what()); } return QByteArray(); } static QDomElement addTblHdr(QDomElement &toRow, const QString &text) { QDomElement header = toRow.ownerDocument().createElement(QStringLiteral("th")); QDomText textNode = toRow.ownerDocument().createTextNode(text); header.appendChild(textNode); toRow.appendChild(header); return header; } static QDomElement addTblCell(QDomElement &toRow, const QString &text) { QDomElement cell = toRow.ownerDocument().createElement(QStringLiteral("td")); cell.setAttribute(QStringLiteral("align"), QStringLiteral("center")); QDomText textNode = toRow.ownerDocument().createTextNode(text); cell.appendChild(textNode); toRow.appendChild(cell); return cell; } void MonthlyTimeSheetReport::update() { // this creates the time sheet // retrieve matching events: const EventIdList matchingEvents = DATAMODEL->eventsThatStartInTimeFrame(startDate(), endDate()); m_secondsMap.clear(); // for every task, make a vector that includes a number of seconds // for every week of a month ( int seconds[m_numberOfWeeks]), and store those in // a map by their task id Q_FOREACH (EventId id, matchingEvents) { const Event &event = DATAMODEL->eventForId(id); QVector seconds(m_numberOfWeeks); if (m_secondsMap.contains(event.taskId())) seconds = m_secondsMap.value(event.taskId()); // what week of the month is the event (normalized to vector indexes): const int weekOfMonth = Charm::weekDifference(startDate(), event.startDateTime().date()); seconds[weekOfMonth] += event.duration(); // store in minute map: m_secondsMap[event.taskId()] = seconds; } // now the reporting: // headline first: QTextDocument report; QDomDocument doc = createReportTemplate(); QDomElement root = doc.documentElement(); QDomElement body = root.firstChildElement(QStringLiteral("body")); // QTextCursor cursor( m_report ); // create the caption: { QDomElement headline = doc.createElement(QStringLiteral("h1")); QDomText text = doc.createTextNode(tr("Monthly Time Sheet")); headline.appendChild(text); body.appendChild(headline); } { QDomElement headline = doc.createElement(QStringLiteral("h3")); QString content = tr("Report for %1, %2 %3 (%4 to %5)") .arg(CONFIGURATION.user.name(), QDate::longMonthName(m_monthNumber), QString::number(startDate().year()), startDate().toString(Qt::TextDate), endDate().addDays(-1).toString(Qt::TextDate)); QDomText text = doc.createTextNode(content); headline.appendChild(text); body.appendChild(headline); QDomElement previousLink = doc.createElement(QStringLiteral("a")); previousLink.setAttribute(QStringLiteral("href"), QStringLiteral("Previous")); QDomText previousLinkText = doc.createTextNode(tr("")); previousLink.appendChild(previousLinkText); body.appendChild(previousLink); QDomElement nextLink = doc.createElement(QStringLiteral("a")); nextLink.setAttribute(QStringLiteral("href"), QStringLiteral("Next")); QDomText nextLinkText = doc.createTextNode(tr("")); nextLink.appendChild(nextLinkText); body.appendChild(nextLink); QDomElement paragraph = doc.createElement(QStringLiteral("br")); body.appendChild(paragraph); } { // now for a table // retrieve the information for the report: // TimeSheetInfoList timeSheetInfo = taskWithSubTasks( m_rootTask, m_secondsMap ); TimeSheetInfoList timeSheetInfo = TimeSheetInfo::filteredTaskWithSubTasks( TimeSheetInfo::taskWithSubTasks(DATAMODEL, m_numberOfWeeks, rootTask(), secondsMap()), activeTasksOnly()); QDomElement table = doc.createElement(QStringLiteral("table")); table.setAttribute(QStringLiteral("width"), QStringLiteral("100%")); table.setAttribute(QStringLiteral("align"), QStringLiteral("left")); table.setAttribute(QStringLiteral("cellpadding"), QStringLiteral("3")); table.setAttribute(QStringLiteral("cellspacing"), QStringLiteral("0")); body.appendChild(table); TimeSheetInfo totalsLine(m_numberOfWeeks); if (!timeSheetInfo.isEmpty()) { totalsLine = timeSheetInfo.first(); if (rootTask() == 0) timeSheetInfo.removeAt(0); // there is always one, because there is always the root item } { //Header Row QDomElement headerRow = doc.createElement(QStringLiteral("tr")); headerRow.setAttribute(QStringLiteral("class"), QStringLiteral("header_row")); table.appendChild(headerRow); addTblHdr(headerRow, tr("Task")); for (int i = 0; i < m_numberOfWeeks; ++i) addTblHdr(headerRow, tr("Week")); addTblHdr(headerRow, tr("Total")); addTblHdr(headerRow, tr("Days")); } { //Header day row QDomElement headerDayRow = doc.createElement(QStringLiteral("tr")); headerDayRow.setAttribute(QStringLiteral("class"), QStringLiteral("header_row")); table.appendChild(headerDayRow); addTblHdr(headerDayRow, QString()); for (int i = 0; i < m_numberOfWeeks; ++i) { QString label = tr("%1").arg(startDate().addDays( i * 7).weekNumber(), 2, 10, QLatin1Char('0')); addTblHdr(headerDayRow, label); } addTblHdr(headerDayRow, QString()); addTblHdr(headerDayRow, QString::number(m_dailyhours) + tr(" hours")); } for (int i = 0; i < timeSheetInfo.size(); ++i) { QDomElement row = doc.createElement(QStringLiteral("tr")); if (i % 2) row.setAttribute(QStringLiteral("class"), QStringLiteral("alternate_row")); table.appendChild(row); QDomElement taskCell = addTblCell(row, timeSheetInfo[i].formattedTaskIdAndName( CONFIGURATION.taskPaddingLength)); taskCell.setAttribute(QStringLiteral("align"), QStringLiteral("left")); taskCell.setAttribute(QStringLiteral("style"), QStringLiteral("text-indent: %1px;") .arg(9 * timeSheetInfo[i].indentation)); for (int week = 0; week < m_numberOfWeeks; ++week) addTblCell(row, hoursAndMinutes(timeSheetInfo[i].seconds[week])); addTblCell(row, hoursAndMinutes(timeSheetInfo[i].total())); addTblCell(row, QString::number(timeSheetInfo[i].total() / SecondsInDay, 'f', 1)); } { // Totals row QDomElement totals = doc.createElement(QStringLiteral("tr")); totals.setAttribute(QStringLiteral("class"), QStringLiteral("header_row")); table.appendChild(totals); addTblHdr(totals, tr("Total:")); for (int i = 0; i < m_numberOfWeeks; ++i) addTblHdr(totals, hoursAndMinutes(totalsLine.seconds[i])); addTblHdr(totals, hoursAndMinutes(totalsLine.total())); addTblHdr(totals, QString::number(totalsLine.total() / SecondsInDay, 'f', 1)); } } // NOTE: seems like the style sheet has to be set before the html // code is pushed into the QTextDocument report.setDefaultStyleSheet(Charm::reportStylesheet(palette())); report.setHtml(doc.toString()); setDocument(&report); uploadButton()->setVisible(false); uploadButton()->setEnabled(false); } void MonthlyTimeSheetReport::slotLinkClicked(const QUrl &which) { QDate start = which.toString() == QLatin1String("Previous") ? startDate().addMonths(-1) : startDate().addMonths(1); QDate end = which.toString() == QLatin1String("Previous") ? endDate().addMonths(-1) : endDate().addMonths(1); setReportProperties(start, end, rootTask(), activeTasksOnly()); } Charm-1.12.0/Charm/Widgets/MonthlyTimesheet.h000066400000000000000000000033341331066577000207530ustar00rootroot00000000000000/* MonthlyTimesheet.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MONTHLYTIMESHEET3_H #define MONTHLYTIMESHEET3_H #include #include "Timesheet.h" class QUrl; class MonthlyTimeSheetReport : public TimeSheetReport { Q_OBJECT public: explicit MonthlyTimeSheetReport(QWidget *parent = nullptr); ~MonthlyTimeSheetReport() override; void setReportProperties(const QDate &start, const QDate &end, TaskId rootTask, bool activeTasksOnly) override; private Q_SLOTS: void slotLinkClicked(const QUrl &which); private: QString suggestedFileName() const override; void update() override; QByteArray saveToText() override; QByteArray saveToXml(SaveToXmlMode mode) override; private: // properties of the report: int m_numberOfWeeks = 0; int m_monthNumber = 0; int m_yearOfMonth = 0; QString m_weeklyhours; float m_dailyhours; }; #endif Charm-1.12.0/Charm/Widgets/MonthlyTimesheetConfigurationDialog.cpp000066400000000000000000000145231331066577000251600ustar00rootroot00000000000000/* MonthlyTimesheetConfigurationDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "MonthlyTimesheetConfigurationDialog.h" #include #include "DateEntrySyncer.h" #include "SelectTaskDialog.h" #include "ViewHelpers.h" #include "CharmCMake.h" #include "MonthlyTimesheet.h" #include "ui_MonthlyTimesheetConfigurationDialog.h" MonthlyTimesheetConfigurationDialog::MonthlyTimesheetConfigurationDialog(QWidget *parent) : ReportConfigurationDialog(parent) , m_ui(new Ui::MonthlyTimesheetConfigurationDialog) { setWindowTitle(tr("Monthly Timesheet")); m_ui->setupUi(this); connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &MonthlyTimesheetConfigurationDialog::accept); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &MonthlyTimesheetConfigurationDialog::reject); connect(m_ui->comboBoxMonth, SIGNAL(currentIndexChanged(int)), SLOT(slotMonthComboItemSelected(int))); connect(m_ui->toolButtonSelectTask, &QToolButton::clicked, this, &MonthlyTimesheetConfigurationDialog::slotSelectTask); connect(m_ui->checkBoxSubTasksOnly, &QCheckBox::toggled, this, &MonthlyTimesheetConfigurationDialog::slotCheckboxSubtasksOnlyChecked); m_ui->comboBoxMonth->setCurrentIndex(1); slotCheckboxSubtasksOnlyChecked(m_ui->checkBoxSubTasksOnly->isChecked()); slotStandardTimeSpansChanged(); connect(ApplicationCore::instance().dateChangeWatcher(), &DateChangeWatcher::dateChanged, this, &MonthlyTimesheetConfigurationDialog::slotStandardTimeSpansChanged); // set current month and year: m_ui->spinBoxMonth->setValue(QDate::currentDate().month()); m_ui->spinBoxYear->setValue(QDate::currentDate().year()); // load settings: QSettings settings; if (settings.contains(MetaKey_TimesheetActiveOnly)) { m_ui->checkBoxActiveOnly->setChecked(settings.value(MetaKey_TimesheetActiveOnly).toBool()); } else { m_ui->checkBoxActiveOnly->setChecked(true); } } MonthlyTimesheetConfigurationDialog::~MonthlyTimesheetConfigurationDialog() { } void MonthlyTimesheetConfigurationDialog::setDefaultMonth(int yearOfMonth, int month) { m_ui->spinBoxMonth->setValue(month); m_ui->spinBoxYear->setValue(yearOfMonth); m_ui->comboBoxMonth->setCurrentIndex(4); } void MonthlyTimesheetConfigurationDialog::accept() { // save settings: QSettings settings; settings.setValue(MetaKey_TimesheetActiveOnly, m_ui->checkBoxActiveOnly->isChecked()); settings.setValue(MetaKey_TimesheetRootTask, m_rootTask); QDialog::accept(); } void MonthlyTimesheetConfigurationDialog::showReportPreviewDialog() { QDate start, end; int index = m_ui->comboBoxMonth->currentIndex(); if (index == m_monthInfo.size() -1) { // manual selection start = QDate(m_ui->spinBoxYear->value(), m_ui->spinBoxMonth->value(), 1); end = start.addMonths(1); } else { start = m_monthInfo[index].timespan.first; end = m_monthInfo[index].timespan.second; } bool activeOnly = m_ui->checkBoxActiveOnly->isChecked(); auto report = new MonthlyTimeSheetReport(); report->setReportProperties(start, end, m_rootTask, activeOnly); report->show(); } void MonthlyTimesheetConfigurationDialog::showEvent(QShowEvent *) { QSettings settings; // we only want to do this once a backend is loaded, and we ignore // the saved root task if it does not exist anymore if (settings.contains(MetaKey_TimesheetRootTask)) { TaskId root = settings.value(MetaKey_TimesheetRootTask).toInt(); const TaskTreeItem &item = DATAMODEL->taskTreeItem(root); if (item.isValid()) { m_rootTask = root; m_ui->labelTaskName->setText(DATAMODEL->fullTaskName(item.task())); m_ui->checkBoxSubTasksOnly->setChecked(true); } } } void MonthlyTimesheetConfigurationDialog::slotCheckboxSubtasksOnlyChecked(bool checked) { if (checked && m_rootTask == 0) slotSelectTask(); if (!checked) { m_rootTask = 0; m_ui->labelTaskName->setText(tr("(All Tasks)")); } } void MonthlyTimesheetConfigurationDialog::slotStandardTimeSpansChanged() { const TimeSpans timeSpans; m_monthInfo = timeSpans.last4Months(); NamedTimeSpan custom = { tr("Manual Selection"), timeSpans.thisMonth().timespan, Range }; m_monthInfo << custom; m_ui->comboBoxMonth->clear(); for (int i = 0; i < m_monthInfo.size(); ++i) m_ui->comboBoxMonth->addItem(m_monthInfo[i].name); // Set current index to "Last Month" as that's what you'll usually want m_ui->comboBoxMonth->setCurrentIndex(1); } void MonthlyTimesheetConfigurationDialog::slotMonthComboItemSelected(int index) { // wait for the next update, in this case: if (m_ui->comboBoxMonth->count() == 0 || index == -1) return; Q_ASSERT(m_ui->comboBoxMonth->count() > index); if (index == m_monthInfo.size() - 1) { // manual selection m_ui->groupBox->setEnabled(true); } else { m_ui->groupBox->setEnabled(false); } } void MonthlyTimesheetConfigurationDialog::slotSelectTask() { SelectTaskDialog dialog(this); dialog.setNonTrackableSelectable(); if (dialog.exec()) { m_rootTask = dialog.selectedTask(); const TaskTreeItem &item = DATAMODEL->taskTreeItem(m_rootTask); m_ui->labelTaskName->setText(DATAMODEL->fullTaskName(item.task())); } else { if (m_rootTask == 0) m_ui->checkBoxSubTasksOnly->setChecked(false); } } Charm-1.12.0/Charm/Widgets/MonthlyTimesheetConfigurationDialog.h000066400000000000000000000035611331066577000246250ustar00rootroot00000000000000/* MonthlyTimesheetConfigurationDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MONTHLYTIMESHEETCONFIGURATIONDIALOG_H #define MONTHLYTIMESHEETCONFIGURATIONDIALOG_H #include #include #include "ReportConfigurationDialog.h" #include namespace Ui { class MonthlyTimesheetConfigurationDialog; } class MonthlyTimesheetConfigurationDialog : public ReportConfigurationDialog { Q_OBJECT public: explicit MonthlyTimesheetConfigurationDialog(QWidget *parent); ~MonthlyTimesheetConfigurationDialog() override; void showReportPreviewDialog() override; void showEvent(QShowEvent *) override; void setDefaultMonth(int yearOfMonth, int month); public Q_SLOTS: void accept() override; private Q_SLOTS: void slotCheckboxSubtasksOnlyChecked(bool); void slotStandardTimeSpansChanged(); void slotMonthComboItemSelected(int); void slotSelectTask(); private: QScopedPointer m_ui; QList m_monthInfo; TaskId m_rootTask; }; #endif Charm-1.12.0/Charm/Widgets/MonthlyTimesheetConfigurationDialog.ui000066400000000000000000000166671331066577000250260ustar00rootroot00000000000000 MonthlyTimesheetConfigurationDialog 0 0 402 270 Qt::Horizontal 40 20 &Select month: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter comboBoxMonth 0 0 QComboBox::AdjustToContents Qt::Horizontal 40 20 Manual Selection QFormLayout::AllNonFixedFieldsGrow Month: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 12 1900 5000 2011 Qt::Horizontal 40 20 What Tasks to include in the report: Show... false 0 0 (task name) Qt::AlignCenter ... and subtasks false Select Task... (all tasks, otherwise). Qt::Horizontal 40 20 Only show tasks with activity true QDialogButtonBox::Cancel|QDialogButtonBox::Ok comboBoxMonth spinBoxMonth spinBoxYear checkBoxSubTasksOnly toolButtonSelectTask checkBoxActiveOnly buttonBox checkBoxSubTasksOnly toggled(bool) labelTaskName setEnabled(bool) 68 171 137 171 checkBoxSubTasksOnly toggled(bool) toolButtonSelectTask setEnabled(bool) 84 182 151 204 Charm-1.12.0/Charm/Widgets/NotificationPopup.cpp000066400000000000000000000040601331066577000214530ustar00rootroot00000000000000/* NotificationPopup.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Michel Boyer de la Giroday This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "NotificationPopup.h" #include "ui_NotificationPopup.h" #include #include #include #include NotificationPopup::NotificationPopup(QWidget *parent) : QDialog(parent) , m_ui(new Ui::NotificationPopup) { m_ui->setupUi(this); setAttribute(Qt::WA_ShowWithoutActivating); setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); } NotificationPopup::~NotificationPopup() { } void NotificationPopup::showNotification(const QString &title, const QString &message) { QString titleText = m_ui->titleLB->text(); m_ui->titleLB->setText(titleText.replace(QLatin1String("TITLE"), title)); QString messageText = m_ui->messageLB->text(); m_ui->messageLB->setText(messageText.replace(QLatin1String("MESSAGE"), message)); setGeometry(QStyle::alignedRect(Qt::RightToLeft, Qt::AlignBottom, size(), qApp->desktop()->availableGeometry())); show(); QTimer::singleShot(10000, this, SLOT(slotCloseNotification())); } void NotificationPopup::slotCloseNotification() { close(); } void NotificationPopup::mousePressEvent(QMouseEvent *) { close(); } Charm-1.12.0/Charm/Widgets/NotificationPopup.h000066400000000000000000000026761331066577000211330ustar00rootroot00000000000000/* NotificationPopup.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Michel Boyer de la Giroday This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef NOTIFICATIONPOPUP_H #define NOTIFICATIONPOPUP_H #include #include namespace Ui { class NotificationPopup; } class NotificationPopup : public QDialog { Q_OBJECT public: explicit NotificationPopup(QWidget *parent = nullptr); ~NotificationPopup() override; void showNotification(const QString &title, const QString &message); private Q_SLOTS: void slotCloseNotification(); private: void mousePressEvent(QMouseEvent *event) override; QScopedPointer m_ui; }; #endif // NOTIFICATIONPOPUP_H Charm-1.12.0/Charm/Widgets/NotificationPopup.ui000066400000000000000000000033421331066577000213100ustar00rootroot00000000000000 NotificationPopup 0 0 200 65 Notification 0.800000000000000 0 0 <html><head/><body><p>MESSAGE</p></body></html> Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 0 0 <html><head/><body><p><span style=" font-weight:600;">TITLE</span></p></body></html> Qt::AlignCenter Charm-1.12.0/Charm/Widgets/ReportConfigurationDialog.cpp000066400000000000000000000020031331066577000231170ustar00rootroot00000000000000/* ReportConfigurationDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ReportConfigurationDialog.h" ReportConfigurationDialog::ReportConfigurationDialog(QWidget *parent) : QDialog(parent) { } Charm-1.12.0/Charm/Widgets/ReportConfigurationDialog.h000066400000000000000000000026561331066577000226020ustar00rootroot00000000000000/* ReportConfigurationDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef REPORTCONFIGURATIONDIALOG_H #define REPORTCONFIGURATIONDIALOG_H #include /** Base class for report configuration dialogs. */ class ReportConfigurationDialog : public QDialog { Q_OBJECT public: explicit ReportConfigurationDialog(QWidget *parent = nullptr); /** generates a report preview dialog that follows the settings made by the user. The dialog is supposed to destroy-on-close and non-modal. @param parent parent widget for the preview dialog */ virtual void showReportPreviewDialog() = 0; }; #endif Charm-1.12.0/Charm/Widgets/ReportPreviewWindow.cpp000066400000000000000000000077361331066577000220230ustar00rootroot00000000000000/* ReportPreviewWindow.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ReportPreviewWindow.h" #include "ViewHelpers.h" #ifndef QT_NO_PRINTER #include #include #endif #include "ui_ReportPreviewWindow.h" ReportPreviewWindow::ReportPreviewWindow(QWidget *parent) : QDialog(parent) , m_ui(new Ui::ReportPreviewWindow) { m_ui->setupUi(this); setAttribute(Qt::WA_DeleteOnClose); connect(m_ui->pushButtonClose, &QPushButton::clicked, this, &ReportPreviewWindow::slotClose); connect(m_ui->pushButtonUpdate, &QPushButton::clicked, this, &ReportPreviewWindow::slotUpdate); connect(m_ui->pushButtonSave, &QPushButton::clicked, this, &ReportPreviewWindow::slotSaveToXml); connect(m_ui->pushButtonSaveTotals, &QPushButton::clicked, this, &ReportPreviewWindow::slotSaveToText); connect(m_ui->textBrowser, &QTextBrowser::anchorClicked, this, &ReportPreviewWindow::anchorClicked); #ifndef QT_NO_PRINTER connect(m_ui->pushButtonPrint, &QPushButton::clicked, this, &ReportPreviewWindow::slotPrint); #else m_ui->pushButtonPrint->setEnabled(false); #endif m_updateTimer.setInterval(60 * 1000); m_updateTimer.start(); connect(&m_updateTimer, &QTimer::timeout, this, &ReportPreviewWindow::slotUpdate); resize(850, 600); } ReportPreviewWindow::~ReportPreviewWindow() { } void ReportPreviewWindow::setDocument(const QTextDocument *document) { if (document != nullptr) { // we keep a copy, to be able to show different versions of the same document QScopedPointer docClone(document->clone()); m_document.swap(docClone); m_ui->textBrowser->setDocument(m_document.data()); } else { m_ui->textBrowser->setDocument(nullptr); m_document.reset(); } } QDomDocument ReportPreviewWindow::createReportTemplate() const { // create XHTML v1.0 structure: QDomDocument doc(QStringLiteral("html")); // FIXME this is only a rudimentary subset of a valid xhtml 1 document // html element QDomElement html = doc.createElement(QStringLiteral("html")); html.setAttribute(QStringLiteral("xmlns"), QStringLiteral("http://www.w3.org/1999/xhtml")); doc.appendChild(html); // head and body, children of html QDomElement head = doc.createElement(QStringLiteral("head")); html.appendChild(head); QDomElement body = doc.createElement(QStringLiteral("body")); html.appendChild(body); return doc; } QPushButton *ReportPreviewWindow::saveToXmlButton() const { return m_ui->pushButtonSave; } QPushButton *ReportPreviewWindow::saveToTextButton() const { return m_ui->pushButtonSaveTotals; } QPushButton *ReportPreviewWindow::uploadButton() const { return m_ui->pushButtonUpload; } void ReportPreviewWindow::slotSaveToXml() { } void ReportPreviewWindow::slotSaveToText() { } void ReportPreviewWindow::slotPrint() { #ifndef QT_NO_PRINTER QPrinter printer; QPrintDialog dialog(&printer, this); if (dialog.exec()) m_document->print(&printer); #endif } void ReportPreviewWindow::slotUpdate() { } void ReportPreviewWindow::slotClose() { close(); } Charm-1.12.0/Charm/Widgets/ReportPreviewWindow.h000066400000000000000000000035541331066577000214620ustar00rootroot00000000000000/* ReportPreviewWindow.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef REPORTPREVIEWWINDOW_H #define REPORTPREVIEWWINDOW_H #include #include #include #include #include namespace Ui { class ReportPreviewWindow; } class QPushButton; class ReportPreviewWindow : public QDialog { Q_OBJECT public: explicit ReportPreviewWindow(QWidget *parent = nullptr); ~ReportPreviewWindow() override; Q_SIGNALS: void anchorClicked(const QUrl &which); protected: void setDocument(const QTextDocument *document); QDomDocument createReportTemplate() const; QPushButton *saveToXmlButton() const; QPushButton *saveToTextButton() const; QPushButton *uploadButton() const; QTimer m_updateTimer; private Q_SLOTS: virtual void slotSaveToXml(); virtual void slotSaveToText(); virtual void slotPrint(); virtual void slotUpdate(); virtual void slotClose(); private: QScopedPointer m_ui; QScopedPointer m_document; }; #endif Charm-1.12.0/Charm/Widgets/ReportPreviewWindow.ui000066400000000000000000000041021331066577000216360ustar00rootroot00000000000000 ReportPreviewWindow 0 0 593 258 Report Preview false &Close Qt::Horizontal 40 20 &Update Upload Save As &XML... Save &Totals... &Print... Charm-1.12.0/Charm/Widgets/SelectTaskDialog.cpp000066400000000000000000000227101331066577000211650ustar00rootroot00000000000000/* SelectTaskDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "SelectTaskDialog.h" #include "ExpandStatesHelper.h" #include "GUIState.h" #include "ViewHelpers.h" #include #include #include "ui_SelectTaskDialog.h" SelectTaskDialogProxy::SelectTaskDialogProxy(CharmDataModel *model, QObject *parent) : ViewFilter(model, parent) { // we filter for the task name column setFilterKeyColumn(Column_TaskId); setFilterCaseSensitivity(Qt::CaseInsensitive); setFilterRole(TasksViewRole_Filter); prefilteringModeChanged(); } bool SelectTaskDialogProxy::filterAcceptsColumn(int column, const QModelIndex &) const { return column == Column_TaskId; } Qt::ItemFlags SelectTaskDialogProxy::flags(const QModelIndex &index) const { if (index.isValid()) { return Qt::ItemIsEnabled | Qt::ItemIsSelectable; } else { return Qt::NoItemFlags; } } QVariant SelectTaskDialogProxy::data(const QModelIndex &index, int role) const { if (index.isValid() && role == Qt::CheckStateRole) { return QVariant(); } else { return ViewFilter::data(index, role); } } SelectTaskDialog::SelectTaskDialog(QWidget *parent) : QDialog(parent) , m_ui(new Ui::SelectTaskDialog()) , m_proxy(MODEL.charmDataModel()) { m_ui->setupUi(this); m_ui->treeView->setModel(&m_proxy); m_ui->treeView->header()->hide(); m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); connect(m_ui->treeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &SelectTaskDialog::slotCurrentItemChanged); connect(m_ui->treeView, &QTreeView::doubleClicked, this, &SelectTaskDialog::slotDoubleClicked); connect(m_ui->filter, &QLineEdit::textChanged, this, &SelectTaskDialog::slotFilterTextChanged); connect(m_ui->showExpired, &QCheckBox::toggled, this, &SelectTaskDialog::slotPrefilteringChanged); connect(m_ui->showSelected, &QCheckBox::toggled, this, &SelectTaskDialog::slotPrefilteringChanged); connect(this, &SelectTaskDialog::accepted, this, &SelectTaskDialog::slotAccepted); connect(MODEL.charmDataModel(), &CharmDataModel::resetGUIState, this, &SelectTaskDialog::slotResetState); connect(m_ui->filter, &QLineEdit::textChanged, this, &SelectTaskDialog::slotSelectTask); QSettings settings; settings.beginGroup(QString::fromUtf8(staticMetaObject.className())); if (settings.contains(MetaKey_MainWindowGeometry)) resize(settings.value(MetaKey_MainWindowGeometry).toSize()); // initialize prefiltering slotPrefilteringChanged(); m_ui->filter->setFocus(); } SelectTaskDialog::~SelectTaskDialog() { } void SelectTaskDialog::slotResetState() { QSettings settings; settings.beginGroup(QString::fromUtf8(staticMetaObject.className())); GUIState state; state.loadFrom(settings); QModelIndex index(m_proxy.indexForTaskId(state.selectedTask())); if (index.isValid()) m_ui->treeView->setCurrentIndex(index); Q_FOREACH (const TaskId id, state.expandedTasks()) { QModelIndex indexForId(m_proxy.indexForTaskId(id)); if (indexForId.isValid()) m_ui->treeView->expand(indexForId); } m_ui->showExpired->setChecked(state.showExpired()); m_ui->showSelected->setChecked(state.showCurrents()); } void SelectTaskDialog::showEvent(QShowEvent *event) { slotResetState(); QDialog::showEvent(event); } void SelectTaskDialog::hideEvent(QHideEvent *event) { QSettings settings; settings.beginGroup(QString::fromUtf8(staticMetaObject.className())); settings.setValue(MetaKey_MainWindowGeometry, size()); QDialog::hideEvent(event); } TaskId SelectTaskDialog::selectedTask() const { return m_selectedTask; } void SelectTaskDialog::selectTask(TaskId task) { m_selectedTask = task; QModelIndex index(m_proxy.indexForTaskId(m_selectedTask)); if (index.isValid()) m_ui->treeView->setCurrentIndex(index); else m_ui->treeView->setCurrentIndex(QModelIndex()); } void SelectTaskDialog::slotCurrentItemChanged(const QModelIndex &first, const QModelIndex &) { const Task task = m_proxy.taskForIndex(first); if (isValidAndTrackable(first)) { m_selectedTask = task.id(); m_ui->taskStatusLB->clear(); } else { m_selectedTask = 0; const bool expired = !task.isCurrentlyValid(); const bool trackable = task.trackable(); const bool notTrackableAndExpired = (!trackable && expired); const QString expirationDate = QLocale::system().toString( task.validUntil(), QLocale::ShortFormat); const QString info = notTrackableAndExpired ? tr( "The selected task is not trackable and expired since %1").arg(expirationDate) : expired ? tr("The selected task is expired since %1").arg( expirationDate) : tr("The selected task is not trackable"); m_ui->taskStatusLB->setText(info); } m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(m_selectedTask != 0); } bool SelectTaskDialog::isValidAndTrackable(const QModelIndex &index) const { if (!index.isValid()) return false; const Task task = m_proxy.taskForIndex(index); const bool taskValid = m_nonValidSelectable || (task.isValid() && task.isCurrentlyValid()); if (m_nonTrackableSelectable) return taskValid; return taskValid && task.trackable(); } void SelectTaskDialog::slotDoubleClicked(const QModelIndex &index) { if (isValidAndTrackable(index)) accept(); } void SelectTaskDialog::slotFilterTextChanged(const QString &text) { QString filtertext = text.simplified(); filtertext.replace(QLatin1Char(' '), QLatin1Char('*')); Charm::saveExpandStates(m_ui->treeView, &m_expansionStates); m_proxy.setFilterWildcard(filtertext); if (!filtertext.isEmpty()) { m_ui->treeView->expandAll(); } else { Charm::restoreExpandStates(m_ui->treeView, &m_expansionStates); } } void SelectTaskDialog::slotAccepted() { QSettings settings; // FIXME refactor, code duplication with taskview // save user settings if (ApplicationCore::instance().state() == Connected || ApplicationCore::instance().state() == Disconnecting) { GUIState state; // selected task state.setSelectedTask(selectedTask()); // expanded tasks TaskList tasks = MODEL.charmDataModel()->getAllTasks(); TaskIdList expandedTasks; Q_FOREACH (const Task &task, tasks) { QModelIndex index(m_proxy.indexForTaskId(task.id())); if (m_ui->treeView->isExpanded(index)) expandedTasks << task.id(); } state.setExpandedTasks(expandedTasks); state.setShowExpired(m_ui->showExpired->isChecked()); state.setShowCurrents(m_ui->showSelected->isChecked()); settings.beginGroup(QString::fromUtf8(staticMetaObject.className())); state.saveTo(settings); } } void SelectTaskDialog::slotPrefilteringChanged() { // find out about the selected mode: Configuration::TaskPrefilteringMode mode; const bool showCurrentOnly = !m_ui->showExpired->isChecked(); const bool showSubscribedOnly = m_ui->showSelected->isChecked(); if (showCurrentOnly && showSubscribedOnly) { mode = Configuration::TaskPrefilter_SubscribedAndCurrentOnly; } else if (showCurrentOnly && !showSubscribedOnly) { mode = Configuration::TaskPrefilter_CurrentOnly; } else if (!showCurrentOnly && showSubscribedOnly) { mode = Configuration::TaskPrefilter_SubscribedOnly; } else { mode = Configuration::TaskPrefilter_ShowAll; } CONFIGURATION.taskPrefilteringMode = mode; Charm::saveExpandStates(m_ui->treeView, &m_expansionStates); m_proxy.prefilteringModeChanged(); Charm::restoreExpandStates(m_ui->treeView, &m_expansionStates); emit saveConfiguration(); } void SelectTaskDialog::setNonTrackableSelectable() { m_nonTrackableSelectable = true; } void SelectTaskDialog::setNonValidSelectable() { m_nonValidSelectable = true; } void SelectTaskDialog::slotSelectTask(const QString &filter) { const QString filterText = filter.simplified().toUpper().replace(QLatin1Char('*'), QLatin1Char(' ')); const int filterTaskId = filterText.toInt(); const TaskList tasks = MODEL.charmDataModel()->getAllTasks(); for (const auto task : tasks) { if (!task.isValid()) continue; if (task.name().toUpper().contains(filterText) || task.id() == filterTaskId) selectTask(task.id()); } } Charm-1.12.0/Charm/Widgets/SelectTaskDialog.h000066400000000000000000000050601331066577000206310ustar00rootroot00000000000000/* SelectTaskDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef SELECTTASKDIALOG_H #define SELECTTASKDIALOG_H #include #include #include #include "ViewFilter.h" class ViewFilter; class CharmDataModel; namespace Ui { class SelectTaskDialog; } class SelectTaskDialogProxy : public ViewFilter { Q_OBJECT public: explicit SelectTaskDialogProxy(CharmDataModel *, QObject *parent = nullptr); Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; protected: bool filterAcceptsColumn(int column, const QModelIndex &parent) const override; }; class SelectTaskDialog : public QDialog { Q_OBJECT public: explicit SelectTaskDialog(QWidget *parent = nullptr); ~SelectTaskDialog() override; TaskId selectedTask() const; void setNonTrackableSelectable(); void setNonValidSelectable(); void selectTask(TaskId); Q_SIGNALS: void saveConfiguration(); protected: void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override; private Q_SLOTS: void slotCurrentItemChanged(const QModelIndex &, const QModelIndex &); void slotDoubleClicked(const QModelIndex &); void slotFilterTextChanged(const QString &); void slotAccepted(); void slotPrefilteringChanged(); void slotResetState(); void slotSelectTask(const QString &); private: bool isValidAndTrackable(const QModelIndex &index) const; private: QScopedPointer m_ui; TaskId m_selectedTask = {}; SelectTaskDialogProxy m_proxy; QHash m_expansionStates; bool m_nonTrackableSelectable = false; bool m_nonValidSelectable = false; }; #endif Charm-1.12.0/Charm/Widgets/SelectTaskDialog.ui000066400000000000000000000057561331066577000210330ustar00rootroot00000000000000 SelectTaskDialog 0 0 494 250 0 0 Select Task... true Qt::ElideMiddle Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok Qt::Horizontal 40 20 Show expired Show only subscribed true buttonBox treeView buttonBox accepted() SelectTaskDialog accept() 300 26 285 1 buttonBox rejected() SelectTaskDialog reject() 320 58 318 -2 Charm-1.12.0/Charm/Widgets/TaskEditor.cpp000066400000000000000000000152571331066577000200640ustar00rootroot00000000000000/* TaskEditor.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TaskEditor.h" #include "SelectTaskDialog.h" #include "ViewHelpers.h" #include "Core/CharmConstants.h" #include "Core/CharmDataModel.h" #include "Core/TaskTreeItem.h" #include #include #include #include #include "ui_TaskEditor.h" TaskEditor::TaskEditor(QWidget *parent) : QDialog(parent) , m_ui(new Ui::TaskEditor) { m_ui->setupUi(this); m_ui->dateEditFrom->calendarWidget()->setFirstDayOfWeek(Qt::Monday); m_ui->dateEditFrom->calendarWidget()->setVerticalHeaderFormat(QCalendarWidget::ISOWeekNumbers); m_ui->dateEditTo->calendarWidget()->setFirstDayOfWeek(Qt::Monday); m_ui->dateEditTo->calendarWidget()->setVerticalHeaderFormat(QCalendarWidget::ISOWeekNumbers); connect(m_ui->pushButtonParent, &QPushButton::clicked, this, &TaskEditor::slotSelectParent); connect(m_ui->dateEditFrom, &QDateEdit::dateChanged, this, &TaskEditor::slotDateChanged); connect(m_ui->dateEditTo, &QDateEdit::dateChanged, this, &TaskEditor::slotDateChanged); connect(m_ui->checkBoxFrom, &QCheckBox::clicked, this, &TaskEditor::slotCheckBoxChecked); connect(m_ui->checkBoxUntil, &QCheckBox::clicked, this, &TaskEditor::slotCheckBoxChecked); } TaskEditor::~TaskEditor() { } void TaskEditor::setTask(const Task &task) { Q_ASSERT(m_ui); m_task = task; const TaskTreeItem &taskTreeItem = MODEL.charmDataModel()->taskTreeItem(task.id()); m_ui->labelTaskName->setText(MODEL.charmDataModel()->fullTaskName(taskTreeItem.task())); m_ui->lineEditName->setText(task.name()); m_ui->checkBoxTrackable->setChecked(task.trackable()); if (task.parent() != 0) { const TaskTreeItem &parentItem = MODEL.charmDataModel()->taskTreeItem(task.parent()); const QString name = parentItem.task().name(); m_ui->pushButtonParent->setText(name); } else { m_ui->pushButtonParent->setText(tr("Choose Parent Task")); } if (task.parent() == 0) { m_ui->checkBoxTopLevel->setChecked(true); } else { m_ui->checkBoxTopLevel->setChecked(false); } QDate start = task.validFrom().date(); if (start.isValid()) { m_ui->dateEditFrom->setDate(start); m_ui->checkBoxFrom->setChecked(false); } else { m_ui->checkBoxFrom->setChecked(true); m_ui->dateEditFrom->setDate(QDate::currentDate()); } QDate end = task.validUntil().date(); if (end.isValid()) { m_ui->dateEditTo->setDate(end); m_ui->checkBoxUntil->setChecked(false); } else { m_ui->checkBoxUntil->setChecked(true); m_ui->dateEditTo->setDate(QDate::currentDate()); } m_ui->lineEditComment->setText(task.comment()); checkInvariants(); } Task TaskEditor::getTask() const { Task newTask = m_task; newTask.setName(m_ui->lineEditName->text()); newTask.setTrackable(m_ui->checkBoxTrackable->isChecked()); if (m_ui->checkBoxTopLevel->isChecked()) newTask.setParent(0); if (m_ui->checkBoxFrom->isChecked()) { newTask.setValidFrom(QDateTime()); } else { newTask.setValidFrom(m_ui->dateEditFrom->dateTime()); } if (m_ui->checkBoxUntil->isChecked()) { newTask.setValidUntil(QDateTime()); } else { newTask.setValidUntil(m_ui->dateEditTo->dateTime()); } newTask.setComment(m_ui->lineEditComment->text()); return newTask; } bool parentTreeIsTree(TaskId newParent, TaskId task) { QList parents; TaskId parent = newParent; while (parent != 0) { parents << parent; const TaskTreeItem &parentItem = MODEL.charmDataModel()->taskTreeItem(parent); parent = parentItem.task().parent(); } return !parents.contains(task); } void TaskEditor::slotSelectParent() { SelectTaskDialog dialog(this); dialog.setNonTrackableSelectable(); Q_FOREVER { if (dialog.exec()) { TaskId newParentId = dialog.selectedTask(); const TaskTreeItem &parentItem = MODEL.charmDataModel()->taskTreeItem(newParentId); QString name = m_task.name(); QString parent = parentItem.task().name(); if (parentTreeIsTree(newParentId, m_task.id())) { m_task.setParent(dialog.selectedTask()); if (newParentId != 0) { m_ui->pushButtonParent->setText(parent); } else { m_ui->pushButtonParent->setText(tr("Choose Parent Task")); } break; } QMessageBox::information(this, tr("Please choose another task"), tr( "The task \"%1\" cannot be selected as the parent task for \"%2\"," " because they are the same, or \"%3\" is a direct or indirect subtask of \"%4\".") .arg(parent, name, parent, name)); } else { break; } } } void TaskEditor::slotDateChanged(const QDate &) { checkInvariants(); } void TaskEditor::slotCheckBoxChecked(bool) { checkInvariants(); } void TaskEditor::checkInvariants() { bool acceptable; // the start and end dates are acceptable if // * both start and end date are deactivated, // * if only one date is set, or // * if none are set, and the start date is before the end date if (m_ui->checkBoxFrom->isChecked() && m_ui->checkBoxUntil->isChecked()) { acceptable = true; } else if (m_ui->checkBoxFrom->isChecked() || m_ui->checkBoxUntil->isChecked()) { acceptable = true; } else if (m_ui->dateEditFrom->dateTime() < m_ui->dateEditTo->dateTime()) { acceptable = true; } else { acceptable = false; } m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(acceptable); } Charm-1.12.0/Charm/Widgets/TaskEditor.h000066400000000000000000000027661331066577000175320ustar00rootroot00000000000000/* TaskEditor.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TASKEDITOR_H #define TASKEDITOR_H #include #include "Core/Task.h" #include namespace Ui { class TaskEditor; } class TaskEditor : public QDialog { Q_OBJECT public: explicit TaskEditor(QWidget *parent = nullptr); ~TaskEditor() override; void setTask(const Task &task); Task getTask() const; private Q_SLOTS: void slotSelectParent(); void slotDateChanged(const QDate &date); void slotCheckBoxChecked(bool); private: void checkInvariants(); QScopedPointer m_ui; Task m_task; }; #endif /* TASKEDITOR_H */ Charm-1.12.0/Charm/Widgets/TaskEditor.ui000066400000000000000000000456231331066577000177170ustar00rootroot00000000000000 TaskEditor 0 0 483 316 Dialog 0 6 0 0 0 1 255 255 255 66 66 66 99 99 99 82 82 82 33 33 33 44 44 44 255 255 255 255 255 255 255 255 255 0 0 0 66 66 66 0 0 0 33 33 33 255 255 220 0 0 0 255 255 255 66 66 66 99 99 99 82 82 82 33 33 33 44 44 44 255 255 255 255 255 255 255 255 255 0 0 0 66 66 66 0 0 0 33 33 33 255 255 220 0 0 0 33 33 33 66 66 66 99 99 99 82 82 82 33 33 33 44 44 44 33 33 33 255 255 255 33 33 33 66 66 66 66 66 66 0 0 0 66 66 66 255 255 220 0 0 0 true (Task Name) Qt::AlignCenter true 12 12 12 12 12 Name: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Parent task: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Choose... No parent true true Valid from: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter No from date Valid until: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter No until date Comment: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Time can be tracked to this task Qt::Vertical 14 13 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() TaskEditor accept() 254 275 157 274 buttonBox rejected() TaskEditor reject() 322 275 286 274 checkBoxTopLevel toggled(bool) pushButtonParent setDisabled(bool) 290 114 223 110 checkBoxFrom toggled(bool) dateEditFrom setDisabled(bool) 313 161 269 158 checkBoxUntil toggled(bool) dateEditTo setDisabled(bool) 315 200 268 201 Charm-1.12.0/Charm/Widgets/TaskIdDialog.cpp000066400000000000000000000036141331066577000203040ustar00rootroot00000000000000/* TaskIdDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TaskIdDialog.h" #include #include TaskIdDialog::TaskIdDialog(TaskModelInterface *model, TasksView *parent) : QDialog(parent) , m_model(model) { m_ui.setupUi(this); m_ui.spinBox->setRange(1, 1000*1000*1000); connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); // resize( minimumSize() ); } TaskIdDialog::~TaskIdDialog() { } void TaskIdDialog::setSuggestedId(int id) { m_ui.spinBox->setValue(id); m_ui.spinBox->selectAll(); } void TaskIdDialog::on_spinBox_valueChanged(int value) { const bool taskExists = m_model->taskIdExists(value); m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!taskExists); m_ui.labelExists->setText(taskExists ? tr("(not ok, exists)") : tr("(ok, does not exist)")); } int TaskIdDialog::selectedId() const { return m_ui.spinBox->value(); } QString TaskIdDialog::taskName() const { return m_ui.taskName->text(); } Charm-1.12.0/Charm/Widgets/TaskIdDialog.h000066400000000000000000000026751331066577000177570ustar00rootroot00000000000000/* TaskIdDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TASKIDDIALOG_H #define TASKIDDIALOG_H #include #include "TasksView.h" #include "Core/TaskModelInterface.h" #include "ui_TaskIdDialog.h" /** * Dialog shown when creating a task */ class TaskIdDialog : public QDialog { Q_OBJECT public: explicit TaskIdDialog(TaskModelInterface *model, TasksView *parent); ~TaskIdDialog() override; void setSuggestedId(int); int selectedId() const; QString taskName() const; private Q_SLOTS: void on_spinBox_valueChanged(int); private: Ui::TaskIdDialog m_ui; TaskModelInterface *m_model; }; #endif Charm-1.12.0/Charm/Widgets/TaskIdDialog.ui000066400000000000000000000054571331066577000201460ustar00rootroot00000000000000 TaskIdDialog 0 0 418 270 Create Task 0 0 Please choose a task ID number. Task ID numbers have to be unique so you cannot select one that already exists. Qt::AlignJustify|Qt::AlignTop true Qt::Horizontal 40 20 0 0 Qt::Horizontal 40 20 (does not exist) Qt::AlignCenter Task name: QDialogButtonBox::Cancel|QDialogButtonBox::Ok labelExists label taskName label_3 buttonBox Charm-1.12.0/Charm/Widgets/TasksView.cpp000066400000000000000000000356061331066577000177330ustar00rootroot00000000000000/* TasksView.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TasksView.h" #include "ApplicationCore.h" #include "Data.h" #include "GUIState.h" #include "MessageBox.h" #include "TaskEditor.h" #include "TaskIdDialog.h" #include "TasksViewDelegate.h" #include "ViewFilter.h" #include "ViewHelpers.h" #include "WidgetUtils.h" #include "Commands/CommandAddTask.h" #include "Commands/CommandDeleteTask.h" #include "Commands/CommandModifyTask.h" #include "Commands/CommandRelayCommand.h" #include "Core/CharmCommand.h" #include "Core/CharmConstants.h" #include "Core/State.h" #include "Core/Task.h" #include #include #include #include #include #include #include #include #include TasksView::TasksView(QWidget *parent) : QDialog(parent) , m_toolBar(new QToolBar(this)) , m_delegate(new TasksViewDelegate(this)) , m_actionNewTask(this) , m_actionNewSubTask(this) , m_actionEditTask(this) , m_actionDeleteTask(this) , m_actionExpandTree(this) , m_actionCollapseTree(this) , m_showCurrentOnly(new QAction(m_toolBar)) , m_showSubscribedOnly(new QAction(m_toolBar)) , m_treeView(new QTreeView(this)) { setWindowTitle(tr("Tasks View")); auto layout = new QVBoxLayout(this); layout->setMargin(0); layout->setSpacing(0); layout->addWidget(m_toolBar); layout->addWidget(m_treeView); m_treeView->setItemDelegate(m_delegate); connect(m_delegate, &TasksViewDelegate::editingStateChanged, this, &TasksView::configureUi); // set up actions m_actionNewTask.setText(tr("New &Task")); m_actionNewTask.setShortcut(QKeySequence::New); m_actionNewTask.setIcon(Data::newTaskIcon()); m_toolBar->addAction(&m_actionNewTask); connect(&m_actionNewTask, &QAction::triggered, this, &TasksView::actionNewTask); m_actionNewSubTask.setText(tr("New &Subtask")); m_actionNewSubTask.setShortcut(Qt::META + Qt::Key_N); m_actionNewSubTask.setIcon(Data::newSubtaskIcon()); m_toolBar->addAction(&m_actionNewSubTask); connect(&m_actionNewSubTask, &QAction::triggered, this, &TasksView::actionNewSubTask); m_actionEditTask.setText(tr("Edit Task")); m_actionEditTask.setShortcut(Qt::CTRL + Qt::Key_E); m_actionEditTask.setIcon(Data::editTaskIcon()); m_toolBar->addAction(&m_actionEditTask); connect(&m_actionEditTask, &QAction::triggered, this, &TasksView::actionEditTask); m_actionDeleteTask.setText(tr("Delete Task")); QList deleteShortcuts; deleteShortcuts << QKeySequence::Delete; #ifdef Q_OS_OSX deleteShortcuts << Qt::Key_Backspace; #endif m_actionDeleteTask.setShortcuts(deleteShortcuts); m_actionDeleteTask.setIcon(Data::deleteTaskIcon()); m_toolBar->addAction(&m_actionDeleteTask); connect(&m_actionDeleteTask, &QAction::triggered, this, &TasksView::actionDeleteTask); m_actionExpandTree.setText(tr("Expand All")); connect(&m_actionExpandTree, &QAction::triggered, m_treeView, &QTreeView::expandAll); m_actionCollapseTree.setText(tr("Collapse All")); connect(&m_actionCollapseTree, &QAction::triggered, m_treeView, &QTreeView::collapseAll); // filter setup m_showCurrentOnly->setText(tr("Current")); m_showCurrentOnly->setCheckable(true); m_toolBar->addAction(m_showCurrentOnly); connect(m_showCurrentOnly, &QAction::triggered, this, &TasksView::taskPrefilteringChanged); m_showSubscribedOnly->setText(tr("Selected")); m_showSubscribedOnly->setCheckable(true); m_toolBar->addAction(m_showSubscribedOnly); connect(m_showSubscribedOnly, &QAction::triggered, this, &TasksView::taskPrefilteringChanged); auto searchField = new QLineEdit(this); connect(searchField, &QLineEdit::textChanged, this, &TasksView::slotFiltertextChanged); m_toolBar->addWidget(searchField); m_treeView->setEditTriggers(QAbstractItemView::EditKeyPressed); m_treeView->setExpandsOnDoubleClick(false); m_treeView->setAlternatingRowColors(true); // The delegate does its own eliding. m_treeView->setTextElideMode(Qt::ElideNone); m_treeView->setRootIsDecorated(true); m_treeView->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_treeView, &QTreeView::customContextMenuRequested, this, &TasksView::slotContextMenuRequested); // A reasonable default depth. m_treeView->expandToDepth(0); } TasksView::~TasksView() { } void TasksView::populateEditMenu(QMenu *editMenu) { editMenu->addAction(&m_actionNewTask); editMenu->addAction(&m_actionNewSubTask); editMenu->addAction(&m_actionEditTask); editMenu->addAction(&m_actionDeleteTask); editMenu->addSeparator(); editMenu->addAction(&m_actionExpandTree); editMenu->addAction(&m_actionCollapseTree); } void TasksView::actionNewTask() { addTaskHelper(Task()); } void TasksView::actionNewSubTask() { Task task = selectedTask(); Q_ASSERT(task.isValid()); addTaskHelper(task); } void TasksView::actionEditTask() { Task task = selectedTask(); Q_ASSERT(task.isValid()); TaskEditor editor(this); editor.setTask(task); if (editor.exec()) { const Task newTask = editor.getTask(); newTask.dump(); auto cmd = new CommandModifyTask(newTask, this); emit emitCommand(cmd); } configureUi(); // FIXME needed? } void TasksView::actionDeleteTask() { Task task = selectedTask(); if (MessageBox::warning(this, tr("Delete Task?"), tr("Do you really want to delete this task?\n" "Warning: All events for this task will be deleted as well!\n" "This operation cannot be undone."), tr("Delete"), tr("Cancel")) == QMessageBox::Yes) { auto cmd = new CommandDeleteTask(task, this); emit emitCommand(cmd); } } void TasksView::addTaskHelper(const Task &parent) { ViewFilter *filter = ApplicationCore::instance().model().taskModel(); Task task; int suggestedId = parent.isValid() ? parent.id() : 1; if (parent.isValid()) { task.setParent(parent.id()); // subscribe if the parent is subscribed: task.setSubscribed( parent.subscribed() || CONFIGURATION.taskPrefilteringMode == Configuration::TaskPrefilter_SubscribedOnly || CONFIGURATION.taskPrefilteringMode == Configuration::TaskPrefilter_SubscribedAndCurrentOnly); } // yeah, daredevil! while (filter->taskIdExists(suggestedId)) ++suggestedId; TaskIdDialog dialog(filter, this); dialog.setSuggestedId(suggestedId); if (dialog.exec()) { task.setId(dialog.selectedId()); task.setName(dialog.taskName()); auto cmd = new CommandAddTask(task, this); emit emitCommand(cmd); if (parent.isValid()) { const QModelIndex parentIdx = filter->indexForTaskId(parent.id()); m_treeView->setExpanded(parentIdx, true); } } } void TasksView::configureUi() { const QItemSelectionModel *smodel = m_treeView->selectionModel(); const QModelIndex current = smodel ? smodel->currentIndex() : QModelIndex(); const ViewFilter *filter = ApplicationCore::instance().model().taskModel(); const bool selected = smodel ? smodel->hasSelection() : false; const Task task = selected ? filter->taskForIndex(current) : Task(); const bool hasChildren = filter->taskHasChildren(task); m_actionDeleteTask.setEnabled(selected && !hasChildren); m_actionEditTask.setEnabled(selected); m_actionNewSubTask.setEnabled(selected); } void TasksView::stateChanged(State) { switch (ApplicationCore::instance().state()) { case Connecting: { // set model on view: ViewFilter *filter = ApplicationCore::instance().model().taskModel(); m_treeView->setModel(filter); const QItemSelectionModel *smodel = m_treeView->selectionModel(); connect(smodel, &QItemSelectionModel::currentChanged, this, &TasksView::configureUi); connect(smodel, &QItemSelectionModel::selectionChanged, this, &TasksView::configureUi); connect(smodel, &QItemSelectionModel::currentColumnChanged, this, &TasksView::configureUi); // due to multiple inheritence we can't use the new style connects here connect(filter, SIGNAL(eventActivationNotice(EventId)), this, SLOT(slotEventActivated(EventId))); connect(filter, SIGNAL(eventDeactivationNotice(EventId)), this, SLOT(slotEventDeactivated(EventId))); connect(MODEL.charmDataModel(), &CharmDataModel::resetGUIState, this, &TasksView::restoreGuiState); configurationChanged(); break; } case Connected: //the model is populated when entering Connected, so delay state restore QMetaObject::invokeMethod(this, "restoreGuiState", Qt::QueuedConnection); configurationChanged(); break; case Disconnecting: saveGuiState(); break; case ShuttingDown: case Dead: default: break; } } void TasksView::saveGuiState() { WidgetUtils::saveGeometry(this, MetaKey_TaskEditorGeometry); Q_ASSERT(m_treeView); ViewFilter *filter = ApplicationCore::instance().model().taskModel(); Q_ASSERT(filter); QSettings settings; // save user settings if (ApplicationCore::instance().state() == Connected || ApplicationCore::instance().state() == Disconnecting) { GUIState state; // selected task state.setSelectedTask(selectedTask().id()); // expanded tasks TaskList tasks = MODEL.charmDataModel()->getAllTasks(); TaskIdList expandedTasks; Q_FOREACH (const Task &task, tasks) { QModelIndex index(filter->indexForTaskId(task.id())); if (m_treeView->isExpanded(index)) expandedTasks << task.id(); } state.setExpandedTasks(expandedTasks); state.saveTo(settings); } } void TasksView::restoreGuiState() { WidgetUtils::restoreGeometry(this, MetaKey_TaskEditorGeometry); Q_ASSERT(m_treeView); ViewFilter *filter = ApplicationCore::instance().model().taskModel(); Q_ASSERT(filter); QSettings settings; // restore user settings, but only when we are connected // (otherwise, we do not have any user data): if (ApplicationCore::instance().state() == Connected) { GUIState state; state.loadFrom(settings); QModelIndex index(filter->indexForTaskId(state.selectedTask())); if (index.isValid()) m_treeView->setCurrentIndex(index); Q_FOREACH (const TaskId &id, state.expandedTasks()) { QModelIndex indexForId(filter->indexForTaskId(id)); if (indexForId.isValid()) m_treeView->expand(indexForId); } } } void TasksView::configurationChanged() { const Configuration::TaskPrefilteringMode mode = CONFIGURATION.taskPrefilteringMode; const bool showSubscribedOnly = mode == Configuration::TaskPrefilter_SubscribedOnly || mode == Configuration::TaskPrefilter_SubscribedAndCurrentOnly; const bool showCurrentOnly = mode == Configuration::TaskPrefilter_CurrentOnly || mode == Configuration::TaskPrefilter_SubscribedAndCurrentOnly; m_showSubscribedOnly->setChecked(showSubscribedOnly); m_showCurrentOnly->setChecked(showCurrentOnly); m_treeView->header()->hide(); configureUi(); WidgetUtils::updateToolButtonStyle(this); } void TasksView::slotFiltertextChanged(const QString &filtertextRaw) { ViewFilter *filter = ApplicationCore::instance().model().taskModel(); QString filtertext = filtertextRaw.simplified(); filtertext.replace(QLatin1Char(' '), QLatin1Char('*')); saveGuiState(); filter->setFilterWildcard(filtertext); filter->setFilterRole(TasksViewRole_Filter); if (!filtertextRaw.isEmpty()) { m_treeView->expandAll(); } else { m_treeView->expandToDepth(0); } restoreGuiState(); } void TasksView::taskPrefilteringChanged() { // find out about the selected mode: Configuration::TaskPrefilteringMode mode; const bool showCurrentOnly = m_showCurrentOnly->isChecked(); const bool showSubscribedOnly = m_showSubscribedOnly->isChecked(); if (showCurrentOnly && showSubscribedOnly) { mode = Configuration::TaskPrefilter_SubscribedAndCurrentOnly; } else if (showCurrentOnly && !showSubscribedOnly) { mode = Configuration::TaskPrefilter_CurrentOnly; } else if (!showCurrentOnly && showSubscribedOnly) { mode = Configuration::TaskPrefilter_SubscribedOnly; } else { mode = Configuration::TaskPrefilter_ShowAll; } ViewFilter *filter = ApplicationCore::instance().model().taskModel(); CONFIGURATION.taskPrefilteringMode = mode; filter->prefilteringModeChanged(); emit saveConfiguration(); } void TasksView::slotContextMenuRequested(const QPoint &point) { // prepare the menu: QMenu menu(m_treeView); menu.addAction(&m_actionNewTask); menu.addAction(&m_actionNewSubTask); menu.addAction(&m_actionEditTask); menu.addAction(&m_actionDeleteTask); menu.addSeparator(); menu.addAction(&m_actionExpandTree); menu.addAction(&m_actionCollapseTree); configureUi(); menu.exec(m_treeView->mapToGlobal(point)); } void TasksView::commitCommand(CharmCommand *command) { command->finalize(); } void TasksView::slotEventActivated(EventId) { configureUi(); } void TasksView::slotEventDeactivated(EventId) { configureUi(); } Task TasksView::selectedTask() { Q_ASSERT(m_treeView); ViewFilter *filter = ApplicationCore::instance().model().taskModel(); Q_ASSERT(filter); // find current selection QModelIndexList selection = m_treeView->selectionModel()->selectedIndexes(); // match it to a task: if (selection.size() > 0) { return filter->taskForIndex(selection.first()); } else { return Task(); } } Charm-1.12.0/Charm/Widgets/TasksView.h000066400000000000000000000052761331066577000174000ustar00rootroot00000000000000/* TasksView.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TASKSVIEW_H #define TASKSVIEW_H #include #include #include #include #include #include #include class QMenu; class TasksViewDelegate; class QToolBar; class QTreeView; class TasksView : public QDialog, public UIStateInterface { Q_OBJECT public: explicit TasksView (QWidget *parent = nullptr); ~TasksView() override; void populateEditMenu(QMenu *); public Q_SLOTS: void commitCommand(CharmCommand *) override; void stateChanged(State previous) override; void configurationChanged() override; void restoreGuiState() override; void saveGuiState() override; Q_SIGNALS: // FIXME connect to MainWindow void saveConfiguration(); void emitCommand(CharmCommand *) override; void emitCommandRollback(CharmCommand *) override; private Q_SLOTS: void actionNewTask(); void actionNewSubTask(); void actionEditTask(); void actionDeleteTask(); void slotFiltertextChanged(const QString &filtertext); void taskPrefilteringChanged(); void slotContextMenuRequested(const QPoint &); void slotEventActivated(EventId); void slotEventDeactivated(EventId); // this method is called every time the UI actions need update, for // example when the current index changes: void configureUi(); private: // helper to retrieve selected task: Task selectedTask(); void addTaskHelper(const Task &parent); QToolBar *m_toolBar; TasksViewDelegate *m_delegate; QAction m_actionNewTask; QAction m_actionNewSubTask; QAction m_actionEditTask; QAction m_actionDeleteTask; QAction m_actionExpandTree; QAction m_actionCollapseTree; QAction *m_showCurrentOnly; QAction *m_showSubscribedOnly; QTreeView *m_treeView; }; #endif Charm-1.12.0/Charm/Widgets/TasksViewDelegate.cpp000066400000000000000000000157401331066577000213630ustar00rootroot00000000000000/* TasksViewDelegate.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TasksViewDelegate.h" #include "TaskModelAdapter.h" #include "ViewHelpers.h" #include #include #include TasksViewDelegate::TasksViewDelegate(QObject *parent) : QItemDelegate(parent) { connect(this, &TasksViewDelegate::closeEditor, this, &TasksViewDelegate::slotCloseEditor); } QWidget *TasksViewDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { QWidget *result = QItemDelegate::createEditor(parent, option, index); m_editing = true; emit editingStateChanged(); return result; } bool TasksViewDelegate::isEditing() const { return m_editing; } void TasksViewDelegate::slotCloseEditor(QWidget *, QAbstractItemDelegate::EndEditHint) { m_editing = false; emit editingStateChanged(); } void TasksViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { painter->save(); // Nasty QTreeView clips the painting to the editor geometry! // We don't want that.... painter->setClipRect(option.rect); drawBackground(painter, option, index); const QVariant checkStateVariant = index.data(Qt::CheckStateRole); const Qt::CheckState checkState = static_cast(checkStateVariant.toInt()); Layout layout = doLayout(option, index); const QRect textRect(option.rect.left(), option.rect.top(), option.rect.width() - layout.cbRect.width(), layout.height); // Prepare QStyleOptionViewItem with the wanted alignments QStyleOptionViewItem modifiedOption = option; modifiedOption.displayAlignment = Qt::AlignLeft | Qt::AlignVCenter; modifiedOption.decorationAlignment = Qt::AlignLeft | Qt::AlignVCenter; // Draw text (task id+name) const QString taskName = index.data(Qt::DisplayRole).toString(); QString elidedTask = Charm::elidedTaskName(taskName, painter->font(), textRect.width()); drawDisplay(painter, modifiedOption, textRect, elidedTask); // Draw checkbox drawCheck(painter, option, layout.cbRect, checkState); painter->restore(); } QSize TasksViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { Layout layout = doLayout(option, index); return QSize(option.rect.width(), layout.height); } bool TasksViewDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) { Q_ASSERT(event); Q_ASSERT(model); // make sure that the item is checkable Qt::ItemFlags flags = model->flags(index); if (!(flags &Qt::ItemIsUserCheckable) || !(option.state & QStyle::State_Enabled) || !(flags & Qt::ItemIsEnabled)) return false; // make sure that we have a check state QVariant value = index.data(Qt::CheckStateRole); if (!value.isValid()) return false; // make sure that we have the right event type if ((event->type() == QEvent::MouseButtonRelease) || (event->type() == QEvent::MouseButtonDblClick)) { const QRect checkRect = checkBoxRect(option, Qt::Checked); QMouseEvent *me = static_cast(event); if (me->button() != Qt::LeftButton || !checkRect.contains(me->pos())) return false; // eat the double click events inside the check rect if (event->type() == QEvent::MouseButtonDblClick) return true; } else if (event->type() == QEvent::KeyPress) { if (static_cast(event)->key() != Qt::Key_Space && static_cast(event)->key() != Qt::Key_Select) return false; } else { return false; } Qt::CheckState state = (static_cast(value.toInt()) == Qt::Checked ? Qt::Unchecked : Qt::Checked); return model->setData(index, state, Qt::CheckStateRole); } QRect TasksViewDelegate::checkBoxRect(const QStyleOptionViewItem &option, const QVariant &variant) const { const QRect bounding = option.rect; // TODO adjust if recording const QRect cbRect = doCheck(option, bounding, variant); // Position checkbox on the right, and vertically aligned return QStyle::alignedRect(option.direction, Qt::AlignRight | Qt::AlignVCenter, cbRect.size(), bounding); } void TasksViewDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const { // TODO use doLayout const QRect cbRect = checkBoxRect(option, Qt::Checked); int firstLineHeight = qMax(cbRect.height(), option.fontMetrics.height()); const QVariant decorationVariant = index.data(Qt::DecorationRole); const QPixmap decorationPixmap = decoration(option, decorationVariant); const QString runningTime = index.data(TasksViewRole_RunningTime).toString(); const int left = decorationPixmap.width() + option.fontMetrics.width(runningTime); QRect r = option.rect.translated(left + 5, 0); r.setRight(cbRect.left()); r.setTop(r.top() + firstLineHeight); editor->setGeometry(r); } void TasksViewDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { // Do not reset the comment lineedit to empty every time TaskModelAdapter emits // dataChanged (because of the running time being updated). if (m_editing) return; QItemDelegate::setEditorData(editor, index); } TasksViewDelegate::Layout TasksViewDelegate::doLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const { Layout layout; // Find size of checkbox const QVariant checkStateVariant = index.data(Qt::CheckStateRole); layout.cbRect = checkBoxRect(option, checkStateVariant); layout.height = qMax(layout.cbRect.height(), option.fontMetrics.height()); return layout; } Charm-1.12.0/Charm/Widgets/TasksViewDelegate.h000066400000000000000000000045561331066577000210330ustar00rootroot00000000000000/* TasksViewDelegate.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TASKSVIEWDELEGATE_H #define TASKSVIEWDELEGATE_H #include /** * Delegate for the tasks view and for the "select task" dialog. */ class TasksViewDelegate : public QItemDelegate { Q_OBJECT public: explicit TasksViewDelegate(QObject *parent = nullptr); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; void setEditorData(QWidget *editor, const QModelIndex &index) const override; bool isEditing() const; Q_SIGNALS: void editingStateChanged() const; private Q_SLOTS: void slotCloseEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint); private: QRect checkBoxRect(const QStyleOptionViewItem &option, const QVariant &variant) const; struct Layout { int height; QRect cbRect; }; Layout doLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const; mutable bool m_editing = false; }; #endif Charm-1.12.0/Charm/Widgets/TimeTrackingTaskSelector.cpp000066400000000000000000000241501331066577000227100ustar00rootroot00000000000000/* TimeTrackingTaskSelector.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld Author: Montel Laurent This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TimeTrackingTaskSelector.h" #include "CommentEditorPopup.h" #include "Data.h" #include "SelectTaskDialog.h" #include "ViewHelpers.h" #include "Core/Configuration.h" #include "Core/Event.h" #include "Core/Task.h" #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #include #include #include #endif namespace { const static char CUSTOM_TASK_PROPERTY_NAME[] = "CUSTOM_TASK_PROPERTY"; static QString escapeAmpersands(QString text) { text.replace(QLatin1String("&"), QLatin1String("&&")); return text; } } TimeTrackingTaskSelector::TimeTrackingTaskSelector(QWidget *parent) : QWidget(parent) , m_stopGoButton(new QToolButton(this)) , m_stopGoAction(new QAction(this)) , m_editCommentButton(new QToolButton(this)) , m_editCommentAction(new QAction(this)) , m_taskSelectorButton(new QToolButton(this)) , m_startOtherTaskAction(new QAction(tr("Start Other Task..."), this)) , m_menu(new QMenu(tr("Start Task"), this)) { QLayout *hbox = new QHBoxLayout(this); hbox->addWidget(m_stopGoButton); hbox->addWidget(m_editCommentButton); hbox->addWidget(m_taskSelectorButton); hbox->setSpacing(1); hbox->setContentsMargins(0, 0, 0, 0); m_taskSelectorButton->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_stopGoAction->setText(tr("Start Task")); m_stopGoAction->setIcon(Data::goIcon()); m_stopGoAction->setShortcut(QKeySequence(Qt::Key_Space)); m_stopGoAction->setCheckable(true); connect(m_stopGoAction, &QAction::triggered, this, &TimeTrackingTaskSelector::slotGoStopToggled); m_stopGoButton->setDefaultAction(m_stopGoAction); m_editCommentAction->setText(tr("Edit Comment")); m_editCommentAction->setIcon(Data::editEventIcon()); m_editCommentAction->setShortcut(Qt::Key_E); m_editCommentAction->setToolTip(m_editCommentAction->text()); connect(m_editCommentAction, &QAction::triggered, this, &TimeTrackingTaskSelector::slotEditCommentClicked); m_editCommentButton->setDefaultAction(m_editCommentAction); m_taskSelectorButton->setPopupMode(QToolButton::InstantPopup); m_taskSelectorButton->setMenu(m_menu); m_taskSelectorButton->setText(tr("Select Task")); m_startOtherTaskAction->setShortcut(Qt::Key_T); connect(m_startOtherTaskAction, &QAction::triggered, this, &TimeTrackingTaskSelector::slotManuallySelectTask); } void TimeTrackingTaskSelector::populateEditMenu(QMenu *menu) { menu->addAction(m_stopGoAction); menu->addAction(m_editCommentAction); menu->addAction(m_startOtherTaskAction); } QMenu *TimeTrackingTaskSelector::menu() const { return m_menu; } void TimeTrackingTaskSelector::populate(const QVector &summaries) { Q_UNUSED(summaries); // Don't repopulate while the menu is displayed; very ugly and it can wait. if (m_menu->isActiveWindow()) return; const auto createTaskAction = [this](TaskId id) { auto action = new QAction(DATAMODEL->taskIdAndSmartNameString(id), m_menu); action->setProperty(CUSTOM_TASK_PROPERTY_NAME, QVariant::fromValue(id)); connect(action, &QAction::triggered, this, &TimeTrackingTaskSelector::slotActionSelected); return action; }; if (m_taskManuallySelected) { m_taskManuallySelected = false; createTaskAction(m_manuallySelectedTask)->trigger(); return; } m_menu->clear(); const TaskIdList interestingTasksToAdd = DATAMODEL->mostRecentlyUsedTasks(); const int maxEntries = qMin(interestingTasksToAdd.size(), CONFIGURATION.numberOfTaskSelectorEntries); for (int i = 0; i < maxEntries; ++i) m_menu->addAction(createTaskAction(interestingTasksToAdd.at(i))); m_menu->addSeparator(); m_menu->addAction(m_startOtherTaskAction); m_taskSelectorButton->setDisabled(m_menu->actions().isEmpty()); } void TimeTrackingTaskSelector::slotEditCommentClicked() { const EventIdList events = DATAMODEL->activeEvents(); Q_ASSERT(events.size() == 1); CommentEditorPopup popup; popup.loadEvent(events.first()); popup.exec(); } void TimeTrackingTaskSelector::handleActiveEvents() { const int activeEventCount = DATAMODEL->activeEventCount(); if (activeEventCount > 1) { m_stopGoAction->setIcon(Data::goIcon()); m_stopGoAction->setText(tr("Start Task")); m_stopGoAction->setEnabled(false); m_stopGoAction->setChecked(true); m_editCommentAction->setEnabled(false); } else if (activeEventCount == 1) { m_stopGoAction->setIcon(Data::stopIcon()); m_stopGoAction->setText(tr("Stop Task")); m_stopGoAction->setEnabled(true); m_stopGoAction->setChecked(true); m_editCommentAction->setEnabled(true); const Event &event = DATAMODEL->eventForId(DATAMODEL->activeEvents().first()); const Task &task = DATAMODEL->getTask(event.taskId()); m_taskSelectorButton->setText(escapeAmpersands(DATAMODEL->taskIdAndSmartNameString( task.id()))); } else { m_stopGoAction->setIcon(Data::goIcon()); m_stopGoAction->setText(tr("Start Task")); if (m_selectedTask != 0) { const Task &task = DATAMODEL->getTask(m_selectedTask); m_stopGoAction->setEnabled(task.isCurrentlyValid()); } else { m_stopGoAction->setEnabled(false); } m_stopGoAction->setChecked(false); m_editCommentAction->setEnabled(false); } updateThumbBar(); } void TimeTrackingTaskSelector::slotActionSelected() { auto action = qobject_cast(sender()); const TaskId taskId = action->property(CUSTOM_TASK_PROPERTY_NAME).value(); const Task &task = DATAMODEL->getTask(taskId); Q_ASSERT(task.isValid()); if (!task.isValid()) return; const bool expired = !task.isCurrentlyValid(); const bool trackable = task.trackable(); if (!trackable || expired) { const bool notTrackableAndExpired = (!trackable && expired); const QString expirationDate = QLocale::system().toString(task.validUntil().date(), QLocale::ShortFormat); const auto taskName = DATAMODEL->taskIdAndSmartNameString(task.id()); const auto reason = notTrackableAndExpired ? tr("The task is not trackable and expired since %1.").arg(expirationDate) : expired ? tr("The task is expired since %1").arg(expirationDate) : tr("The task is not trackable"); const auto message = tr("Cannot select task %1: %2. Please choose another task.").arg(taskName.toHtmlEscaped(), reason); QMessageBox::information(this, tr("Please choose another task"), message); return; } taskSelected(taskId); handleActiveEvents(); if (!DATAMODEL->isTaskActive(taskId)) { if (!DATAMODEL->activeEvents().isEmpty()) emit stopEvents(); emit startEvent(taskId); } } void TimeTrackingTaskSelector::updateThumbBar() { #ifdef Q_OS_WIN if (!m_stopGoThumbButton && window()->windowHandle()) { QWinThumbnailToolBar *toolBar = new QWinThumbnailToolBar(this); toolBar->setWindow(window()->windowHandle()); m_stopGoThumbButton = new QWinThumbnailToolButton(toolBar); toolBar->addButton(m_stopGoThumbButton); connect(m_stopGoThumbButton, &QWinThumbnailToolButton::clicked, [this](){ slotGoStopToggled(!m_stopGoButton->isChecked()); }); } if (m_stopGoThumbButton) { if (m_stopGoButton->isChecked()) { m_stopGoThumbButton->setToolTip(tr("Stop Task")); m_stopGoThumbButton->setIcon(Data::stopIcon()); } else { m_stopGoThumbButton->setToolTip(tr("Start Task")); m_stopGoThumbButton->setIcon(Data::goIcon()); } m_stopGoThumbButton->setEnabled(m_stopGoButton->isEnabled()); } #endif } void TimeTrackingTaskSelector::taskSelected(TaskId id) { m_selectedTask = id; m_stopGoAction->setEnabled(true); const auto taskname = DATAMODEL->taskIdAndSmartNameString(id); m_taskSelectorButton->setText(escapeAmpersands(taskname)); } void TimeTrackingTaskSelector::slotGoStopToggled(bool on) { if (on) { Q_ASSERT(m_selectedTask); emit startEvent(m_selectedTask); } else { emit stopEvents(); } } void TimeTrackingTaskSelector::taskSelected(const WeeklySummary &summary) { taskSelected(summary.task); } void TimeTrackingTaskSelector::slotManuallySelectTask() { SelectTaskDialog dialog(this); if (!dialog.exec()) return; m_manuallySelectedTask = dialog.selectedTask(); if (m_selectedTask <= 0) m_selectedTask = m_manuallySelectedTask; m_taskManuallySelected = true; handleActiveEvents(); emit updateSummariesPlease(); } void TimeTrackingTaskSelector::showEvent(QShowEvent *e) { updateThumbBar(); QWidget::showEvent(e); } Charm-1.12.0/Charm/Widgets/TimeTrackingTaskSelector.h000066400000000000000000000052121331066577000223530ustar00rootroot00000000000000/* TimeTrackingTaskSelector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TIMETRACKINGTASKSELECTOR_H #define TIMETRACKINGTASKSELECTOR_H #include #include #include #include "Core/Event.h" #include "Core/Task.h" #include "WeeklySummary.h" class QAction; class QMenu; class QToolButton; class QTextEdit; class QToolBar; class QWinThumbnailToolButton; class TimeTrackingTaskSelector : public QWidget { Q_OBJECT public: explicit TimeTrackingTaskSelector(QWidget *parent = nullptr); void populate(const QVector &summaries); void handleActiveEvents(); void taskSelected(const WeeklySummary &); QMenu *menu() const; void populateEditMenu(QMenu *); Q_SIGNALS: void startEvent(TaskId); void stopEvents(); void updateSummariesPlease(); private Q_SLOTS: void slotActionSelected(); void slotGoStopToggled(bool); void slotEditCommentClicked(); void slotManuallySelectTask(); protected: void showEvent(QShowEvent *) override; private: void updateThumbBar(); void taskSelected(TaskId id); QToolButton *m_stopGoButton; QAction *m_stopGoAction; QToolButton *m_editCommentButton; QAction *m_editCommentAction; QToolButton *m_taskSelectorButton; QAction *m_startOtherTaskAction; QMenu *m_menu; /** The task that has been selected from the menu. */ TaskId m_selectedTask = {}; /** If the user selected a task through the "Select other task..." menu action, its Id is stored here. */ TaskId m_manuallySelectedTask = {}; /** Temporarily store that a task has been manually selected, so that it can be activated in the menu once after selection. */ bool m_taskManuallySelected = false; #ifdef Q_OS_WIN QWinThumbnailToolButton *m_stopGoThumbButton = nullptr; #endif }; #endif // TIMETRACKINGTASKSELECTOR_H Charm-1.12.0/Charm/Widgets/TimeTrackingView.cpp000066400000000000000000000400471331066577000212220ustar00rootroot00000000000000/* TimeTrackingView.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TimeTrackingView.h" #include "Data.h" #include "ViewHelpers.h" #include "Core/CharmConstants.h" #include "Core/Configuration.h" #include #include #include #include #include #include #include #include const int Margin = 2; TimeTrackingView::TimeTrackingView(QWidget *parent) : QWidget(parent) , m_taskSelector(new TimeTrackingTaskSelector(this)) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); // plumbing m_paintAttributes.initialize(palette()); for (int i = 0; i < 7; ++i) m_shortDayNames[i] = QDate::shortDayName(i + 1); connect(m_taskSelector, &TimeTrackingTaskSelector::startEvent, this, &TimeTrackingView::startEvent); connect(m_taskSelector, &TimeTrackingTaskSelector::stopEvents, this, &TimeTrackingView::stopEvents); connect(m_taskSelector, &TimeTrackingTaskSelector::updateSummariesPlease, this, &TimeTrackingView::slotUpdateSummaries); setFocusProxy(m_taskSelector); setFocusPolicy(Qt::StrongFocus); } void TimeTrackingView::populateEditMenu(QMenu *menu) { m_taskSelector->populateEditMenu(menu); } void TimeTrackingView::PaintAttributes::initialize(const QPalette &palette) { headerBrush = palette.mid(); taskBrushEven = palette.light(); taskBrushOdd = palette.midlight(); totalsRowBrush = headerBrush; totalsRowEvenDayBrush = QBrush(taskBrushEven.color().darker(125)); headerEvenDayBrush = totalsRowEvenDayBrush; QColor dimHighlight = palette.highlight().color(); dim = 0.25; dimHighlight.setAlphaF(dim * dimHighlight.alphaF()); runningTaskColor = palette.highlight().color(); } QSize TimeTrackingView::sizeHint() const { if (!m_cachedSizeHint.isValid()) { // the sizeHint is like the minimum size hint, only it allows // for more text in the task name column const QFontMetrics narrowFontMetrics = QFontMetrics(m_narrowFont); const QRect textRect = narrowFontMetrics.boundingRect(tr("moremoremoremoremore")); const int widthHint = minimumSizeHint().width() + textRect.width(); m_cachedSizeHint = QSize(widthHint, minimumSizeHint().height()); } return m_cachedSizeHint; } QSize TimeTrackingView::minimumSizeHint() const { if (!m_cachedMinimumSizeHint.isValid()) { // the header row, task rows, and totals row are all of the same height const QFontMetrics fixedFontMetrics(m_fixedFont); const QFontMetrics narrowFontMetrics(m_narrowFont); const QRect totalsColumnFieldRect( fixedFontMetrics.boundingRect(QStringLiteral("100:00")) .adjusted(0, 0, 2 * Margin, 2 * Margin)); const int dayWidth = fixedFontMetrics.width(QStringLiteral("00:00")) + 2 * Margin; const int fieldHeight = qMax(fixedFontMetrics.lineSpacing(), narrowFontMetrics.lineSpacing()) + 2 * Margin; const QRect taskColumnFieldRect = narrowFontMetrics.boundingRect( tr("KDABStuffngy")) .adjusted(0, 0, 2 * Margin, 2 * Margin); // the tracking row needs to accommodate the task selector widget const QSize taskSelectorSizeHint = m_taskSelector->sizeHint(); const int trackingRowHeight = qMax(fieldHeight, taskSelectorSizeHint.height() + 2 * Margin); const int minimumWidth = totalsColumnFieldRect.width() + 7 * dayWidth + taskColumnFieldRect.width(); const int minimumHeight = (rowCount() - 1) * fieldHeight + trackingRowHeight; m_cachedMinimumSizeHint = QSize(minimumWidth, minimumHeight); m_cachedTotalsFieldRect = QRect(0, 0, totalsColumnFieldRect.width(), fieldHeight); m_cachedDayFieldRect = QRect(0, 0, dayWidth, fieldHeight); } return m_cachedMinimumSizeHint; } QMenu *TimeTrackingView::menu() const { return m_taskSelector->menu(); } void TimeTrackingView::paintEvent(QPaintEvent *e) { m_activeFieldRects.clear(); const int FieldHeight = m_cachedTotalsFieldRect.height(); QPainter painter(this); // all attributes are determined in data(), we just paint the rects: for (int row = 0; row < rowCount() - 1; ++row) { for (int column = 0; column < columnCount(); ++column) { // get the rectangle of the field that will be drawn QRect fieldRect; const int y = row * FieldHeight; if (column == columnCount() - 1) { // totals column fieldRect = QRect(width() - m_cachedTotalsFieldRect.width(), y, m_cachedTotalsFieldRect.width(), FieldHeight); } else if (column == 0) { // task column fieldRect = QRect(0, y, taskColumnWidth(), FieldHeight); } else if (column > 0) { // a task fieldRect = QRect(width() - m_cachedTotalsFieldRect.width() - 8 * m_cachedDayFieldRect.width() + column * m_cachedDayFieldRect.width(), y, m_cachedDayFieldRect.width(), FieldHeight); } // paint the field, if it is in the dirty region if (e->rect().contains(fieldRect)) { DataField field = m_defaultField; data(field, column, row); int alignment = Qt::AlignRight | Qt::AlignVCenter; if (row == 0) { alignment = Qt::AlignCenter | Qt::AlignVCenter; } else if (column == 0 && row < rowCount() - 1) { alignment = Qt::AlignLeft | Qt::AlignVCenter; } if (column == 0) // task column field.text = elidedText(field.text, field.font, fieldRect.width() - 2*Margin); if (field.storeAsActive) m_activeFieldRects << fieldRect; const QRect textRect = fieldRect.adjusted(Margin, Margin, -Margin, -Margin); if (field.hasHighlight) { painter.setBrush(field.highlight); painter.setPen(Qt::NoPen); painter.drawRect(fieldRect); } else { painter.setBrush(field.background); painter.setPen(Qt::NoPen); painter.drawRect(fieldRect); } painter.setPen(palette().text().color()); painter.setFont(field.font); painter.drawText(textRect, alignment, field.text); } } } // paint the tracking row const int top = (rowCount() - 1) * FieldHeight; const QRect fieldRect(0, top, width(), height() - top); if (e->rect().contains(fieldRect)) { DataField field = m_defaultField; data(field, 0, rowCount() - 1); painter.setBrush(field.background); painter.setPen(Qt::NoPen); painter.drawRect(fieldRect); } } void TimeTrackingView::resizeEvent(QResizeEvent *) { sizeHint(); // make sure cached values are updated m_taskSelector->resize(width() - 2*Margin, m_taskSelector->sizeHint().height()); m_taskSelector->move(Margin, height() - Margin - m_taskSelector->height()); m_elidedTexts.clear(); } void TimeTrackingView::mousePressEvent(QMouseEvent *event) { const int position = getSummaryAt(event->pos()); if (position < 0) return; const TaskId id = m_summaries.at(position).task; if (!taskIsValidAndTrackable(id)) return; if (!isTracking()) { if (position >= 0 && position < m_summaries.size()) m_taskSelector->taskSelected(m_summaries.at(position)); } } void TimeTrackingView::mouseDoubleClickEvent(QMouseEvent *event) { // we rely on the mouse press event that was received before the doubleclick! // start tracking const int position = getSummaryAt(event->pos()); if (position < 0) return; const TaskId id = m_summaries.at(position).task; if (!taskIsValidAndTrackable(id)) return; if (!DATAMODEL->isTaskActive(id)) { emit stopEvents(); emit startEvent(id); } else { emit stopEvents(); } } int TimeTrackingView::getSummaryAt(const QPoint &position) { const int left = 0; const int right = taskColumnWidth(); const int fieldIndex = position.y() / m_cachedTotalsFieldRect.height(); const int taskIndex = fieldIndex - 1; if (taskIndex < 0 || taskIndex >= m_summaries.count()) return -1; if (position.x() < left || position.x() > right) return -1; return taskIndex; } int TimeTrackingView::taskColumnWidth() const { return width() - m_cachedTotalsFieldRect.width() - 7 * m_cachedDayFieldRect.width(); } void TimeTrackingView::data(DataField &field, int column, int row) const { const int HeaderRow = 0; const int TotalsRow = rowCount() - 2; const int TrackingRow = rowCount() - 1; const int TaskColumn = 0; const int TotalsColumn = columnCount() - 1; const int Day = column - 1; field.font = m_fixedFont; if (row == HeaderRow) { field.font = m_narrowFont; if (column == TaskColumn) { field.text = tr("Task"); } else if (column == TotalsColumn) { field.text = tr("Total"); } else { field.text = m_shortDayNames[ column - 1 ]; } field.background = (Day % 2) ? m_paintAttributes.headerBrush : m_paintAttributes.headerEvenDayBrush; } else if (row == TotalsRow) { field.background = m_paintAttributes.totalsRowBrush; if (column == TaskColumn) { // field.text = tr( "Total" ); } else if (column == TotalsColumn) { int total = 0; Q_FOREACH (const WeeklySummary &s, m_summaries) total += std::accumulate(s.durations.begin(), s.durations.end(), 0); field.text = hoursAndMinutes(total); } else { int total = 0; Q_FOREACH (const WeeklySummary &s, m_summaries) total += s.durations[Day]; field.text = hoursAndMinutes(total); field.background = (Day % 2) ? m_paintAttributes.totalsRowBrush : m_paintAttributes.totalsRowEvenDayBrush; } } else if (row == TrackingRow) { // we only return one value, the paint method will treat this // column as a special case field.background = m_paintAttributes.taskBrushOdd; // field.text = tr( " 00:45 2345 KDAB/HR/Project Time Bookkeeping" ); } else { // a task row field.background = row % 2 ? m_paintAttributes.taskBrushEven : m_paintAttributes.taskBrushOdd; if (m_summaries.size() > row - 1) { const int index = row - 1; // index into summaries const bool active = DATAMODEL->isTaskActive(m_summaries[index].task); if (active) { field.hasHighlight = true; field.highlight = m_paintAttributes.halfHighlight; } int day = column - 1; if (column == TaskColumn) { field.text = DATAMODEL->taskIdAndSmartNameString(m_summaries[index].task); field.font = m_narrowFont; } else if (column == TotalsColumn) { const QVector &durations = m_summaries[index].durations; const int total = std::accumulate(durations.begin(), durations.end(), 0); field.text = hoursAndMinutes(total); } else { int duration = m_summaries[index].durations[day]; field.text = duration > 0 ? hoursAndMinutes(duration) : QString(); // highlight today as well, with the half highlight: if (day == m_dayOfWeek -1) { field.hasHighlight = true; field.storeAsActive = active; field.highlight = active ? QBrush(m_paintAttributes.runningTaskColor) : m_paintAttributes. halfHighlight; } } } } } void TimeTrackingView::setSummaries(const QVector &summaries) { m_activeFieldRects.clear(); m_summaries = summaries; m_cachedMinimumSizeHint = QSize(); m_cachedSizeHint = QSize(); m_dayOfWeek = QDate::currentDate().dayOfWeek(); m_elidedTexts.clear(); updateGeometry(); update(); // populate menu: m_taskSelector->populate(m_summaries); // FIXME maybe remember last selected task handleActiveEvents(); emit taskMenuChanged(); } bool TimeTrackingView::isTracking() const { return DATAMODEL->activeEventCount() > 0; } void TimeTrackingView::configurationChanged() { m_fixedFont = font(); #ifdef Q_OS_OSX m_fixedFont.setFamily(QStringLiteral("Andale Mono")); m_fixedFont.setPointSize(11); #endif switch (CONFIGURATION.timeTrackerFontSize) { case Configuration::TimeTrackerFont_Small: m_fixedFont.setPointSizeF(0.9 * m_fixedFont.pointSize()); break; case Configuration::TimeTrackerFont_Regular: break; case Configuration::TimeTrackerFont_Large: m_fixedFont.setPointSizeF(1.2 * m_fixedFont.pointSize()); break; } // re-populate menu: m_taskSelector->populate(m_summaries); emit taskMenuChanged(); m_narrowFont = font(); // stay with the desktop m_narrowFont.setPointSize(m_fixedFont.pointSize()); /* invalidate cache and force recalc */ m_cachedSizeHint = QSize(); m_cachedMinimumSizeHint = QSize(); updateGeometry(); sizeHint(); /* force repaint */ repaint(); } void TimeTrackingView::handleActiveEvents() { m_activeFieldRects.clear(); Q_ASSERT(DATAMODEL->activeEventCount() >= 0); m_taskSelector->handleActiveEvents(); } QString TimeTrackingView::elidedText(const QString &text, const QFont &font, int width) { if (!m_elidedTexts.contains(text)) m_elidedTexts.insert(text, Charm::elidedTaskName(text, font, width)); Q_ASSERT(m_elidedTexts.contains(text)); return m_elidedTexts.value(text); } void TimeTrackingView::slotUpdateSummaries() { setSummaries(m_summaries); } bool TimeTrackingView::taskIsValidAndTrackable(int taskId) { const Task &task = DATAMODEL->getTask(taskId); bool expired = !task.isCurrentlyValid(); bool trackable = task.trackable(); bool notTrackableAndExpired = (!trackable && expired); const auto id = QString::number(task.id()); const QString name = task.name(); QString expirationDate = QLocale::system().toString(task.validUntil(), QLocale::ShortFormat); if (!trackable || expired) { QString message = notTrackableAndExpired ? tr( "The task %1 (%2) is not trackable and expired since %3").arg(id, name, expirationDate) : expired ? tr("The task %1 (%2) is expired since %3").arg(id, name, expirationDate) : tr("The task %1 (%2) is not trackable").arg(id, name); QMessageBox::information(this, tr("Please choose another task"), message); return false; } return true; } Charm-1.12.0/Charm/Widgets/TimeTrackingView.h000066400000000000000000000074001331066577000206630ustar00rootroot00000000000000/* TimeTrackingView.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TimeTrackingView_H #define TimeTrackingView_H #include #include #include #include "Core/Task.h" #include "TimeTrackingTaskSelector.h" class QPalette; class QToolBar; class TimeTrackingView : public QWidget { Q_OBJECT private: struct DataField { QString text; QBrush background; bool hasHighlight = false; // QBrush does not have isValid() bool storeAsActive = false; QBrush highlight; QFont font; }; /** A struct to store the attributes used during painting. The initialize function has and this class have been factored out for performance reasonsduring profiling. */ struct PaintAttributes { QBrush headerBrush; QBrush taskBrushEven; QBrush taskBrushOdd; QBrush totalsRowBrush; QBrush totalsRowEvenDayBrush; QBrush headerEvenDayBrush; QBrush halfHighlight; QColor runningTaskColor; float dim; void initialize(const QPalette &palette); }; public: explicit TimeTrackingView(QWidget *parent = nullptr); void paintEvent(QPaintEvent *) override; void resizeEvent(QResizeEvent *) override; void mousePressEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void setSummaries(const QVector &summaries); QSize sizeHint() const override; QSize minimumSizeHint() const override; QMenu *menu() const; void populateEditMenu(QMenu *); void handleActiveEvents(); bool isTracking() const; void configurationChanged(); Q_SIGNALS: void startEvent(TaskId); void stopEvents(); void taskMenuChanged(); private Q_SLOTS: void slotUpdateSummaries(); private: void data(DataField &out, int column, int row) const; int columnCount() const { return 9; } int rowCount() const { return qMax(6, m_summaries.count()) + 3; } int getSummaryAt(const QPoint &position); bool taskIsValidAndTrackable(int taskId); int taskColumnWidth() const; QVector m_summaries; mutable QSize m_cachedSizeHint; mutable QSize m_cachedMinimumSizeHint; mutable QRect m_cachedTotalsFieldRect; mutable QRect m_cachedDayFieldRect; mutable QFont m_fixedFont; mutable QFont m_narrowFont; TimeTrackingTaskSelector *m_taskSelector; QList m_activeFieldRects; PaintAttributes m_paintAttributes; DataField m_defaultField; /** Stored for performance reasons, QDate::currentDate() is expensive. */ int m_dayOfWeek = 0; /** Stored for performance reasons, QDate::shortDayName() is slow on Mac. */ QString m_shortDayNames[7]; /** Stored for performance reasons, QFontMetrics::elidedText is slow if called many times. */ QMap m_elidedTexts; QString elidedText(const QString &text, const QFont &font, int width); }; #endif Charm-1.12.0/Charm/Widgets/TimeTrackingWindow.cpp000066400000000000000000000712421331066577000215600ustar00rootroot00000000000000/* TimeTrackingWindow.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld Author: Mathias Hasselmann This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TimeTrackingWindow.h" #include "ActivityReport.h" #include "ApplicationCore.h" #include "CharmAboutDialog.h" #include "CharmCMake.h" #include "CharmNewReleaseDialog.h" #include "CharmPreferences.h" #include "CommentEditorPopup.h" #include "EnterVacationDialog.h" #include "IdleCorrectionDialog.h" #include "MakeTemporarilyVisible.h" #include "MessageBox.h" #include "MonthlyTimesheet.h" #include "MonthlyTimesheetConfigurationDialog.h" #include "TemporaryValue.h" #include "TimeTrackingView.h" #include "ViewHelpers.h" #include "WeeklyTimesheet.h" #include "Commands/CommandExportToXml.h" #include "Commands/CommandImportFromXml.h" #include "Commands/CommandMakeEvent.h" #include "Commands/CommandModifyEvent.h" #include "Commands/CommandSetAllTasks.h" #include "Core/TaskListMerger.h" #include "Core/TimeSpans.h" #include "Core/XmlSerialization.h" #include "HttpClient/GetProjectCodesJob.h" #include "HttpClient/RestJob.h" #include "HttpClient/UploadTimesheetJob.h" #include "Idle/IdleDetector.h" #include "Lotsofcake/Configuration.h" #include "Reports/WeeklyTimesheetXmlWriter.h" #include "Widgets/HttpJobProgressDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include TimeTrackingWindow::TimeTrackingWindow(QWidget *parent) : CharmWindow(tr("Time Tracker"), parent) , m_summaryWidget(new TimeTrackingView(this)) , m_billDialog(new BillDialog(this)) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); setWindowNumber(3); setWindowIdentifier(QStringLiteral("window_tracking")); setCentralWidget(m_summaryWidget); connect(m_summaryWidget, &TimeTrackingView::startEvent, this, &TimeTrackingWindow::slotStartEvent); connect(m_summaryWidget, &TimeTrackingView::stopEvents, this, &TimeTrackingWindow::slotStopEvent); connect(m_summaryWidget, &TimeTrackingView::taskMenuChanged, this, &TimeTrackingWindow::taskMenuChanged); connect(&m_checkUploadedSheetsTimer, &QTimer::timeout, this, &TimeTrackingWindow::slotCheckUploadedTimesheets); connect(m_billDialog, &BillDialog::finished, this, &TimeTrackingWindow::slotBillGone); connect(&m_checkCharmReleaseVersionTimer, &QTimer::timeout, this, &TimeTrackingWindow::slotCheckForUpdatesAutomatic); connect(&m_updateUserInfoAndTasksDefinitionsTimer, &QTimer::timeout, this, &TimeTrackingWindow::slotGetUserInfo); //Check every 60 minutes if there are timesheets due if (CONFIGURATION.warnUnuploadedTimesheets) m_checkUploadedSheetsTimer.start(); m_checkUploadedSheetsTimer.setInterval(60 * 60 * 1000); #if defined(Q_OS_OSX) || defined(Q_OS_WIN) m_checkCharmReleaseVersionTimer.setInterval(24 * 60 * 60 * 1000); if (!CharmUpdateCheckUrl().isEmpty()) { QTimer::singleShot(1000, this, SLOT(slotCheckForUpdatesAutomatic())); m_checkCharmReleaseVersionTimer.start(); } #endif //Update tasks definitions once every 24h m_updateUserInfoAndTasksDefinitionsTimer.setInterval(24 * 60 * 60 * 1000); QTimer::singleShot(1000, this, SLOT(slotSyncTasksAutomatic())); m_updateUserInfoAndTasksDefinitionsTimer.start(); toolBar()->hide(); } bool TimeTrackingWindow::event(QEvent *event) { if (event->type() == QEvent::LayoutRequest) setMaximumHeight(sizeHint().height()); return CharmWindow::event(event); } void TimeTrackingWindow::showEvent(QShowEvent *e) { CharmWindow::showEvent(e); } QMenu *TimeTrackingWindow::menu() const { return m_summaryWidget->menu(); } TimeTrackingWindow::~TimeTrackingWindow() { if (ApplicationCore::hasInstance()) DATAMODEL->unregisterAdapter(this); } void TimeTrackingWindow::stateChanged(State previous) { CharmWindow::stateChanged(previous); switch (ApplicationCore::instance().state()) { case Connecting: connect(ApplicationCore::instance().dateChangeWatcher(), &DateChangeWatcher::dateChanged, this, &TimeTrackingWindow::slotSelectTasksToShow); DATAMODEL->registerAdapter(this); m_summaryWidget->setSummaries(QVector()); m_summaryWidget->handleActiveEvents(); break; case Disconnecting: case ShuttingDown: default: break; } } void TimeTrackingWindow::restore() { show(); } // model adapter: void TimeTrackingWindow::resetTasks() { slotSelectTasksToShow(); } void TimeTrackingWindow::taskAboutToBeAdded(TaskId, int) { } void TimeTrackingWindow::taskAdded(TaskId) { slotSelectTasksToShow(); } void TimeTrackingWindow::taskModified(TaskId) { slotSelectTasksToShow(); } void TimeTrackingWindow::taskParentChanged(TaskId, TaskId, TaskId) { slotSelectTasksToShow(); } void TimeTrackingWindow::taskAboutToBeDeleted(TaskId) { } void TimeTrackingWindow::taskDeleted(TaskId) { slotSelectTasksToShow(); } void TimeTrackingWindow::resetEvents() { slotSelectTasksToShow(); } void TimeTrackingWindow::eventAboutToBeAdded(EventId) { } void TimeTrackingWindow::eventAdded(EventId) { slotSelectTasksToShow(); } void TimeTrackingWindow::eventModified(EventId, Event) { slotSelectTasksToShow(); } void TimeTrackingWindow::eventAboutToBeDeleted(EventId) { } void TimeTrackingWindow::eventDeleted(EventId) { slotSelectTasksToShow(); } void TimeTrackingWindow::eventActivated(EventId) { m_summaryWidget->handleActiveEvents(); } void TimeTrackingWindow::eventDeactivated(EventId id) { m_summaryWidget->handleActiveEvents(); if (CONFIGURATION.requestEventComment) { Event event = DATAMODEL->eventForId(id); if (event.isValid() && event.comment().isEmpty()) { CommentEditorPopup popup; popup.loadEvent(id); popup.exec(); } } } void TimeTrackingWindow::configurationChanged() { if (CONFIGURATION.warnUnuploadedTimesheets) { m_checkUploadedSheetsTimer.start(); } else { m_checkUploadedSheetsTimer.stop(); } m_summaryWidget->configurationChanged(); CharmWindow::configurationChanged(); } void TimeTrackingWindow::slotSelectTasksToShow() { // we would like to always show some tasks, if there are any // first, we select tasks that most recently where active const NamedTimeSpan thisWeek = TimeSpans().thisWeek(); // and update the widget: m_summaries = WeeklySummary::summariesForTimespan(DATAMODEL, thisWeek.timespan); m_summaryWidget->setSummaries(m_summaries); } void TimeTrackingWindow::insertEditMenu() { QMenu *editMenu = menuBar()->addMenu(tr("Edit")); m_summaryWidget->populateEditMenu(editMenu); } void TimeTrackingWindow::slotStartEvent(TaskId id) { const TaskTreeItem &item = DATAMODEL->taskTreeItem(id); if (item.task().isCurrentlyValid()) { DATAMODEL->startEventRequested(item.task()); } else { QString nm = DATAMODEL->taskIdAndSmartNameString(id); if (item.task().isValid()) QMessageBox::critical(this, tr("Invalid task"), tr("Task '%1' is no longer valid, so can't be started").arg(nm)); else if (id > 0) QMessageBox::critical(this, tr("Invalid task"), tr("Task '%1' does not exist").arg(id)); } ApplicationCore::instance().updateTaskList(); uploadStagedTimesheet(); } void TimeTrackingWindow::slotStopEvent() { DATAMODEL->endAllEventsRequested(); } void TimeTrackingWindow::slotEditPreferences(bool) { MakeTemporarilyVisible m(this); CharmPreferences dialog(CONFIGURATION, this); if (dialog.exec()) { CONFIGURATION.timeTrackerFontSize = dialog.timeTrackerFontSize(); CONFIGURATION.durationFormat = dialog.durationFormat(); CONFIGURATION.toolButtonStyle = dialog.toolButtonStyle(); CONFIGURATION.detectIdling = dialog.detectIdling(); CONFIGURATION.warnUnuploadedTimesheets = dialog.warnUnuploadedTimesheets(); CONFIGURATION.requestEventComment = dialog.requestEventComment(); CONFIGURATION.enableCommandInterface = dialog.enableCommandInterface(); CONFIGURATION.numberOfTaskSelectorEntries = dialog.numberOfTaskSelectorEntries(); emit saveConfiguration(); } } void TimeTrackingWindow::slotAboutDialog() { MakeTemporarilyVisible m(this); CharmAboutDialog dialog(this); dialog.exec(); } void TimeTrackingWindow::slotEnterVacation() { MakeTemporarilyVisible m(this); EnterVacationDialog dialog(this); if (dialog.exec() != QDialog::Accepted) return; const EventList events = dialog.events(); Q_FOREACH (const Event &event, events) { auto command = new CommandMakeEvent(event, this); sendCommand(command); } } void TimeTrackingWindow::slotActivityReport() { delete m_activityReportDialog; m_activityReportDialog = new ActivityReportConfigurationDialog(this); m_activityReportDialog->setAttribute(Qt::WA_DeleteOnClose); connect(m_activityReportDialog, &ActivityReportConfigurationDialog::finished, this, &TimeTrackingWindow::slotActivityReportPreview); m_activityReportDialog->open(); } void TimeTrackingWindow::resetWeeklyTimesheetDialog() { delete m_weeklyTimesheetDialog; m_weeklyTimesheetDialog = new WeeklyTimesheetConfigurationDialog(this); m_weeklyTimesheetDialog->setAttribute(Qt::WA_DeleteOnClose); connect(m_weeklyTimesheetDialog, &WeeklyTimesheetConfigurationDialog::finished, this, &TimeTrackingWindow::slotWeeklyTimesheetPreview); } void TimeTrackingWindow::slotWeeklyTimesheetReport() { resetWeeklyTimesheetDialog(); m_weeklyTimesheetDialog->open(); } void TimeTrackingWindow::resetMonthlyTimesheetDialog() { delete m_monthlyTimesheetDialog; m_monthlyTimesheetDialog = new MonthlyTimesheetConfigurationDialog(this); m_monthlyTimesheetDialog->setAttribute(Qt::WA_DeleteOnClose); connect(m_monthlyTimesheetDialog, &MonthlyTimesheetConfigurationDialog::finished, this, &TimeTrackingWindow::slotMonthlyTimesheetPreview); } void TimeTrackingWindow::slotMonthlyTimesheetReport() { resetMonthlyTimesheetDialog(); m_monthlyTimesheetDialog->open(); } void TimeTrackingWindow::slotWeeklyTimesheetPreview(int result) { showPreview(m_weeklyTimesheetDialog, result); m_weeklyTimesheetDialog = nullptr; } void TimeTrackingWindow::slotMonthlyTimesheetPreview(int result) { showPreview(m_monthlyTimesheetDialog, result); m_monthlyTimesheetDialog = nullptr; } void TimeTrackingWindow::slotActivityReportPreview(int result) { showPreview(m_activityReportDialog, result); m_activityReportDialog = nullptr; } void TimeTrackingWindow::showPreview(ReportConfigurationDialog *dialog, int result) { if (result == QDialog::Accepted) dialog->showReportPreviewDialog(); } void TimeTrackingWindow::slotExportToXml() { MakeTemporarilyVisible m(this); // ask for a filename: QSettings settings; QString path; if (settings.contains(MetaKey_ExportToXmlRecentSavePath)) { path = settings.value(MetaKey_ExportToXmlRecentSavePath).toString(); QDir dir(path); if (!dir.exists()) path = QString(); } QString filename = QFileDialog::getSaveFileName(this, tr("Enter File Name"), path); if (filename.isEmpty()) return; QFileInfo fileinfo(filename); path = fileinfo.absolutePath(); if (!path.isEmpty()) settings.setValue(MetaKey_ExportToXmlRecentSavePath, path); if (fileinfo.suffix().isEmpty()) filename += QLatin1String(".charmdatabaseexport"); // get a XML export: CommandExportToXml *command = new CommandExportToXml(filename, this); sendCommand(command); } void TimeTrackingWindow::slotImportFromXml() { MakeTemporarilyVisible m(this); // ask for the filename: QSettings settings; QString path; if (settings.contains(MetaKey_ExportToXmlRecentSavePath)) { path = settings.value(MetaKey_ExportToXmlRecentSavePath).toString(); QDir dir(path); if (!dir.exists()) path = QString(); } QString filename = QFileDialog::getOpenFileName(this, tr("Please Select File"), path); if (filename.isEmpty()) return; QFileInfo fileinfo(filename); Q_ASSERT(fileinfo.exists()); // warn the user about the consequences: if (MessageBox::warning(this, tr("Watch out!"), tr("During import, all existing tasks and events will be deleted" " and replaced with the imported ones. Are you sure?"), tr("Delete"), tr("Cancel")) != QMessageBox::Yes) return; // ask the controller to import the file: CommandImportFromXml *cmd = new CommandImportFromXml(filename, this); sendCommand(cmd); } void TimeTrackingWindow::slotSyncTasks(VerboseMode mode) { if (ApplicationCore::instance().state() != Connected) return; Lotsofcake::Configuration configuration; auto client = new GetProjectCodesJob(this); client->setUsername(configuration.username()); client->setDownloadUrl(configuration.projectCodeDownloadUrl()); if (mode == Verbose) { HttpJobProgressDialog *dialog = new HttpJobProgressDialog(client, this); dialog->setWindowTitle(tr("Downloading")); } else { client->setVerbose(false); } connect(client, &GetProjectCodesJob::finished, this, &TimeTrackingWindow::slotTasksDownloaded); client->start(); } void TimeTrackingWindow::slotSyncTasksVerbose() { slotSyncTasks(Verbose); } void TimeTrackingWindow::slotSyncTasksAutomatic() { if (Lotsofcake::Configuration().isConfigured()) slotSyncTasks(Silent); } void TimeTrackingWindow::slotTasksDownloaded(HttpJob *job_) { GetProjectCodesJob *job = qobject_cast(job_); Q_ASSERT(job); const bool verbose = job->isVerbose(); if (job->error() == HttpJob::Canceled) return; if (job->error()) { const QString title = tr("Error"); const QString message = tr("Could not download the task list: %1").arg(job->errorString()); if (verbose) { QMessageBox::critical(this, title, message); } else if (job->error() != HttpJob::HostNotFound) { emit showNotification(title, message); } return; } QBuffer buffer; buffer.setData(job->payload()); buffer.open(QIODevice::ReadOnly); importTasksFromDeviceOrFile(&buffer, QString(), verbose); } void TimeTrackingWindow::slotImportTasks() { const QString filename = QFileDialog::getOpenFileName(this, tr( "Please Select File"), QLatin1String(""), tr( "Task definitions (*.xml);;All Files (*)")); if (filename.isNull()) return; importTasksFromDeviceOrFile(0, filename); } void TimeTrackingWindow::slotExportTasks() { const MakeTemporarilyVisible m(this); const QString filename = QFileDialog::getSaveFileName(this, tr( "Please select export filename"), QLatin1String(""), tr( "Task definitions (*.xml);;All Files (*)")); if (filename.isNull()) return; try { const TaskList tasks = DATAMODEL->getAllTasks(); TaskExport::writeTo(filename, tasks); } catch (const XmlSerializationException &e) { const QString message = e.what().isEmpty() ? tr("Error exporting the task definitions!") : tr("There was an error exporting the task definitions:
    %1"). arg(e.what()); QMessageBox::critical(this, tr("Error during export"), message); return; } } void TimeTrackingWindow::slotCheckUploadedTimesheets() { WeeksByYear missing = missingTimeSheets(); if (missing.isEmpty()) return; m_checkUploadedSheetsTimer.stop(); //The usual case is just one missing week, unless we've been giving Bill a hard time //Perhaps in the future Bill can bug us about more than one report at a time Q_ASSERT(!missing.begin().value().isEmpty()); int year = missing.begin().key(); int week = missing.begin().value().first(); m_billDialog->setReport(year, week); m_billDialog->show(); m_billDialog->raise(); m_billDialog->activateWindow(); } void TimeTrackingWindow::slotBillGone(int result) { switch (result) { case BillDialog::AlreadyDone: addUploadedTimesheet(m_billDialog->year(), m_billDialog->week()); break; case BillDialog::AsYouWish: resetWeeklyTimesheetDialog(); m_weeklyTimesheetDialog->setDefaultWeek(m_billDialog->year(), m_billDialog->week()); m_weeklyTimesheetDialog->open(); break; case BillDialog::Later: break; } if (CONFIGURATION.warnUnuploadedTimesheets) m_checkUploadedSheetsTimer.start(); } void TimeTrackingWindow::slotCheckForUpdatesAutomatic() { // do not display message error when auto-checking startCheckForUpdates(); } void TimeTrackingWindow::slotCheckForUpdatesManual() { startCheckForUpdates(Verbose); } void TimeTrackingWindow::startCheckForUpdates(VerboseMode mode) { CheckForUpdatesJob *checkForUpdates = new CheckForUpdatesJob(this); connect(checkForUpdates, &CheckForUpdatesJob::finished, this, &TimeTrackingWindow::slotCheckForUpdates); checkForUpdates->setUrl(QUrl(CharmUpdateCheckUrl())); if (mode == Verbose) checkForUpdates->setVerbose(true); checkForUpdates->start(); } void TimeTrackingWindow::slotCheckForUpdates(CheckForUpdatesJob::JobData data) { const QString errorString = data.errorString; if (data.verbose && (data.error != 0 || !errorString.isEmpty())) { QMessageBox::critical(this, tr("Error"), errorString); return; } const QString releaseVersion = data.releaseVersion; QSettings settings; settings.beginGroup(QStringLiteral("UpdateChecker")); const QString skipVersion = settings.value(QStringLiteral("skip-version")).toString(); if ((skipVersion == releaseVersion) && !data.verbose) return; if (Charm::versionLessThan(CharmVersion(), releaseVersion)) { informUserAboutNewRelease(releaseVersion, data.link, data.releaseInformationLink); } else { if (data.verbose) QMessageBox::information(this, tr("Charm Release"), tr("There is no newer Charm version available!")); } } void TimeTrackingWindow::informUserAboutNewRelease(const QString &releaseVersion, const QUrl &link, const QString &releaseInfoLink) { QString localVersion = CharmVersion(); localVersion.truncate(releaseVersion.length()); CharmNewReleaseDialog dialog(this); dialog.setVersion(releaseVersion, localVersion); dialog.setDownloadLink(link); dialog.setReleaseInformationLink(releaseInfoLink); dialog.exec(); } void TimeTrackingWindow::handleIdleEvents(IdleDetector *detector, bool restart) { EventIdList activeEvents = DATAMODEL->activeEvents(); DATAMODEL->endAllEventsRequested(); // FIXME with this option, we can only change the events to // the start time of one idle period, I chose to use the last // one: const auto periods = detector->idlePeriods(); const IdleDetector::IdlePeriod period = periods.last(); Q_FOREACH (EventId eventId, activeEvents) { Event event = DATAMODEL->eventForId(eventId); if (event.isValid()) { Event old = event; QDateTime start = period.first; // initializes a valid QDateTime event.setEndDateTime(qMax(event.startDateTime(), start)); Q_ASSERT(event.isValid()); auto cmd = new CommandModifyEvent(event, old, this); emit emitCommand(cmd); if (restart) { Task task; task.setId(event.taskId()); if (task.isValid()) DATAMODEL->startEventRequested(task); } } } } void TimeTrackingWindow::maybeIdle(IdleDetector *detector) { Q_ASSERT(detector); Q_ASSERT(!detector->idlePeriods().isEmpty()); if (m_idleCorrectionDialogVisible) return; const TemporaryValue tempValue(m_idleCorrectionDialogVisible, true); // handle idle merging: IdleCorrectionDialog dialog(detector->idlePeriods().last(), this); MakeTemporarilyVisible m(this); dialog.exec(); switch (dialog.result()) { case IdleCorrectionDialog::Idle_Ignore: break; case IdleCorrectionDialog::Idle_EndEvent: { handleIdleEvents(detector, false); break; } case IdleCorrectionDialog::Idle_RestartEvent: { handleIdleEvents(detector, true); break; } default: break; // should not happen } detector->clear(); } void TimeTrackingWindow::importTasksFromDeviceOrFile(QIODevice *device, const QString &filename, bool verbose) { const MakeTemporarilyVisible m(this); Q_UNUSED(m); TaskExport exporter; TaskListMerger merger; try { if (device) { exporter.readFrom(device); } else { exporter.readFrom(filename); } merger.setOldTasks(DATAMODEL->getAllTasks()); merger.setNewTasks(exporter.tasks()); if (merger.modifiedTasks().isEmpty() && merger.addedTasks().isEmpty()) { const QString title = tr("Tasks Import"); const QString message = tr("The selected task file does not contain any updates."); if (verbose) { QMessageBox::information(this, title, message); } else { emit showNotification(title, message); } } else { auto cmd = new CommandSetAllTasks(merger.mergedTaskList(), this); sendCommand(cmd); // At this point the command was finalized and we have a result. const bool success = cmd->finalize(); const QString detailsText = success ? tr("The task list has been updated.") : tr( "Setting the new tasks failed."); const QString title = success ? tr("Tasks Import") : tr("Failure setting new tasks"); if (verbose) { QMessageBox::information(this, title, detailsText); } else if (!success) { emit showNotification(title, detailsText); } } Lotsofcake::Configuration lotsofcakeConfig; const auto oldUserName = lotsofcakeConfig.username(); lotsofcakeConfig.importFromTaskExport(exporter); const auto newUserName = lotsofcakeConfig.username(); ApplicationCore::instance().setHttpActionsVisible(lotsofcakeConfig.isConfigured()); // update user info in case the user name has changed if (!oldUserName.isEmpty() && oldUserName != newUserName) slotGetUserInfo(); } catch (const CharmException &e) { const QString title = tr("Invalid Task Definitions"); const QString message = e.what().isEmpty() ? tr( "The selected task definitions are invalid and cannot be imported.") : tr("There was an error importing the task definitions:
    %1"). arg(e.what()); if (verbose) { QMessageBox::critical(this, title, message); } else { emit showNotification(title, message); } return; } } void TimeTrackingWindow::uploadStagedTimesheet() { try { if (m_uploadingStagedTimesheet) return; const Lotsofcake::Configuration configuration; if (!configuration.isConfigured()) return; const auto today = QDate::currentDate(); const auto lastUpload = configuration.lastStagedTimesheetUpload(); if (lastUpload.isValid() && lastUpload >= today) return; const auto thisWeek = TimeSpans().thisWeek(); const auto weekStart = thisWeek.timespan.first; const auto yesterday = TimeSpans().yesterday().timespan.second; if (yesterday < weekStart) return; int year = 0; const auto weekNumber = today.weekNumber(&year); WeeklyTimesheetXmlWriter timesheet; timesheet.setDataModel(DATAMODEL); timesheet.setYear(year); timesheet.setWeekNumber(weekNumber); timesheet.setIncludeTaskList(false); const auto matchingEventIds = DATAMODEL->eventsThatStartInTimeFrame(weekStart, yesterday); EventList events; events.reserve(matchingEventIds.size()); Q_FOREACH (const EventId &id, matchingEventIds) events.append(DATAMODEL->eventForId(id)); timesheet.setEvents(events); QScopedPointer job(new UploadTimesheetJob); connect(job.data(), &HttpJob::finished, this, [this](HttpJob *job) { m_uploadingStagedTimesheet = false; if (job->error() == HttpJob::NoError) { Lotsofcake::Configuration configuration; configuration.setLastStagedTimesheetUpload(QDate::currentDate()); } }); job->setUsername(configuration.username()); job->setUploadUrl(configuration.timesheetUploadUrl()); job->setStatus(UploadTimesheetJob::Staged); job->setPayload(timesheet.saveToXml()); job.take()->start(); m_uploadingStagedTimesheet = true; } catch (const XmlSerializationException &e) { QMessageBox::critical(this, tr("Error generating the staged timesheet"), e.what()); } } void TimeTrackingWindow::slotGetUserInfo() { Lotsofcake::Configuration configuration; if (!configuration.isConfigured()) return; const auto restUrl = configuration.restUrl(); const auto userName = configuration.username(); auto url = QUrl(restUrl); url.setPath(url.path() + QLatin1String("user")); QUrlQuery query; query.addQueryItem(QLatin1String("user"), userName); url.setQuery(query); auto job = new RestJob(this); job->setUsername(userName); job->setUrl(url); connect(job, &RestJob::finished, this, &TimeTrackingWindow::slotUserInfoDownloaded); job->start(); } void TimeTrackingWindow::slotUserInfoDownloaded(HttpJob *job_) { // getUserInfo done -> sync task slotSyncTasksAutomatic(); auto job = qobject_cast(job_); Q_ASSERT(job); if (job->error() == HttpJob::Canceled) return; if (job->error()) { QMessageBox::critical(this, tr("Error"), tr("Could not download weekly hours: %1").arg(job->errorString())); return; } const auto readData = job->resultData(); QJsonParseError parseError; const auto doc = QJsonDocument::fromJson(readData, &parseError); if (parseError.error != QJsonParseError::NoError) { QMessageBox::critical(this, tr("Error"), tr("Could not parse weekly hours: %1").arg(parseError.errorString())); return; } const auto weeklyHoursValue = doc.object().value(QLatin1String("hrInfo")).toObject().value(QLatin1String( "weeklyHours")); const auto weeklyHours = weeklyHoursValue.isDouble() ? weeklyHoursValue.toDouble() : 40; QSettings settings; settings.beginGroup(QStringLiteral("users")); settings.setValue(QStringLiteral("weeklyhours"), weeklyHours); settings.endGroup(); } Charm-1.12.0/Charm/Widgets/TimeTrackingWindow.h000066400000000000000000000122641331066577000212240ustar00rootroot00000000000000/* TimeTrackingWindow.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TimeTrackingWindow_H #define TimeTrackingWindow_H #include #include "Core/CharmDataModelAdapterInterface.h" #include "Charm/HttpClient/CheckForUpdatesJob.h" #include "CharmWindow.h" #include "Charm/WeeklySummary.h" #include "BillDialog.h" class HttpJob; class CheckForUpdatesJob; class CharmCommand; class TimeTrackingView; class IdleDetector; class ReportConfigurationDialog; class WeeklyTimesheetConfigurationDialog; class MonthlyTimesheetConfigurationDialog; class ActivityReportConfigurationDialog; class TimeTrackingWindow : public CharmWindow, public CharmDataModelAdapterInterface { Q_OBJECT public: explicit TimeTrackingWindow(QWidget *parent = nullptr); ~TimeTrackingWindow() override; enum VerboseMode { Verbose = 0, Silent }; // application: void stateChanged(State previous) override; void restore() override; bool event(QEvent *) override; void showEvent(QShowEvent *) override; QMenu *menu() const; // model adapter: void resetTasks() override; void taskAboutToBeAdded(TaskId parent, int pos) override; void taskAdded(TaskId id) override; void taskModified(TaskId id) override; void taskParentChanged(TaskId task, TaskId oldParent, TaskId newParent) override; void taskAboutToBeDeleted(TaskId) override; void taskDeleted(TaskId id) override; void resetEvents() override; void eventAboutToBeAdded(EventId id) override; void eventAdded(EventId id) override; void eventModified(EventId id, Event discardedEvent) override; void eventAboutToBeDeleted(EventId id) override; void eventDeleted(EventId id) override; void eventActivated(EventId id) override; void eventDeactivated(EventId id) override; public Q_SLOTS: // slots migrated from the old main window: void slotEditPreferences(bool); // show prefs dialog void slotAboutDialog(); void slotEnterVacation(); void slotActivityReport(); void slotWeeklyTimesheetReport(); void slotMonthlyTimesheetReport(); void slotExportToXml(); void slotImportFromXml(); void slotSyncTasks(VerboseMode mode = Verbose); void slotSyncTasksVerbose(); void slotImportTasks(); void slotExportTasks(); void maybeIdle(IdleDetector *idleDetector); void slotTasksDownloaded(HttpJob *); void slotUserInfoDownloaded(HttpJob *); void slotCheckForUpdatesManual(); void slotStartEvent(TaskId); void configurationChanged() override; protected: void insertEditMenu() override; private Q_SLOTS: void slotStopEvent(); void slotSelectTasksToShow(); void slotWeeklyTimesheetPreview(int result); void slotMonthlyTimesheetPreview(int result); void slotActivityReportPreview(int result); void slotCheckUploadedTimesheets(); void slotBillGone(int result); void slotCheckForUpdatesAutomatic(); void slotCheckForUpdates(CheckForUpdatesJob::JobData); void slotSyncTasksAutomatic(); void slotGetUserInfo(); Q_SIGNALS: void emitCommand(CharmCommand *) override; void emitCommandRollback(CharmCommand *) override; void showNotification(const QString &title, const QString &message); void taskMenuChanged(); private: void uploadStagedTimesheet(); void resetWeeklyTimesheetDialog(); void resetMonthlyTimesheetDialog(); void showPreview(ReportConfigurationDialog *, int result); //ugly but private: void importTasksFromDeviceOrFile(QIODevice *device, const QString &filename, bool verbose = true); void startCheckForUpdates(VerboseMode mode = Silent); void informUserAboutNewRelease(const QString &releaseVersion, const QUrl &link, const QString &releaseInfoLink); void handleIdleEvents(IdleDetector *detector, bool restart); WeeklyTimesheetConfigurationDialog *m_weeklyTimesheetDialog = nullptr; MonthlyTimesheetConfigurationDialog *m_monthlyTimesheetDialog = nullptr; ActivityReportConfigurationDialog *m_activityReportDialog = nullptr; TimeTrackingView *m_summaryWidget; QVector m_summaries; QTimer m_checkUploadedSheetsTimer; QTimer m_checkCharmReleaseVersionTimer; QTimer m_updateUserInfoAndTasksDefinitionsTimer; BillDialog *m_billDialog; bool m_idleCorrectionDialogVisible = false; bool m_uploadingStagedTimesheet = false; }; #endif Charm-1.12.0/Charm/Widgets/Timesheet.cpp000066400000000000000000000066311331066577000177360ustar00rootroot00000000000000/* Timesheet.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Timesheet.h" #include #include #include #include "ViewHelpers.h" #include "CharmCMake.h" TimeSheetReport::TimeSheetReport(QWidget *parent) : ReportPreviewWindow(parent) { } TimeSheetReport::~TimeSheetReport() { } void TimeSheetReport::setReportProperties( const QDate &start, const QDate &end, TaskId rootTask, bool activeTasksOnly) { m_start = start; m_end = end; m_rootTask = rootTask; m_activeTasksOnly = activeTasksOnly; update(); } void TimeSheetReport::slotUpdate() { update(); } void TimeSheetReport::slotSaveToXml() { // first, ask for a file name: QString filename = getFileName(tr("Charm reports (*.charmreport)")); if (filename.isEmpty()) return; QFileInfo fileinfo(filename); if (fileinfo.suffix().isEmpty()) filename += QLatin1String(".charmreport"); QByteArray payload = saveToXml(IncludeTaskList); if (payload.isEmpty()) return; // Error should have been already displayed by saveToXml() QFile file(filename); if (file.open(QIODevice::WriteOnly)) { file.write(payload); } else { QMessageBox::critical(this, tr("Error saving report"), tr("Cannot write to selected location:\n%1").arg(file.errorString())); } } void TimeSheetReport::slotSaveToText() { // first, ask for a file name: const QString filename = getFileName(QStringLiteral("Text files (*.txt)")); if (filename.isEmpty()) return; QFile file(filename); if (!file.open(QIODevice::WriteOnly)) { QMessageBox::critical(this, tr("Error saving report"), tr("Cannot write to selected location:\n%1") .arg(file.errorString())); return; } file.write(saveToText()); file.close(); } QString TimeSheetReport::getFileName(const QString &filter) { QSettings settings; QString path; if (settings.contains(MetaKey_ReportsRecentSavePath)) { path = settings.value(MetaKey_ReportsRecentSavePath).toString(); QDir dir(path); if (!dir.exists()) path = QString(); } // suggest file name: path += QDir::separator() + suggestedFileName(); // ask: QString filename = QFileDialog::getSaveFileName(this, tr("Enter File Name"), path, filter); if (filename.isEmpty()) return QString(); QFileInfo fileinfo(filename); path = fileinfo.absolutePath(); if (!path.isEmpty()) settings.setValue(MetaKey_ReportsRecentSavePath, path); return filename; } Charm-1.12.0/Charm/Widgets/Timesheet.h000066400000000000000000000044621331066577000174030ustar00rootroot00000000000000/* Timesheet.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TIMESHEET3_H #define TIMESHEET3_H #include #include "ReportPreviewWindow.h" #include "Reports/TimesheetInfo.h" class TimeSheetReport : public ReportPreviewWindow { Q_OBJECT public: explicit TimeSheetReport(QWidget *parent = nullptr); ~TimeSheetReport() override; virtual void setReportProperties(const QDate &start, const QDate &end, TaskId rootTask, bool activeTasksOnly); protected: enum SaveToXmlMode { IncludeTaskList, ExcludeTaskList }; virtual QString suggestedFileName() const = 0; virtual void update() = 0; virtual QByteArray saveToText() = 0; virtual QByteArray saveToXml(SaveToXmlMode mode) = 0; protected: inline QDate startDate() const { return m_start; } inline QDate endDate() const { return m_end; } inline TaskId rootTask() const { return m_rootTask; } inline bool activeTasksOnly() const { return m_activeTasksOnly; } inline const SecondsMap &secondsMap() const { return m_secondsMap; } QString getFileName(const QString &filter); void slotUpdate() override; void slotSaveToText() override; void slotSaveToXml() override; protected: SecondsMap m_secondsMap; private: // properties of the report: QDate m_start; QDate m_end; TaskId m_rootTask = {}; bool m_activeTasksOnly = false; }; #endif Charm-1.12.0/Charm/Widgets/TrayIcon.cpp000066400000000000000000000033301331066577000175300ustar00rootroot00000000000000/* TrayIcon.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TrayIcon.h" #include "ApplicationCore.h" TrayIcon::TrayIcon(QObject *parent) : QSystemTrayIcon(parent) { connect(this, &QSystemTrayIcon::activated, this, &TrayIcon::slotActivated); } TrayIcon::~TrayIcon() { } void TrayIcon::slotActivated(QSystemTrayIcon::ActivationReason reason) { switch (reason) { case QSystemTrayIcon::Context: // show context menu // m_systrayContextMenu.show(); break; case QSystemTrayIcon::Trigger: //(single click) case QSystemTrayIcon::DoubleClick: #ifndef Q_OS_OSX ApplicationCore::instance().showMainWindow(ApplicationCore::ShowMode::ShowAndRaise); #endif break; case QSystemTrayIcon::MiddleClick: // TODO: Start task? ApplicationCore::instance().slotStopAllTasks(); break; case QSystemTrayIcon::Unknown: default: break; } } Charm-1.12.0/Charm/Widgets/TrayIcon.h000066400000000000000000000022351331066577000172000ustar00rootroot00000000000000/* TrayIcon.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TRAYICON_H #define TRAYICON_H #include class TrayIcon : public QSystemTrayIcon { Q_OBJECT public: explicit TrayIcon(QObject *parent = nullptr); ~TrayIcon() override; private Q_SLOTS: void slotActivated(QSystemTrayIcon::ActivationReason); }; #endif // TRAYICON_H Charm-1.12.0/Charm/Widgets/WeeklyTimesheet.cpp000066400000000000000000000552501331066577000211200ustar00rootroot00000000000000/* WeeklyTimesheet.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "WeeklyTimesheet.h" #include "Reports/WeeklyTimesheetXmlWriter.h" #include #include #include #include #include #include #include "DateEntrySyncer.h" #include "HttpClient/UploadTimesheetJob.h" #include "Lotsofcake/Configuration.h" #include "Widgets/HttpJobProgressDialog.h" #include "SelectTaskDialog.h" #include "ViewHelpers.h" #include "ui_WeeklyTimesheetConfigurationDialog.h" namespace { static QString SETTING_GRP_TIMESHEETS = QStringLiteral("timesheets"); static QString SETTING_VAL_FIRSTYEAR = QStringLiteral("firstYear"); static QString SETTING_VAL_FIRSTWEEK = QStringLiteral("firstWeek"); static const int MAX_WEEK = 53; static const int MIN_YEAR = 1990; static const int DaysInWeek = 7; enum TimeSheetTableColumns { Column_Task, Column_Monday, Column_Tuesday, Column_Wednesday, Column_Thursday, Column_Friday, Column_Saturday, Column_Sunday, Column_Total, NumberOfColumns }; } void addUploadedTimesheet(int year, int week) { Q_ASSERT(year >= MIN_YEAR && week > 0 && week <= MAX_WEEK); QSettings settings; settings.beginGroup(SETTING_GRP_TIMESHEETS); QString yearStr = QString::number(year); QString weekStr = QString::number(week); QStringList existingSheets = settings.value(yearStr).toStringList(); if (!existingSheets.contains(weekStr)) settings.setValue(yearStr, existingSheets << weekStr); if (settings.value(SETTING_VAL_FIRSTYEAR, QString()).toString().isEmpty()) settings.setValue(SETTING_VAL_FIRSTYEAR, yearStr); if (settings.value(SETTING_VAL_FIRSTWEEK, QString()).toString().isEmpty()) settings.setValue(SETTING_VAL_FIRSTWEEK, weekStr); } WeeksByYear missingTimeSheets() { WeeksByYear missing; QSettings settings; settings.beginGroup(SETTING_GRP_TIMESHEETS); int year = 0; int week = QDateTime::currentDateTime().date().weekNumber(&year); int firstYear = settings.value(SETTING_VAL_FIRSTYEAR, year).value(); int firstWeek = settings.value(SETTING_VAL_FIRSTWEEK, week).value(); for (int iYear = firstYear; iYear <= year; ++iYear) { QStringList uploaded = settings.value(QString::number(iYear)).toStringList(); int firstWeekOfYear = iYear == firstYear ? firstWeek : 1; int lastWeekOfYear = iYear == year ? week - 1 : Charm::numberOfWeeksInYear(iYear); for (int iWeek = firstWeekOfYear; iWeek <= lastWeekOfYear; ++iWeek) { if (!uploaded.contains(QString::number(iWeek))) { Q_ASSERT(iYear >= MIN_YEAR && iWeek > 0 && iWeek <= MAX_WEEK); missing[iYear].append(iWeek); } } } return missing; } /************************************************** WeeklyTimesheetConfigurationDialog */ WeeklyTimesheetConfigurationDialog::WeeklyTimesheetConfigurationDialog(QWidget *parent) : ReportConfigurationDialog(parent) , m_ui(new Ui::WeeklyTimesheetConfigurationDialog) { setWindowTitle(tr("Weekly Timesheet")); m_ui->setupUi(this); m_ui->dateEditDay->calendarWidget()->setFirstDayOfWeek(Qt::Monday); m_ui->dateEditDay->calendarWidget()->setVerticalHeaderFormat(QCalendarWidget::ISOWeekNumbers); connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &WeeklyTimesheetConfigurationDialog::accept); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &WeeklyTimesheetConfigurationDialog::reject); connect(m_ui->comboBoxWeek, SIGNAL(currentIndexChanged(int)), SLOT(slotWeekComboItemSelected(int))); connect(m_ui->toolButtonSelectTask, &QToolButton::clicked, this, &WeeklyTimesheetConfigurationDialog::slotSelectTask); connect(m_ui->checkBoxSubTasksOnly, &QCheckBox::toggled, this, &WeeklyTimesheetConfigurationDialog::slotCheckboxSubtasksOnlyChecked); m_ui->comboBoxWeek->setCurrentIndex(1); slotCheckboxSubtasksOnlyChecked(m_ui->checkBoxSubTasksOnly->isChecked()); new DateEntrySyncer(m_ui->spinBoxWeek, m_ui->spinBoxYear, m_ui->dateEditDay, 1, this); slotStandardTimeSpansChanged(); connect(ApplicationCore::instance().dateChangeWatcher(), &DateChangeWatcher::dateChanged, this, &WeeklyTimesheetConfigurationDialog::slotStandardTimeSpansChanged); // load settings: QSettings settings; if (settings.contains(MetaKey_TimesheetActiveOnly)) { m_ui->checkBoxActiveOnly->setChecked(settings.value(MetaKey_TimesheetActiveOnly).toBool()); } else { m_ui->checkBoxActiveOnly->setChecked(true); } } WeeklyTimesheetConfigurationDialog::~WeeklyTimesheetConfigurationDialog() { } void WeeklyTimesheetConfigurationDialog::setDefaultWeek(int yearOfWeek, int week) { m_ui->spinBoxWeek->setValue(week); m_ui->spinBoxYear->setValue(yearOfWeek); m_ui->comboBoxWeek->setCurrentIndex(4); } void WeeklyTimesheetConfigurationDialog::accept() { // save settings: QSettings settings; settings.setValue(MetaKey_TimesheetActiveOnly, m_ui->checkBoxActiveOnly->isChecked()); settings.setValue(MetaKey_TimesheetRootTask, m_rootTask); QDialog::accept(); } void WeeklyTimesheetConfigurationDialog::showReportPreviewDialog() { QDate start, end; int index = m_ui->comboBoxWeek->currentIndex(); if (index == m_weekInfo.size() -1) { // manual selection QDate selectedDate = m_ui->dateEditDay->date(); start = selectedDate.addDays(-selectedDate.dayOfWeek() + 1); end = start.addDays(DaysInWeek); } else { start = m_weekInfo[index].timespan.first; end = m_weekInfo[index].timespan.second; } bool activeOnly = m_ui->checkBoxActiveOnly->isChecked(); auto report = new WeeklyTimeSheetReport(); report->setReportProperties(start, end, m_rootTask, activeOnly); report->show(); } void WeeklyTimesheetConfigurationDialog::showEvent(QShowEvent *) { QSettings settings; // we only want to do this once a backend is loaded, and we ignore // the saved root task if it does not exist anymore if (settings.contains(MetaKey_TimesheetRootTask)) { TaskId root = settings.value(MetaKey_TimesheetRootTask).toInt(); const TaskTreeItem &item = DATAMODEL->taskTreeItem(root); if (item.isValid()) { m_rootTask = root; m_ui->labelTaskName->setText(DATAMODEL->fullTaskName(item.task())); m_ui->checkBoxSubTasksOnly->setChecked(true); } } } void WeeklyTimesheetConfigurationDialog::slotCheckboxSubtasksOnlyChecked(bool checked) { if (checked && m_rootTask == 0) slotSelectTask(); if (!checked) { m_rootTask = 0; m_ui->labelTaskName->setText(tr("(All Tasks)")); } } void WeeklyTimesheetConfigurationDialog::slotStandardTimeSpansChanged() { const TimeSpans timeSpans; m_weekInfo = timeSpans.last4Weeks(); NamedTimeSpan custom = { tr("Manual Selection"), timeSpans.thisWeek().timespan, Range }; m_weekInfo << custom; m_ui->comboBoxWeek->clear(); for (int i = 0; i < m_weekInfo.size(); ++i) m_ui->comboBoxWeek->addItem(m_weekInfo[i].name); // Set current index to "Last Week" as that's what you'll usually want m_ui->comboBoxWeek->setCurrentIndex(1); } void WeeklyTimesheetConfigurationDialog::slotWeekComboItemSelected(int index) { // wait for the next update, in this case: if (m_ui->comboBoxWeek->count() == 0 || index == -1) return; Q_ASSERT(m_ui->comboBoxWeek->count() > index); if (index == m_weekInfo.size() - 1) { // manual selection m_ui->groupBox->setEnabled(true); } else { m_ui->dateEditDay->setDate(m_weekInfo[index].timespan.first); m_ui->groupBox->setEnabled(false); } } void WeeklyTimesheetConfigurationDialog::slotSelectTask() { SelectTaskDialog dialog(this); dialog.setNonTrackableSelectable(); if (dialog.exec()) { m_rootTask = dialog.selectedTask(); const TaskTreeItem &item = DATAMODEL->taskTreeItem(m_rootTask); m_ui->labelTaskName->setText(DATAMODEL->fullTaskName(item.task())); } else { if (m_rootTask == 0) m_ui->checkBoxSubTasksOnly->setChecked(false); } } /*************************************************************** WeeklyTimeSheetReport */ // here begins ... the actual report: WeeklyTimeSheetReport::WeeklyTimeSheetReport(QWidget *parent) : TimeSheetReport(parent) { QPushButton *upload = uploadButton(); connect(upload, &QPushButton::clicked, this, &WeeklyTimeSheetReport::slotUploadTimesheet); connect(this, &ReportPreviewWindow::anchorClicked, this, &WeeklyTimeSheetReport::slotLinkClicked); if (!Lotsofcake::Configuration().isConfigured()) upload->hide(); } WeeklyTimeSheetReport::~WeeklyTimeSheetReport() { } void WeeklyTimeSheetReport::setReportProperties( const QDate &start, const QDate &end, TaskId rootTask, bool activeTasksOnly) { m_weekNumber = start.weekNumber(&m_yearOfWeek); TimeSheetReport::setReportProperties(start, end, rootTask, activeTasksOnly); } void WeeklyTimeSheetReport::slotUploadTimesheet() { const Lotsofcake::Configuration configuration; auto client = new UploadTimesheetJob(this); client->setUsername(configuration.username()); client->setUploadUrl(configuration.timesheetUploadUrl()); auto dialog = new HttpJobProgressDialog(client, this); dialog->setWindowTitle(tr("Uploading")); connect(client, &HttpJob::finished, this, &WeeklyTimeSheetReport::slotTimesheetUploaded); client->setFileName(suggestedFileName()); client->setPayload(saveToXml(ExcludeTaskList)); client->start(); uploadButton()->setEnabled(false); } void WeeklyTimeSheetReport::slotTimesheetUploaded(HttpJob *client) { uploadButton()->setEnabled(true); switch (client->error()) { case HttpJob::NoError: addUploadedTimesheet(m_yearOfWeek, m_weekNumber); QMessageBox::information(this, tr("Timesheet Uploaded"), tr("Your timesheet was successfully uploaded.")); break; case HttpJob::Canceled: break; case HttpJob::AuthenticationFailed: uploadButton()->setEnabled(false); slotUploadTimesheet(); break; default: QMessageBox::critical(this, tr("Error"), tr("Could not upload timesheet: %1").arg(client->errorString())); } } QString WeeklyTimeSheetReport::suggestedFileName() const { return tr("WeeklyTimeSheet-%1-%2").arg(m_yearOfWeek).arg(m_weekNumber, 2, 10, QLatin1Char('0')); } void WeeklyTimeSheetReport::update() { // this creates the time sheet // retrieve matching events: const EventIdList matchingEvents = DATAMODEL->eventsThatStartInTimeFrame(startDate(), endDate()); m_secondsMap.clear(); // for every task, make a vector that includes a number of seconds // for every day of the week ( int seconds[7]), and store those in // a map by their task id Q_FOREACH (EventId id, matchingEvents) { const Event &event = DATAMODEL->eventForId(id); QVector seconds(DaysInWeek); if (m_secondsMap.contains(event.taskId())) seconds = m_secondsMap.value(event.taskId()); // what day in the week is the event (normalized to vector indexes): int dayOfWeek = event.startDateTime().date().dayOfWeek() - 1; Q_ASSERT(dayOfWeek >= 0 && dayOfWeek < DaysInWeek); seconds[dayOfWeek] += event.duration(); // store in minute map: m_secondsMap[event.taskId()] = seconds; } // now the reporting: // headline first: QTextDocument report; QDomDocument doc = createReportTemplate(); QDomElement root = doc.documentElement(); QDomElement body = root.firstChildElement(QStringLiteral("body")); // create the caption: { QDomElement headline = doc.createElement(QStringLiteral("h1")); QDomText text = doc.createTextNode(tr("Weekly Time Sheet")); headline.appendChild(text); body.appendChild(headline); } { QDomElement headline = doc.createElement(QStringLiteral("h3")); QString content = tr("Report for %1, Week %2 (%3 to %4)") .arg(CONFIGURATION.user.name()) .arg(m_weekNumber, 2, 10, QLatin1Char('0')) .arg(startDate().toString(Qt::TextDate)) .arg(endDate().addDays(-1).toString(Qt::TextDate)); QDomText text = doc.createTextNode(content); headline.appendChild(text); body.appendChild(headline); QDomElement previousLink = doc.createElement(QStringLiteral("a")); previousLink.setAttribute(QStringLiteral("href"), QStringLiteral("Previous")); QDomText previousLinkText = doc.createTextNode(tr("")); previousLink.appendChild(previousLinkText); body.appendChild(previousLink); QDomElement nextLink = doc.createElement(QStringLiteral("a")); nextLink.setAttribute(QStringLiteral("href"), QStringLiteral("Next")); QDomText nextLinkText = doc.createTextNode(tr("")); nextLink.appendChild(nextLinkText); body.appendChild(nextLink); QDomElement paragraph = doc.createElement(QStringLiteral("br")); body.appendChild(paragraph); } { // now for a table // retrieve the information for the report: // TimeSheetInfoList timeSheetInfo = taskWithSubTasks( rootTask(), secondsMap() ); TimeSheetInfoList timeSheetInfo = TimeSheetInfo::filteredTaskWithSubTasks( TimeSheetInfo::taskWithSubTasks(DATAMODEL, DaysInWeek, rootTask(), secondsMap()), activeTasksOnly()); QDomElement table = doc.createElement(QStringLiteral("table")); table.setAttribute(QStringLiteral("width"), QStringLiteral("100%")); table.setAttribute(QStringLiteral("align"), QStringLiteral("left")); table.setAttribute(QStringLiteral("cellpadding"), QStringLiteral("3")); table.setAttribute(QStringLiteral("cellspacing"), QStringLiteral("0")); body.appendChild(table); TimeSheetInfo totalsLine(DaysInWeek); if (!timeSheetInfo.isEmpty()) { totalsLine = timeSheetInfo.first(); if (rootTask() == 0) timeSheetInfo.removeAt(0); // there is always one, because there is always the root item } QDomElement headerRow = doc.createElement(QStringLiteral("tr")); headerRow.setAttribute(QStringLiteral("class"), QStringLiteral("header_row")); table.appendChild(headerRow); QDomElement headerDayRow = doc.createElement(QStringLiteral("tr")); headerDayRow.setAttribute(QStringLiteral("class"), QStringLiteral("header_row")); table.appendChild(headerDayRow); const QString Headlines[NumberOfColumns] = { tr("Task"), QDate::shortDayName(1), QDate::shortDayName(2), QDate::shortDayName(3), QDate::shortDayName(4), QDate::shortDayName(5), QDate::shortDayName(6), QDate::shortDayName(7), tr("Total") }; const QString DayHeadlines[NumberOfColumns] = { QString(), tr("%1").arg(startDate().day(), 2, 10, QLatin1Char('0')), tr("%1").arg(startDate().addDays(1).day(), 2, 10, QLatin1Char('0')), tr("%1").arg(startDate().addDays(2).day(), 2, 10, QLatin1Char('0')), tr("%1").arg(startDate().addDays(3).day(), 2, 10, QLatin1Char('0')), tr("%1").arg(startDate().addDays(4).day(), 2, 10, QLatin1Char('0')), tr("%1").arg(startDate().addDays(5).day(), 2, 10, QLatin1Char('0')), tr("%1").arg(startDate().addDays(6).day(), 2, 10, QLatin1Char('0')), QString() }; for (int i = 0; i < NumberOfColumns; ++i) { QDomElement header = doc.createElement(QStringLiteral("th")); QDomText text = doc.createTextNode(Headlines[i]); header.appendChild(text); headerRow.appendChild(header); QDomElement dayHeader = doc.createElement(QStringLiteral("th")); QDomText dayText = doc.createTextNode(DayHeadlines[i]); dayHeader.appendChild(dayText); headerDayRow.appendChild(dayHeader); } for (int i = 0; i < timeSheetInfo.size(); ++i) { QDomElement row = doc.createElement(QStringLiteral("tr")); if (i % 2) row.setAttribute(QStringLiteral("class"), QStringLiteral("alternate_row")); table.appendChild(row); QString texts[NumberOfColumns]; texts[Column_Task] = timeSheetInfo[i].formattedTaskIdAndName( CONFIGURATION.taskPaddingLength); texts[Column_Monday] = hoursAndMinutes(timeSheetInfo[i].seconds[0]); texts[Column_Tuesday] = hoursAndMinutes(timeSheetInfo[i].seconds[1]); texts[Column_Wednesday] = hoursAndMinutes(timeSheetInfo[i].seconds[2]); texts[Column_Thursday] = hoursAndMinutes(timeSheetInfo[i].seconds[3]); texts[Column_Friday] = hoursAndMinutes(timeSheetInfo[i].seconds[4]); texts[Column_Saturday] = hoursAndMinutes(timeSheetInfo[i].seconds[5]); texts[Column_Sunday] = hoursAndMinutes(timeSheetInfo[i].seconds[6]); texts[Column_Total] = hoursAndMinutes(timeSheetInfo[i].total()); for (int column = 0; column < NumberOfColumns; ++column) { QDomElement cell = doc.createElement(QStringLiteral("td")); cell.setAttribute(QStringLiteral("align"), column == Column_Task ? QStringLiteral("left") : QStringLiteral("center")); if (column == Column_Task) { QString style = QStringLiteral("text-indent: %1px;") .arg(9 * timeSheetInfo[i].indentation); cell.setAttribute(QStringLiteral("style"), style); } QDomText text = doc.createTextNode(texts[column]); cell.appendChild(text); row.appendChild(cell); } } // put the totals: QString TotalsTexts[NumberOfColumns] = { tr("Total:"), hoursAndMinutes(totalsLine.seconds[0]), hoursAndMinutes(totalsLine.seconds[1]), hoursAndMinutes(totalsLine.seconds[2]), hoursAndMinutes(totalsLine.seconds[3]), hoursAndMinutes(totalsLine.seconds[4]), hoursAndMinutes(totalsLine.seconds[5]), hoursAndMinutes(totalsLine.seconds[6]), hoursAndMinutes(totalsLine.total()) }; QDomElement totals = doc.createElement(QStringLiteral("tr")); totals.setAttribute(QStringLiteral("class"), QStringLiteral("header_row")); table.appendChild(totals); for (int i = 0; i < NumberOfColumns; ++i) { QDomElement cell = doc.createElement(QStringLiteral("th")); QDomText text = doc.createTextNode(TotalsTexts[i]); cell.appendChild(text); totals.appendChild(cell); } } // NOTE: seems like the style sheet has to be set before the html // code is pushed into the QTextDocument report.setDefaultStyleSheet(Charm::reportStylesheet(palette())); report.setHtml(doc.toString()); setDocument(&report); uploadButton()->setEnabled(true); } QByteArray WeeklyTimeSheetReport::saveToXml(SaveToXmlMode mode) { try { WeeklyTimesheetXmlWriter timesheet; timesheet.setDataModel(DATAMODEL); timesheet.setYear(m_yearOfWeek); timesheet.setWeekNumber(m_weekNumber); timesheet.setRootTask(rootTask()); timesheet.setIncludeTaskList(mode == IncludeTaskList); const EventIdList matchingEventIds = DATAMODEL->eventsThatStartInTimeFrame( startDate(), endDate()); EventList events; events.reserve(matchingEventIds.size()); Q_FOREACH (const EventId &id, matchingEventIds) events.append(DATAMODEL->eventForId(id)); timesheet.setEvents(events); return timesheet.saveToXml(); } catch (const XmlSerializationException &e) { QMessageBox::critical(this, tr("Error exporting the report"), e.what()); } return QByteArray(); } QByteArray WeeklyTimeSheetReport::saveToText() { QByteArray output; QTextStream stream(&output); QString content = tr("Report for %1, Week %2 (%3 to %4)") .arg(CONFIGURATION.user.name()) .arg(m_weekNumber, 2, 10, QLatin1Char('0')) .arg(startDate().toString(Qt::TextDate)) .arg(endDate().addDays(-1).toString(Qt::TextDate)); stream << content << '\n'; stream << '\n'; TimeSheetInfoList timeSheetInfo = TimeSheetInfo::filteredTaskWithSubTasks( TimeSheetInfo::taskWithSubTasks(DATAMODEL, DaysInWeek, rootTask(), secondsMap()), activeTasksOnly()); TimeSheetInfo totalsLine(DaysInWeek); if (!timeSheetInfo.isEmpty()) { totalsLine = timeSheetInfo.first(); if (rootTask() == 0) timeSheetInfo.removeAt(0); // there is always one, because there is always the root item } for (int i = 0; i < timeSheetInfo.size(); ++i) stream << timeSheetInfo[i].formattedTaskIdAndName(CONFIGURATION.taskPaddingLength) << "\t" << hoursAndMinutes(timeSheetInfo[i].total()) << '\n'; stream << '\n'; stream << "Week total: " << hoursAndMinutes(totalsLine.total()) << '\n'; stream.flush(); return output; } void WeeklyTimeSheetReport::slotLinkClicked(const QUrl &which) { QDate start = which.toString() == QLatin1String("Previous") ? startDate().addDays(-7) : startDate().addDays(7); QDate end = which.toString() == QLatin1String("Previous") ? endDate().addDays(-7) : endDate().addDays(7); setReportProperties(start, end, rootTask(), activeTasksOnly()); } Charm-1.12.0/Charm/Widgets/WeeklyTimesheet.h000066400000000000000000000054741331066577000205700ustar00rootroot00000000000000/* WeeklyTimesheet.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef WEEKLYTIMESHEET3_H #define WEEKLYTIMESHEET3_H #include #include #include "Timesheet.h" #include "ReportConfigurationDialog.h" #include namespace Ui { class WeeklyTimesheetConfigurationDialog; } class HttpJob; class QUrl; typedef QHash > WeeksByYear; ///Set the timesheet for the @param week of the @param year as having been uploaded void addUploadedTimesheet(int year, int week); ///Get all missing timesheets WeeksByYear missingTimeSheets(); class WeeklyTimesheetConfigurationDialog : public ReportConfigurationDialog { Q_OBJECT public: explicit WeeklyTimesheetConfigurationDialog(QWidget *parent); ~WeeklyTimesheetConfigurationDialog() override; void showReportPreviewDialog() override; void showEvent(QShowEvent *) override; void setDefaultWeek(int yearOfWeek, int week); public Q_SLOTS: void accept() override; private Q_SLOTS: void slotCheckboxSubtasksOnlyChecked(bool); void slotStandardTimeSpansChanged(); void slotWeekComboItemSelected(int); void slotSelectTask(); private: QScopedPointer m_ui; QList m_weekInfo; TaskId m_rootTask = {}; }; class WeeklyTimeSheetReport : public TimeSheetReport { Q_OBJECT public: explicit WeeklyTimeSheetReport(QWidget *parent = nullptr); ~WeeklyTimeSheetReport() override; void setReportProperties(const QDate &start, const QDate &end, TaskId rootTask, bool activeTasksOnly) override; private Q_SLOTS: void slotUploadTimesheet(); void slotTimesheetUploaded(HttpJob *); void slotLinkClicked(const QUrl &which); private: QString suggestedFileName() const override; void update() override; QByteArray saveToXml(SaveToXmlMode mode) override; QByteArray saveToText() override; private: // properties of the report: int m_weekNumber = 0; int m_yearOfWeek = 0; }; #endif Charm-1.12.0/Charm/Widgets/WeeklyTimesheetConfigurationDialog.ui000066400000000000000000000175421331066577000246250ustar00rootroot00000000000000 WeeklyTimesheetConfigurationDialog 0 0 402 370 Qt::Horizontal 40 20 &Select week: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter comboBoxWeek 0 0 QComboBox::AdjustToContents Qt::Horizontal 40 20 Manual Selection QFormLayout::AllNonFixedFieldsGrow Week: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter dateEditDay 1 53 1900 5000 2011 Qt::Horizontal 40 20 Week containing: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter dateEditDay true What Tasks to include in the report: Show... false 0 0 (task name) Qt::AlignCenter ... and subtasks false Select Task... (all tasks, otherwise). Qt::Horizontal 40 20 Only show tasks with activity true QDialogButtonBox::Cancel|QDialogButtonBox::Ok checkBoxSubTasksOnly toggled(bool) labelTaskName setEnabled(bool) 68 171 137 171 checkBoxSubTasksOnly toggled(bool) toolButtonSelectTask setEnabled(bool) 84 182 151 204 Charm-1.12.0/Charm/Widgets/WidgetUtils.cpp000066400000000000000000000032761331066577000202550ustar00rootroot00000000000000/* WidgetUtils.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2016-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Hannah von Reth This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "WidgetUtils.h" #include #include #include #include void WidgetUtils::restoreGeometry(QWidget *widget, const QString &metaKey) { QSettings settings; settings.beginGroup(QStringLiteral("WindowStates")); if (settings.contains(metaKey)) widget->restoreGeometry(settings.value(metaKey).toByteArray()); } void WidgetUtils::saveGeometry(QWidget *widget, const QString &metaKey) { QSettings settings; settings.beginGroup(QStringLiteral("WindowStates")); settings.setValue(metaKey, widget->saveGeometry()); } void WidgetUtils::updateToolButtonStyle(QWidget *widget) { const QList buttons = widget->findChildren(); Q_FOREACH (auto button, buttons) button->setToolButtonStyle(CONFIGURATION.toolButtonStyle); } Charm-1.12.0/Charm/Widgets/WidgetUtils.h000066400000000000000000000022301331066577000177070ustar00rootroot00000000000000/* WidgetUtils.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2016-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Hannah von Reth This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef WIDGETUTILS_H #define WIDGETUTILS_H #include class QWidget; namespace WidgetUtils { void restoreGeometry(QWidget *widget, const QString &metaKey); void saveGeometry(QWidget *widget, const QString &metaKey); void updateToolButtonStyle(QWidget *widget); } #endif // WIDGETUTILS_H Charm-1.12.0/Charm/Widgets/report_stylesheet.sty000066400000000000000000000011611331066577000216210ustar00rootroot00000000000000h1 { font-size: 1500%; margin-bottom: 24px; margin-bottom: 12px; } h2 { font-size: 125%; margin-bottom: 18px; } h3 { font-size: 110%; } th { align: left; font-size: 100%; } td { font-size: 80%; } .header_row { background-color: @header_row_background_color@; color: @header_row_foreground_color@; } .alternate_row { background-color: @alternate_row_background_color@; } .event_description { padding-left: 24px; } .event_attributes { align: left; font-family: Bagdad, Serif, Fixed; padding: 6px; } .event_attributes_row { background-color: @event_attributes_row_background_color@; } Charm-1.12.0/Charm/bill.jpg000066400000000000000000001221211331066577000153120ustar00rootroot00000000000000JFIFHHC     C   . Vnœy.A Q%1w/02sre''bLQ芥%9-HVuGe@`vyZ\hiE:(iá%S@ eK -xKM<9 aAҲBi;}< vERIP͕- P! )6rOZg\!A}"Ƭ5d1oyޣUTƺ?~nU.V5,}.)eGx:!e޳D jm9pU $eD—™Iy]:|T=<-=*wWKMqS8v(5\gJwߞͧ %TZ@ia,{]%l*aW= t.!\sJ",|DHUT'}woaç{=r;܌iGb ˻ʉQ~oc",SG0ADTz(}l{JʎQkL#lvHV?́FXv=Oq((JYt` ;[K9E<V5,~?e=ϖnPZ|S*dW$I:ԬHfN.Zfu`#7#巜>߀'n` Z'eߛ"\x7UhZYTpvkP^^_^|QaW6|C?>.<;JlH<җ7n|cU,gʩlw8)SE0ύFهV}}%;`,9\Ncmc<'?q^J?'(ffeAztQ=כh3y0*E[|媋 rj#;ytm酓V 0=5"z]}.|\wX׳tˌ?ܮ?N8Ι\g+κq]sfKIm^u^{n@13zgsܙRNFm$D\oPicZuYa1ߙ3P.R&=8U5Uƹ{F:c\Ӣ_] \ʱ'Ӻrj?ܮ'ftM=蔮JUu.&aOsz+GaO'5ˎNTj{êkIs]<氹%e^݊tw f &G:2+DpCz''\IK#qcuW0tfw]`§3zn.ILNnu,O>;:mT 46n¬A48 CGbYTkC:ާKTr>e"gNiOG-ݠ1XӥⳐȐ}ou\m˩CnZ'znt'7=,r uD2j-twO2}*9-!{_6/Nr Ĺ\#sϮ Ω;=6fqťڳtCÒۤTiuS9#nD%jP2#eoVKsBw xc_̱RU"7{Ui?=xR#7P$QlIH3QoYW?\eiEFO妉Ff!8!3ZUKdֹ='f~a2W6X FyE)TUR\?"<6oܛ6:<:$*Zyܿ#};ȾI诱>B 9ыt5G/upמ$&Vj7+w8rv{8bEg<,uγDpww_=6R2Ym&jx|[Kܑlqc:rE W]ssщ)!xlӷef˷-|}vyo$2Fg7aLXj r5E3UvH!a-מcӽDTl ~t5 e[]:GG-6< (K pഀ?%=_6GYu9qszw7`]2:NqcBp+COcHooܓӌWK4va21\9cկuD ! YP_+Px<Q% 1\͢dm/*{R}M v1C#M@ʲL7XWT%M=/ Uhz*4ܹv sn,0bFZɣׇx<C,!RB.?OYˡ;h7冴m!҃: Db6:8Iϥ36y=y0>>ub p1MJ]*$Y|ksв^xˋjrG])QXe\_JL{At65<MV}D|OчZI Ԉv9J2mMYsEcYsX7iv]`Kn' u,n*GPu99+T/o{mnͥ*3 Ӕd.w^Y1~,gT{l_0c;cL*x|fqܣҬ~4Mcy%Д52b B#R,+jB] _59,B*$(qF8cqfgv9.j+gߖW2 b|Y4J~.,f7l;GZ-FS96} DLU(f G9TD%e*ԺΧjhlp;3.G.4A[ 7B&,Jim 1 >к8ܞQ0fϢHY"N#>S4i]Xj E51%V:K 2>zAO4C=ARHՌuM(bkyk!H*M07ru\dk-[K! !ڇ_h5+%p82Z f ݥ@ʚ6>'?_%B#C|('M9Jm@[: GH L fz ]ZbCp8⾓$"B*w4Hoѩ9j>5bfs9=SJv2^M\]o9qu}y0!4>-"@БCDL~ZWy;s\=G&UھU4-IN\ ~ bA R$YkcLLF.7,VF٠?on+m^/Fn2;>qzGvsD "hHIPŅӅc&X‘V+bS~((`W(֚P]RgcVWz+E$D>PkSS\{U*.<*O·i!ϭkYLlO'T"L*Ad1ȆF5֠GmG_Lk<]qd-Yc!IOB*L3zYjU.JJF|铦\E} zB@ k n΍"?R\kFTvk v@"xnH@+kdX)4I΋CĨc3($HJ}A$@-/7AV 9:ΓS k L1JgFuz)q<VȞhcBG 1̶.=d;UQay"ml՝ uLYr$$,.s_Ri. :p\s҅Tƾh/brSvT}tl*$Fr~d]9g)_n\[]3.zFdKD2LG\g wZ-Q9Ӳ0;gcm\N$Č8AUͳ+yd]Dg'+LWY]2=3;&cu dXRcEc-Mdn0)${ve=| ˒;51[ =`6צ-縆2OSj_OZmL.C85Q|$Dc _tiC)R[@.H1H*$???n>j̢I n$ꘈu*5#yqe%qiI[q`3Ʒl=y+yEAmrvĩmLw/~1ֶ2CorՇjl w:*vWgfLFJ ú&oDk 7q/U3U^\޲5z{Q#gF?$#lgLlGS8תkЀG/Gby(vٜ%LO)jo5~)[7n!Y=8/ꤧ9x*yj/g07WF71DFHxap65EГ1V<rC˪%V4"N{9[8)#8r65&4\Y|j*րk ߢ`OM%Nh_(@Ⱦ Z_nƓ59 CxH^Llv=u #nܱ_*1u]rXӽi!G8Nvu:\|Lie(j-gc4)dMvΓet*w7 xG'_s|E\kkۧ}Ꞥgٴ5mbQ >kk+t򦒡"2WGl(ҹ Z yF%Rƍ$Fpyĩyd]팍GDq־ 5MTZ(;t3Ck GLQ$`I56O|`@Ko,YrT&0y2c]FӶ*Vyf_n*/ihh3dzaSbPFqHpH6{KtysjDAZ Ԯ8K6%)E^av,c3R *2ɝErwa3c9mvY'Da J($^7p޶Y]! NV k^/|o翄+3>z"\9lҊ((ZZ1Ăy-cY&_%hkGrl D$Pֱe.MN=x$B<&1hgCe cZH7 #l3m2ƐUEQg*m 4#.l}h1>O#QpuU"ÎEDl.K)Ri/~2cD=# 6S<'7c~+g>fĖFzg"#v<Қm$)JfY%. ˺B|P\Ar6dƟ,Pbvn[)4m}{6|?'.VPR%!x|>PG"Z藑qJN9gEeY]Tx_4%71In F1H1nR"e#uZ"L%#ꙍS"mYYmևƋ\QPē"y8ͷ:T)rʙm$crl|fz/"'qQ^ץwtV;;Y =\P#*HL6y)&ΐbvaJJ0%ϣ&g7>]!*B"Q"2/Iբ- S/F1oOXU2S"Pϕ_$0G~Demc>Ï{&M%$n\}V=_D_$Rze""}ZGڇĚ$9%*a⠻3y2Z\#HE.5s3ds W7FIz+_9^1}HlB|vc~GLݑ>Ǔ/e=i|Dhݸ^/LX\,2_tLy-acmgdM%/f>߱^ϝ"t'd] Z1Ѳج*ƿmLh'> K*481XZ)m|i4cn"E5DdKCJσs7 XH8Je3hQzK`[".Y#T%/EC D1-ٻ /| ѼܴLbr9bwOyĒ) RH/d_!:.\IZP%o1"^䮆م^D)%/UдG$N5c$GعzGoQ/'/bqANT2xRs3<UriZGw'bїC,L/ܞDi{؟?|U#K16x$Ed;%ް'ct'p"N.h؇Q/D_:œ!'i1$y>DpFfo/&gH21>Cbꗲ\n#!"Y.I>g(rjmJ>F>4i:[+XvK.EKbcwzbzFGz]!t?1!'Ћ z2x,k˳_f1Ǒv4mdv1;b,{y#߹iFB$ N↬B%п36Ҳl!>G" K]qIrjE˄,Mrz,Lؠ(8cv,Lpq#.AIǍ.|%hHe+F\<\k]ٺj2G%&$">|Q팹)MNԇN%hȿJ %[,|Gi,\GTAZ?|/b~ދ+1)R+n\ds;;B/X.G*tB[dxkI+f%п?ȆyQ1h$)gZ.čXvBczpEMdq+K}֩tN i&رOx,r1^"=kJ|$aKM#vM\SҴ_oOp8h˥ؤ"'I%hoiQыĒn,qx] OK_:i2+|(ё8Ǻ0EN#c'.+D_1;TISe-/G]#_hh\?""?#,k|*"7ֲDzXi -$Lm$1jf2I'ƒbXbE/!1 AQ"20aq#@B3R?|qϸ}“YO GD8k 6/r>6?lJke>ѹn7܍ލދ,nxQWBLq(S999fmɲ_&|%l%5hE+%&W]_?Gb7?s-eu^lF|%}7moM7G9#E)p>)GMf S-F7"]}R9w6KԨ3OYjbRFmcl|_#iFѮl^Lov*?jhڇV^^Qh2,#mVRG|Y-^MPKز׆_VMZ5^rm4ĩ f,+XeWƼ5FrmX?sjӞůBǭQGEn(.Ɗ(#iFh,n1;7Ev&{5uhN_-s%+&%(?l |2NiMN7Ĉᱲ%4_.%}4HHm [Nعf8rX~Vp5XFȱ!=h7wLE,j&.7ɧdI>GȐB-.|VP}CT]J,z=e >VCV%;Ƨ] B%Ls3^+=3M^f9ѹ|m՟65O'tHIDъ'G,ԛTVطQ~'#cE(iE.PoM) ?a$iq%Ȑ&"h]-cI 4t:%SC~柧ps--a#%8cDdt)E"\Pӻ6kdv5t OFig$_i 6!E"IW#V(소hDS"Ch5^ĩ4Wb#XR3WIj.GLJOM5?Z}|xM\XŎ27- dӣOӥ=:%PHhڍF5ȱĚN),9wD$8I{1GcO rFH+!'z܊ZhHa!+vQ&IqC +e5:x,mNM!"V3Fm:_ɠ4B 7!V%$C${QZj]$iCb.K/[4٠]t%BF/܌+tm67E5s"IS#~e :hQd7fn~Əd6Бz@Jȕ IJyXY\Ddz(xD_ tV?OMOc?k :R^oOERXq GxjDQ;R+/"G] d, p8J+)OfߓR6Mc6V(}^TyBÎ+h,+ `\aQk!` c0%eVn> < qȆYGNUv.ǒyI_ '_ 'i Tk=?3|Y+W?|DKoy!لVNxC[g\o_WT99 4V;3mSc *@̭ Eh^+ (ĉ}VNNoF, fBU7+XW%e$f4Ac5w'7߹5Qq=,-2 oօ|i"ٖf#qNdu7*o5cV:yO@xyw..jVtYԐt(ܵC .`H,-j~er[G'HdS;KY(LŻqAf!AV:+JK%P#ʨ<ӎ+ O__{ˏ ݹaһC5r4R.:OC$z SFnGRe3i"XZUntMc` %ͫtVBq YuYu&)9)9FZo( zs$qYhʚ1EBQ!JaN̨e\R[y t8Epd Lіt(ndPnt!em]MtsfVZNVjPZm%bmKղ;Ly8F=\LP0Hb ZZ ĔxÿMKٻ%~lrXpNƵXQmCmm{mQ>n^8^h81kwC%IVxy㲆C*Jugy8T#Y,XۢDhڥ}CO5Lc 1Thl -co=0b\1EFSC,[7ųFֹ?PMIq=pt漋joz:S"mn1ajAª9|jvv]Gޜ%V]9Sr._}Ҹ+mGzภ 9uu"km-F5 |\~5e+TL"פ=PhWmg )lFOk*(x8P&WܛHE*8'iSX o(ӽ6-'+RO@txUdy,n!U0|d%)c . iR6ܮb_ G潜;km^9FW.$=B&.-nhm}!`.y L[Lg}2Jφ+Iu;IJki}(2. {qT0(;kəWkmGbQ-wS-6 Ns(TqK+cZqy>C Vcއ%:8*f(Ѡh=~Y({Ԯm!; }fGȏEņLD9D@ B 3+ &@" ("k\M^i9mh吼9X+f RjӻzB+lG 8)C/:-Fnbb0Rc)ҕ$7E' U'bw9?k5R:<3T Ŧ7Pos\U2X´3{TNj~G7AhXܼ0UFs^n AQ(/HJ84wVJ |w1CrےS\y+)m]JMI1w ճ7~iT,bBR$֛0&Y۵HpģR_#\QF؁|]Ai*t#hrɫs0Kn lZ`k .G3GWѺ+"u]Uldpfy$_Í(5K扆:EfVxcym׈ymq B0~Smgqz88^>{cjy6cozTBKB7N+wL䆗Gp~UtAΉJladୠd%+)\z߳ ]u(޴H.u(ɍqc`ܙ Jfp>Y*'s4WҘFNJŅ3X++T@Fk$Q52&44H OIKFnûw]'wzQ}3ֈ)F7NS}Ǿ#]_#5mk'6puI'%#$8aRIuX oDpPdY_˜al?ҢDƪ U4r4]po2\Xm*siIes[ea,=n2G5;hDo>&HYЋ'A#APU#xw;NGջVGr~Tiʋ% b j"]r9{|<m[#@\܂0aڶ#-<m)5ݲqk4QXp"BϑG:QrA ã荃(9"/܁Q}qWQ: oeiJyk PsSNz8Ed]m*Br%ZƇƛ2}f!h.k{h\da01yF+-xUv VkP]$ǐ};{9g5.?o@)9!YٝͳܙT6 G^0[(Oю*yG=K:8':Mc /el Y=Xk+ڽ]BYe  Qn 1'4jٌ{ْ4XR3=EZy{6k BWFgKYX7ӽo5.] &45e*}/N=誛*>j{~έ] 4 ܎('T_cSrP4ֵrJpYGN6,*\7 ToMA UZh=B5JZYGVc޹mP,Ǚ[8{K\B#e\8'O%G|1쵸(bU!W4kwvzLN(,V涏ʧZi a)jdwQG1YQ1Sh@Ā4~m+&!*j>H {#k/-@tY>G٫X9u2VH|xգAy-⮸Ӟi|˘Uq6qZd^d>HSEB@OXĒ37 0OBVh8o9, nH.:|6)+enB-aiA#$ޘs?6(6^{CqARQůQEqB塬\^omk\^GJȵ6_u/8ҀwFI#Deq_ڙ 0lolӊkY񿫮^t::-aUcYczmD<ͧ*/qv+GZۭ+Wj_Yɕ.עXmq+lrصS\쪥 ^/l;*;+fl}fCZo87:;up-kK[ތ’'Czd53apPYv Qfˬ܂I^{햩k,TP?wƙ/G:\$ ת'ahy/G7f O_Yif6iMV;iփz1ncf vX!zoEgq=R4 *gv~3;}3[U3#cD;*HDfwn;jۢ8\q2dM8QBdcnZ(ݶCQիW,cSRF(^ڃuƔZuֲ HYuV-dk_W궡\V6WUꝨ\7kup5kjm8+%]~uQMe+'D^鹼k CʷD =6mG7nS)?e69+)oOم[[,,BVZ xldu`Z;ѽCgCvwh ^~?1#*:-JGQ`xGX7ATk;ڮCNhN]4 KUqi`Jq*bF(G,IZ{ 89޿'QƝQVCiSMZ!O{z $B}A®2F_)DQ]9vۍw'7x9^--BsQm&_p`iBF9*S/Uq!<XZMO~4t)+_V.w&Cߒ1Zͫc{K8YѲ5C2Sh[%bvZoi^>74Ap%Ƹ2QJfڊ]l9,&o*>Wu=g<?0ǡkwV53j8[v`F8k$$7~IumHVM C]-^{Ӓyuۮ,Ȯh+srHSsݑ=,{rpܩjpsV&=Z"}{,9,V+i]o0캸,bs_S+CEpUJ7g)}#*b㤍qzsĴx˗zb(o  Qu=JRZP1Q P08ƀ9 vr_`Y4ҏP@]_k^ 7%*ˏc?PYB |2E\A(J#rQ5/2\Yu!\!gaE/ܵ" $.#N-Aܷpٙ//jW(ݢ3vq^xa0~`3 C3 Xj`%ġ%LqxSEtw|b8\KPXM:0VB-,4$7w=5c7&[ϙ9wҸ|Lbfj 5f2s1J#))jE2PosQ5!M#4:iKJ=\J3PQ1S oőb52 -%j10~ +pa)H^eX4b.UHGiw0̹İJ( J7A|¹Y5w [H^zDT;a܏ur`3ug \/сlF@T8?0 pxWF^5sA[d|jA5{:@ NmnH2TURaqaTgaɎ]`Zs=E;^+2@N[lSrq(w8wD0R) q)D9j0Йیʸ 67.KTTS\L. XGDD?zj3@KizRLd1kZKF{dn(Z;n6іH&Q{R40vKn\ L`f(>eʩèey FqsY;VqfR`fDsT x88±l\j9XwcHS % +qL*+#9T$\n*ZdWiGH ; hUb`F>Qܦ<1 \xjR" -**l3G*Aw-e21^=\nY^f*s J_._Z*)RV㸢y`նZ#)FD{xu[,6V CalM~c#.RZ8e" TȊPஈ](ꁵVb1sMD$(2:^Rjv3_L}BU枙nt)J} ր ]HȥԨP5 WnzAJpbqY5]Hcl1XRo[$ Y1vLBT%ٯ 2ZfVQK5%Gu ę[T^}a]L o2),8"Pi~~ڸ&K@(Y M˝!'x RXQH[>'jBK#=(1G/n7 F(Js .Om-G7XPi@t] #御}jW2_u!o#{  *t '4y#vH>|;Vg# @` 빵R>^Y}: zw,m[TpG /nayq&TXy ]k'/h<^7J W(}_jxԸ@[uHF^a RE 3P]V[gH,7)\M*bŃ/>J5m5(2 7c 02js%ho498ZD/r39hi@./~X4@Ú/ 8gґ@g 2|"\{5ʰ=L^ M 3&\kx"N︺[j(F?2 5M?5]{OpKeaG͜B9FT5KD 8K-iec63Ps ήU[)El L01N0iB?y|)(JHc#(IPLelDZ %9X0jjK*iy*,q=6wx []#nw ĭEMt1bѩ{ŗTL$?R` As 2 nTPKU.ᎴÈ;8߾ m_eӈ< 5.2L~_ABm^Zy1.yYG"VPV?!oV0$]Ft Ipy8`g7zz 0@t3ӄlyhNW&c+J Stܙ2]a1!&d8?$֗ 䗂[1JJd `.]c4r=Tgqp )fLn{aOX%.wO_][eC[DP@ESI*QaĹ-FԲ7[)^ 2g%ҝK;Q[⻬Ef^T9 jm1687Wmup#IV7C%%8F،aW1,vxK+j^ ik?yp+3*hӹk*墀 mkuYڨ4^*!N*T!!QwXc?yhIC*|j3f8R`G@a,6)3> TT-yZ@3lm(pM B09B̐ocig* )PdLŸ`>"7QԫWd,,Wb~EzVTZVb"S zIW)2\~"T hE4t( Dpg ~jьNA}rÀW߸)7s|{΢lT+=K> fӉ/E,YIZEHuz '0샵NT#!neD4.@YoP7/91QW zVXoԥ% ymjxj \WUmG#7úW y.&X2/ܲڣslØNu_.C(Ui[]4ނyj+ OvC80, ϛr#2,=:y/Y#U xh+Y(`? ,VF7.Ϙj6o;~*P-e_`0r}z&slxĦ;"հm(I`d K)K;"[\GX7L@ۉ@|Lù)V n K*{/G*bŷ1b|=c4N댼?O5KT~0C.<nbLc ^< dLo}]3%x5fzB(H0Ja b\Sw٫(m7ėgҊsm`y bmog%:0APOZWtQ9qPBrR9p4X0#C ~䡌1@G_r.\|KIQ>ǍՎ]lҼD0iT(JU~*ëC^W^NJ`-I_%-ye,.]v5Pt frmD̂sA/au(RneK+b8w1{ 3_R[M ˶eH,Z~~B.ɴ"6Xڻid5-1~eٽG8E17Qc]Pt& naNN2y8(YU!&qQϨ]*N*i2\jm L#]w-(srɐ֥w0(Ea2 ,#fc5j^9#)Hc5}G@ۖ!F-چք#RBs*mQhGX8\ETqFaes:г(İY8>B^-jaPI@\񇸰X 4֦GA7Ԯ*o HQ2j%OUKA@5vaBXx.|Òù/!aFwcU8frf|b zB h5zv+k2y'CW0[X4/6RL.q"*n*\0y *>`n*8S{Vq1X3/5b0mKaliKŖ^EQ| Im/¸xye, Jǘf?0p/L@:>6q:q8V).J,Jrsd&];o e⥆_2gTm?*ء…21YTR%0Յd59SqR{1/F'rql+= q.Tw O`S:˩V犧ȇQP(Zƙ+1.rRx'bnpZ] (/[wλw,s qXZbF9mӛ8|P.C3( 7-WuIRB6CͲGW,l.]BTlТTN}FH Vtkܥur7S=`x;ۤL[o8q?xX#1<})7E`C1.NaŇcg\Vo$CE~x$vl40`ķZ 14`0Z~f?M~c C!d l@ &P$i%i6 #!@!&FIM^*,#H aAlIIiP)@ Ahm)6,;eJ-L H SlL M e"K%d&I$E YH0 Cdh-[`dH,R%H@2,Le[)h(0DRmKEBHie6 (AXR H $X(&mA m%$l6l@P,h!AD#-!&LJ IP@"i%"ILH@mD&H!hBHI% iA0ACLIHDh6HmL!")Y@H$Y` A$Bi`m{iSh2HlEHDimh`2I Rhi$e6 (I)A6C L4Ii $&I$m6dd Sl$HL$R$ia4 %$-I mL%iK$iYl4AM4ID6! l2PAiClEi"EJ E6!m$mYaIcR-Z%gѯbY `=y2jeTi=@"4H $mHmHEL֓em$mEE&ZLdi[fi$x,Lg Fm(!1 0APQ@`aqp?N OWȟ]YtxKo#m8@ۯ,tr&u};w2>o7@ ! SϿN%~ߋWco8@^<|} _ @;m%skYph^QwO}a-'7{@v3{±w1??{߳efrIhval9?/Cg߀Fwk!Y}0X_$ 'wZP6.PysϏo[r^qt' NO1niWԾIM8'?lz <=rW/ô9s{SB]0{gނ}oNĶY>LФF\O}a~lݲsXb'@=ʐ> XSB݉Rw v^֛i^JX6egy`V!j.7)CV2$N]ھ.I.SZ8!C] A*L21!PYF7}F(؈l#2pPM1jperS$6#1b66p#RitE)JRnAݴC] QޏKJѳg~"1{mpN)!1A 0@QaPq`p?%7p=>|+$G w~a5*?2Ay!ҁa?¹|nîr&ay6:}8}D| 8D@9׌_N5oOR`R;HW?\ ;7>~$s3yy?.n @ h`4b<jǤ}Kt)6ikܽTl'^)=Ykw ^#I&#[ X1Jƙi$QE =vq2$Z<̆l=P(535i͖9L63bk2%~.,Mv4Uq#32%<oa1W xD#x/5& x= gb \-.y_޾p p/(!1AQaq 0?9 ھrKمR  匉t@uf412'b oi2aG-B}6,H v&h%{"DA C ј$[ J,2~ Cd{4R;zfgs 1^$#'M# ?^Q(ߩJ  cP>$|O;{-v#EHCO#RF| [ SRϯ@Y6*=.Y~"gGpX?seA"4`n`^HT^l`SDTSH$2&l!$0a`piT t܅O+vX6.1/jf$ $ UN"_b$pAWJZeF eF{ D]`CfQ`!3S 53Bw 5fGBLq,+pD1(Fuya"{!&PUp%W5ʙ(P&n>?aF 9inDn|2 ,a/'bP%ahZ#TXO|bY`` FԀCY8јɋEҷDnsS02Z!#͓IXC=9"dA夎w}90sR$S%)׃HsQhhf$a9P#@@9ȒY%pr kAS!y0АLс(>PdewBz0AՋEA =X(N_E>- DG(lAd K>AF r@=6: D, "&$%~ ʏ@C &jq t }@ytI 0H۰,D{ϨdKt1Ј!CA;fyWDX@RFKR `ODT}0A+FrKvq at7 Av&>€à{ #X!@ >:H7fCYAe@]$B|brJ~ς0Dhj @`B@tQFĈ6!EiI0.RH$@b%hj@.jH!FcH=G7h;|( MP0>͉mAڒ .KTH̗ Zj`:1, .Y$|Qd 1,<à#p@52,!ȜTC g w# 4pL%fXE5t!"@Hch t#k0ACHQ@Ӣ&7`Q$@[D'.N Qm6r 1gh yr"MREUh@ HZ̀A$_-"  q@x$ JG , N@4"hJKDPN ؇@b Gaf`ɑ 232@|95s;0b\v2ZA HG>$3!#  .Hw `ɀ](cs`ɍ2kIp; H2 'q&462drHv l8 : $2@H y2 #a 8MD΀w7"FFdb< # e(l!$C^͠Dd[D^-W Ȃ50INf15fFPfdʞ Mo܏ @] pDR.#dV> XHK$Y1B &$f 0RQ5lF E } # b*la09> ! E_Ȁ%N!u02g wH偄646NxedjDK SaB-xl\X NFH:Wd?H10 L e4"% t}ĽJ (RvA\(Am̌!/*[b0b@Ɋ M2"%ɩI (h.@}?0D$H%hIzGDrB!E?¶KQ6[ &"d2LG83[a!l6((}C`Us iIu/TPS~`Ѵ2."P@ALEI47f(Z"FwI0\FH-z b_c,M,$5#ms%:!c.z IJ4t!>”ILQ ؇gȒ% ?GTAH- F €(/j }K"{ْ+)@q$&c>d'I02b 8^c)@AD %TQDO N| G5o3Q1~aD]́GKx!>CfR; DfAl@k BЇqk"~Q tr"d`gA4([4> Px $* `*D(CRtQZZOy n `(C76#bT,ʓfc.;lyKi3%d)EFEdFײ(TIUCt5Pw,H #RD4e(Yi Y vpLEiLD~;?Ob,!4<=)xp+ `@2^#6"(v70 ufH),$t`dHj/9#;0 Sdӡf G H{̇;D9AB,Kp#F SB$m" > ؤ{  I:"< ),%H fX?%52 ۃv((Jd& t[G(Kp5 %ЃJn-ز6 ^SW X% ꞂU1iCMn"fY칥~}(LФOo3Q27^{ C0 t` `Ff.B?e#amei!.Zl(X6J $-2pF,Ѱ! J)oH[X$S"LXRr1SzLGINd.ьk2Ɂ'1y 2JO/z0diRأKKDA0$Q <$PpCL3p\ԒȋgrA$% dI0HI-D1}0q + HJ ?B;%g̍H@3nIIHDހ#Z7XzG~ ZLd)F4 a!8+ x~T2 ,`DFB n6R"ɈDD4&'+6If$oPQpG I| n "TNH$uZ𱙣KX-C@F CC n StIt10@" 3p̈4;4`,D<d33F~@rjaK3X嫸! ^."W/XkcAsa"CsάB";O$L`F%16,UDQ[-HΪoGx6dS#(#`] CJ'F#Ȉ3 Cu*'G 5 }5>LAiNǐʏ$O@Pp~ܕ30?FCfˠܒ=-\ %`1ݩz$I53&#Ñ#F"d#hu$u_ɐ@tl,2*$4/# DF<=f6V $eIK`Uf"! ,}@ȏdCyŸ>TbKȄ 9G`d3X؃},t0{#8$ y$% N4hCbdASG/%HsS70V١>DBYlHrb@aS(@2G (/v@ET3`[F@S[SM*f:M8IHa(BwkH%%tB:0`A!t$0`Ōp{Ј:F/, [y`w90|?  @CL$0̦~2d'$ٌ`5;^ubOD rCCa3D^;A 0_H Wd*[h'xn# Kc2|Ah q/drCpCX`  Q`Y;9 P:0_#`3 `Gn0 3lCI_Ez1rid ?f L  Cu> ,('h{Ne{fEPIrDCJ|1L2X D@@G3 }@`Gv[$DA'& b,> C`7#>J!مd?f&|1::G:6"0m%lirI3.`Cű@kQIIz%^Wg;@CȪ4 `hӁ0`S#v K2̻:2$\ L3 m;qb*Eٗ@ B Y$ϣ=p7Qa0C&sF8݆vBp2O190#F`Q"BH 2Sȍ}$AOyl ؍0fRf| Y؁rdMkF$vG D)@bfPIJT&G6g-d:Rv'wfmr,YC0GDlFHXJp۶d~ D6fPyBB:@ *yY?L{؃^:t  Y.{$Cc 3ČԈ0 _`A3FE*?d^B&H?@:! ;gFw/]=GPBK(8TTwL& 5*p!߿Ȃ7#3 ABB0J :Z(  -*LF6iwsa%b`5r@(m$G,Dd ,dA< dKؘ#@ &1F#(}@>.:QldFDɀ- r{3"5&q0"7gp#p!48D'^ 2`,% Fp`h@tgZM1\DЌ>CG@O, awg5q'-ol'F1!ݳD 2`LtlGc7QC t ACD35$",N7d3noQ C*h1ܬ$CrAMÅ- ndt TX 8Ͽ6iKU o;’iLenӒ\'JjXJwC#f , K"!I$"Lbkcd/DmXLSRD)Vs.q8;(iD#ܧq뉼j'r5S)&1M1+ [򴰠`vf%Rc}8)13vm6քMFQ\&|RWAȮ)e")CJו24" I[W/h]>f~1bO*|Yc;V #N'~`0 #j%"bkqI*v$oNP83f0;H *υh1~ɚ4@F/6,vy#'^܍68jL>oe+k_bmfgK2D$t,xa%9 HxAQb^ LGܷU&dЊO157ibeYk q2@~,zOГ LJ)Y5{\4[#RCZ7S=Qn0;GxShg3h܄!f<R[2j,d.F#Ƃv0=;PH[b,4`-LpI<(o J G^&f^ϖ(W̐("dFJJ'3YFcF$(Wa2 %1 s)y2@#D_^ CBZjgJ1Dh LeIT4$h'a6 -+40^,Xj#@%z R CI=,˟?%M8O&dX}i1#iJ4G_&ҸKY~G)|cYc94ҵtYhI)(jMKS*Y[NS{)sSͪfBP5~A.z`IJ4Q@HQcDD73tH&6V| PPݧ7vt2r7Fy:~,Charm-1.12.0/Charm/charmtimetracker.desktop000066400000000000000000000022501331066577000206060ustar00rootroot00000000000000[Desktop Entry] Name=Charm Name[bg]=Charm Name[de]=Charm Name[el]=Charm Name[en_GB]=Charm Name[eo]=Charm Name[es]=Charm Name[et]=Charm Name[fi]=Charm Name[fr]=Charme Name[gl]=Encanto Name[ja]=Charm Name[km]=Charm Name[nds]=Charm Name[pt]=Charm Name[pt_BR]=Charm Name[sv]=Charm Name[tr]=Charm Name[uk]=Шарм Name[x-test]=xxCharmxx GenericName=Time Tracker GenericName[bg]=Замерване на време GenericName[de]=Zeiterfassung GenericName[el]=Καταγραφέας χρόνου GenericName[en_GB]=Time Tracker GenericName[es]=Administrador de tiempo GenericName[et]=Ajaarvestus GenericName[fi]=Ajanseuranta GenericName[fr]=Outil de suivi du temps GenericName[gl]=Administrador do tempo GenericName[ja]=タイムトラッカー GenericName[km]=កម្មវិធី​តាមដាន​ពេលវេលា GenericName[nds]=Tietkieker GenericName[pt]=Gestor de Tempo GenericName[pt_BR]=Registro de tempo GenericName[sv]=Tidmätning GenericName[tr]=Zaman İzleyici GenericName[uk]=Інструмент стеження за часом GenericName[x-test]=xxTime Trackerxx Exec=charmtimetracker Icon=Charm Type=Application Categories=Qt;KDE;Utility; X-KDE-StartupNotify=true Charm-1.12.0/Charm/qt.conf.in.windows000066400000000000000000000000311331066577000172520ustar00rootroot00000000000000[Paths] Plugins = pluginsCharm-1.12.0/CharmCMake.h.cmake000066400000000000000000000020311331066577000160140ustar00rootroot00000000000000#ifndef CHARM_CMAKE_H #define CHARM_CMAKE_H #define CHARM_VERSION "@Charm_VERSION@" #include /* Define to the version from CMake */ static inline QString CharmVersion() { static const auto version = QStringLiteral("@Charm_VERSION@"); return version; } /* Define if you have enabled the idle detection */ #cmakedefine CHARM_IDLE_DETECTION /* Defined if idle detection is available on X11 or XCB*/ #cmakedefine CHARM_IDLE_DETECTION_AVAILABLE /* Delay for idle detection, default is 360 */ Q_CONSTEXPR static int CharmIdleTime = @CHARM_IDLE_TIME@; /* Define the url where to check for updates */ static inline QString CharmUpdateCheckUrl(){ static const auto url = QStringLiteral("@UPDATE_CHECK_URL@"); return url; } /* Defined if command interface is enabled */ #cmakedefine CHARM_CI_SUPPORT #ifdef CHARM_CI_SUPPORT /* Defined if TCP command interface is enabled */ #cmakedefine CHARM_CI_TCPSERVER /* Defined if local socket command interface is enabled */ #cmakedefine CHARM_CI_LOCALSERVER #endif #endif // CHARM_CMAKE_H Charm-1.12.0/Core/000077500000000000000000000000001331066577000135255ustar00rootroot00000000000000Charm-1.12.0/Core/CMakeLists.txt000066400000000000000000000012401331066577000162620ustar00rootroot00000000000000if(POLICY CMP0020) CMAKE_POLICY(SET CMP0020 NEW) endif() SET( CharmCore_SRCS CharmConstants.cpp CharmExceptions.cpp Controller.cpp Dates.cpp SqlRaiiTransactor.cpp SqLiteStorage.cpp MySqlStorage.cpp Configuration.cpp SqlStorage.cpp Event.cpp Task.cpp TaskListMerger.cpp State.cpp CharmDataModel.cpp TaskTreeItem.cpp TimeSpans.cpp CharmCommand.cpp SmartNameCache.cpp XmlSerialization.cpp CharmQtCompat.cpp ) ADD_LIBRARY( CharmCore STATIC ${CharmCore_SRCS} ) kde_target_enable_exceptions( CharmCore PUBLIC ) TARGET_LINK_LIBRARIES( CharmCore Qt5::Core Qt5::Widgets Qt5::Sql Qt5::Xml) Charm-1.12.0/Core/CharmCommand.cpp000066400000000000000000000045411331066577000165660ustar00rootroot00000000000000/* CharmCommand.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmCommand.h" #include "CommandEmitterInterface.h" #include CharmCommand::CharmCommand(const QString &description, QObject *parent) : QObject(parent) , m_description(description) { CommandEmitterInterface *emitter = dynamic_cast(parent); if (emitter) { m_owner = emitter; } else { Q_ASSERT_X(false, Q_FUNC_INFO, "CharmCommand widget pointers have to implement the " "CommandEmitterInterface."); } } CharmCommand::~CharmCommand() { } QString CharmCommand::description() const { return m_description; } bool CharmCommand::rollback(Controller *) { return false; } CommandEmitterInterface *CharmCommand::owner() const { return m_owner; } void CharmCommand::requestExecute() { Q_EMIT emitExecute(this); } void CharmCommand::requestRollback() { Q_EMIT emitRollback(this); } void CharmCommand::requestSlotEventIdChanged(int oldId, int newId) { Q_EMIT emitSlotEventIdChanged(oldId, newId); } void CharmCommand::showInformation(const QString &title, const QString &message) { QWidget *parent = dynamic_cast(owner()); Q_ASSERT(parent); QMessageBox::information(parent, title, message); } void CharmCommand::showCritical(const QString &title, const QString &message) { QWidget *parent = dynamic_cast(owner()); Q_ASSERT(parent); QMessageBox::critical(parent, title, message); } Charm-1.12.0/Core/CharmCommand.h000066400000000000000000000065311331066577000162340ustar00rootroot00000000000000/* CharmCommand.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARMCOMMAND_H #define CHARMCOMMAND_H #include class View; class Controller; class CommandEmitterInterface; /** CharmCommand encapsulates a command the view sends to the controller. A command is able to, for example, set the hour glass cursor on creation and restore the previous cursor on deletion, if it can be executed without user interaction. CharmCommand is the implementation of the command pattern in Charm. It holds the complete state of the requested operation. When the operation has finished, the object has to be deleted. The QObject parent has to implement the CommandEmitterInterface. Commands cannot be copied or assigned. After creating them, the View will send the command object to the controller. The controller will execute the necessary operations, and send the command back to the View. Objects that initiate view actions and therefore issue there own commands need to relay those to the view (relayCommand()). The View will call prepare() on the command before it is send to the controller. execute() is called by the controller. finalize() is called by the view after the controller has returned the command to the view. */ class CharmCommand : public QObject { Q_OBJECT public: explicit CharmCommand(const QString &description, QObject *parent = nullptr); ~CharmCommand() override; QString description() const; virtual bool prepare() = 0; virtual bool execute(Controller *controller) = 0; virtual bool rollback(Controller *controller); virtual bool finalize() = 0; CommandEmitterInterface *owner() const; //used by UndoCharmCommandWrapper to forward signal firing //forwards to emitExecute/emitRollback/emitRequestSlotEventIdChanged void requestExecute(); void requestRollback(); void requestSlotEventIdChanged(int oldId, int newId); //notify CharmCommands in a QUndoStack that an event ID has changed virtual void eventIdChanged(int, int) { } Q_SIGNALS: void emitExecute(CharmCommand *); void emitRollback(CharmCommand *); void emitSlotEventIdChanged(int, int); protected: void showInformation(const QString &title, const QString &message); void showCritical(const QString &title, const QString &message); private: CharmCommand(const CharmCommand &); // disallow copying CommandEmitterInterface *m_owner = nullptr; const QString m_description; }; #endif Charm-1.12.0/Core/CharmConstants.cpp000066400000000000000000000141341331066577000171630ustar00rootroot00000000000000/* CharmConstants.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: Olivier JG This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmConstants.h" #include "CharmDataModel.h" #include "Controller.h" #include #include const QString MetaKey_EventsInLeafsOnly = QStringLiteral("EventsInLeafsOnly"); const QString MetaKey_OneEventAtATime = QStringLiteral("OneEventAtATime"); const QString MetaKey_MainWindowGeometry = QStringLiteral("MainWindowGeometry"); const QString MetaKey_MainWindowVisible = QStringLiteral("MainWindowVisible"); const QString MetaKey_MainWindowGUIStateSelectedTask = QStringLiteral( "MainWindowGUIStateSelectedTask"); const QString MetaKey_MainWindowGUIStateExpandedTasks = QStringLiteral( "MainWindowGUIStateExpandedTasks"); const QString MetaKey_MainWindowGUIStateShowExpiredTasks = QStringLiteral( "MainWindowGUIStateShowExpiredTasks"); const QString MetaKey_MainWindowGUIStateShowCurrentTasks = QStringLiteral( "MainWindowGUIStateShowCurrentTasks"); const QString MetaKey_TimeTrackerGeometry = QStringLiteral("TimeTrackerGeometry"); const QString MetaKey_TimeTrackerVisible = QStringLiteral("TimeTrackerVisible"); const QString MetaKey_EventEditorGeometry = QStringLiteral("EventEditorGeometry"); const QString MetaKey_TaskEditorGeometry = QStringLiteral("TaskEditorGeometry"); const QString MetaKey_ReportsRecentSavePath = QStringLiteral("ReportsRecentSavePath"); const QString MetaKey_ExportToXmlRecentSavePath = QStringLiteral("ExportToXmlSavePath"); const QString MetaKey_TimesheetSubscribedOnly = QStringLiteral("TimesheetSubscribedOnly"); const QString MetaKey_TimesheetActiveOnly = QStringLiteral("TimesheetActiveOnly"); const QString MetaKey_TimesheetRootTask = QStringLiteral("TimesheetRootTask"); const QString MetaKey_LastEventEditorDateTime = QStringLiteral("LastEventEditorDateTime"); const QString MetaKey_Key_InstallationId = QStringLiteral("InstallationId"); const QString MetaKey_Key_UserName = QStringLiteral("UserName"); const QString MetaKey_Key_UserId = QStringLiteral("UserId"); const QString MetaKey_Key_LocalStorageDatabase = QStringLiteral("LocalStorageDatabase"); const QString MetaKey_Key_LocalStorageType = QStringLiteral("LocalStorageType"); const QString MetaKey_Key_SubscribedTasksOnly = QStringLiteral("SubscribedTasksOnly"); const QString MetaKey_Key_TimeTrackerFontSize = QStringLiteral("TimeTrackerFontSize"); const QString MetaKey_Key_24hEditing = QStringLiteral("Key24hEditing"); const QString MetaKey_Key_DurationFormat = QStringLiteral("DurationFormat"); const QString MetaKey_Key_IdleDetection = QStringLiteral("IdleDetection"); const QString MetaKey_Key_WarnUnuploadedTimesheets = QStringLiteral("WarnUnuploadedTimesheets"); const QString MetaKey_Key_RequestEventComment = QStringLiteral("RequestEventComment"); const QString MetaKey_Key_ToolButtonStyle = QStringLiteral("ToolButtonStyle"); const QString MetaKey_Key_ShowStatusBar = QStringLiteral("ShowStatusBar"); const QString MetaKey_Key_EnableCommandInterface = QStringLiteral("EnableCommandInterface"); const QString MetaKey_Key_NumberOfTaskSelectorEntries = QStringLiteral("NumberOfTaskSelectorEntries"); const QString TrueString(QStringLiteral("true")); const QString FalseString(QStringLiteral("false")); const QString &stringForBool(bool val) { return val ? TrueString : FalseString; } void connectControllerAndModel(Controller *controller, CharmDataModel *model) { QObject::connect(controller, SIGNAL(eventAdded(Event)), model, SLOT(addEvent(Event))); QObject::connect(controller, SIGNAL(eventModified(Event)), model, SLOT(modifyEvent(Event))); QObject::connect(controller, SIGNAL(eventDeleted(Event)), model, SLOT(deleteEvent(Event))); QObject::connect(controller, SIGNAL(allEvents(EventList)), model, SLOT(setAllEvents(EventList))); QObject::connect(controller, SIGNAL(definedTasks(TaskList)), model, SLOT(setAllTasks(TaskList))); QObject::connect(controller, SIGNAL(taskAdded(Task)), model, SLOT(addTask(Task))); QObject::connect(controller, SIGNAL(taskUpdated(Task)), model, SLOT(modifyTask(Task))); QObject::connect(controller, SIGNAL(taskDeleted(Task)), model, SLOT(deleteTask(Task))); } static QString formatDecimal(double d) { const QString s = QLocale::system().toString(d, 'f', 2); if (d > -10 && d < 10) { //hack to get the hours always have two decimals: e.g. 00.50 instead of 0.50 return QStringLiteral("0") + s; } else { return s; } } QString hoursAndMinutes(int duration) { if (duration == 0) { if (CONFIGURATION.durationFormat == Configuration::Minutes) { return QObject::tr("00:00"); } else { return formatDecimal(0.0); } } int minutes = duration / 60; int hours = minutes / 60; minutes = minutes % 60; if (CONFIGURATION.durationFormat == Configuration::Minutes) { QString text; QTextStream stream(&text); stream << qSetFieldWidth(2) << qSetPadChar(QLatin1Char('0')) << hours << qSetFieldWidth(0) << ":" << qSetFieldWidth(2) << minutes; return text; } else { //Decimal return formatDecimal(hours + minutes / 60.0); } } Charm-1.12.0/Core/CharmConstants.h000066400000000000000000000105641331066577000166330ustar00rootroot00000000000000/* CharmConstants.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: Olivier JG This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARMCONSTANTS_H #define CHARMCONSTANTS_H #include #include "CharmDataModel.h" #include "Configuration.h" // increment when SQL DB format changes: #define CHARM_DATABASE_VERSION_DESCRIPTOR QStringLiteral("CharmDatabaseSchemaVersion") #define CHARM_DATABASE_VERSION_BEFORE_TASK_EXPIRY 2 #define CHARM_DATABASE_VERSION_BEFORE_TRACKABLE 3 #define CHARM_DATABASE_VERSION_BEFORE_COMMENT 4 #define CHARM_DATABASE_VERSION 5 #define REQUIRED_CHARM_DATABASE_VERSION CHARM_DATABASE_VERSION // FIXME this may have to go into some plugin configuration later: // FIXME also, we may need some verbose descriptors for configuration #define CHARM_SQLITE_BACKEND_DESCRIPTOR QStringLiteral("sqlite") #define CHARM_MYSQL_BACKEND_DESCRIPTOR QStringLiteral("mysql") // Metadata and QSettings Keys: extern const QString MetaKey_MainWindowGeometry; extern const QString MetaKey_MainWindowVisible; extern const QString MetaKey_MainWindowGUIStateSelectedTask; extern const QString MetaKey_MainWindowGUIStateExpandedTasks; extern const QString MetaKey_MainWindowGUIStateShowExpiredTasks; extern const QString MetaKey_MainWindowGUIStateShowCurrentTasks; extern const QString MetaKey_TimeTrackerGeometry; extern const QString MetaKey_TimeTrackerVisible; extern const QString MetaKey_EventEditorGeometry; extern const QString MetaKey_TaskEditorGeometry; extern const QString MetaKey_ReportsRecentSavePath; extern const QString MetaKey_ExportToXmlRecentSavePath; extern const QString MetaKey_TimesheetActiveOnly; extern const QString MetaKey_TimesheetSubscribedOnly; extern const QString MetaKey_TimesheetRootTask; extern const QString MetaKey_LastEventEditorDateTime; extern const QString MetaKey_Key_InstallationId; extern const QString MetaKey_Key_UserName; extern const QString MetaKey_Key_UserId; extern const QString MetaKey_Key_LocalStorageDatabase; extern const QString MetaKey_Key_LocalStorageType; extern const QString MetaKey_Key_SubscribedTasksOnly; extern const QString MetaKey_Key_TimeTrackerFontSize; extern const QString MetaKey_Key_DurationFormat; extern const QString MetaKey_Key_IdleDetection; extern const QString MetaKey_Key_WarnUnuploadedTimesheets; extern const QString MetaKey_Key_RequestEventComment; extern const QString MetaKey_Key_ToolButtonStyle; extern const QString MetaKey_Key_ShowStatusBar; extern const QString MetaKey_Key_EnableCommandInterface; extern const QString MetaKey_Key_NumberOfTaskSelectorEntries; extern const QString TrueString; extern const QString FalseString; #define CONFIGURATION (Configuration::instance()) // helper functions to persist meta data: template T strToT(const QString &str); template<> inline int strToT(const QString &str) { bool ok; int ret = str.toInt(&ok); Q_ASSERT(ok); Q_UNUSED(ok); return ret; } template<> inline bool strToT(const QString &str) { return str.simplified() == TrueString; } #define INT_CONFIG_TYPE(TYPE) \ template<> inline TYPE strToT(const QString &str) \ { return static_cast(strToT(str)); } INT_CONFIG_TYPE(Configuration::TimeTrackerFontSize) INT_CONFIG_TYPE(Configuration::DurationFormat) INT_CONFIG_TYPE(Configuration::TaskPrefilteringMode) INT_CONFIG_TYPE(Qt::ToolButtonStyle) const QString &stringForBool(bool val); class Controller; class CharmDataModel; void connectControllerAndModel(Controller *, CharmDataModel *); // helpers: /** A string containing hh:mm for the given duration of seconds. */ QString hoursAndMinutes(int seconds); #endif Charm-1.12.0/Core/CharmDataModel.cpp000066400000000000000000000506021331066577000170410ustar00rootroot00000000000000/* CharmDataModel.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: David Faure Author: Mike McQuaid Author: Guillermo A. Amaral Author: Allen Winter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmDataModel.h" #include "CharmConstants.h" #include "Configuration.h" #include #include #include #include #include #include #include #include #include #include CharmDataModel::CharmDataModel() : QObject() { connect(&m_timer, SIGNAL(timeout()), SLOT(eventUpdateTimerEvent())); } CharmDataModel::~CharmDataModel() { m_adapters.clear(); setAllTasks(TaskList()); } void CharmDataModel::stateChanged(State previous, State next) { if (previous == Connected && next == Disconnecting) { Q_FOREACH (EventId id, m_activeEventIds) { const Event &event = findEvent(id); const Task &task = findTask(event.taskId()); Q_ASSERT(task.isValid()); endEventRequested(task); } setAllTasks(TaskList()); } } void CharmDataModel::registerAdapter(CharmDataModelAdapterInterface *adapter) { m_adapters.append(adapter); adapter->resetEvents(); } void CharmDataModel::unregisterAdapter(CharmDataModelAdapterInterface *adapter) { Q_ASSERT(m_adapters.contains(adapter)); m_adapters.removeAll(adapter); } void CharmDataModel::setAllTasks(const TaskList &tasks) { clearTasks(); Q_ASSERT(Task::checkForTreeness(tasks)); Q_ASSERT(Task::checkForUniqueTaskIds(tasks)); // fill the tasks into the map to TaskTreeItems for (int i = 0; i < tasks.size(); ++i) { const TaskTreeItem item(tasks[i], &m_rootItem); Q_ASSERT(!taskExists(tasks[i].id())); // the tasks form a tree and have unique task ids m_tasks[ tasks[i].id() ] = item; } // create parent-child-relationships: for (TaskTreeItem::Map::iterator it = m_tasks.begin(); it != m_tasks.end(); ++it) { const Task &task = it->second.task(); TaskTreeItem &parent = parentItem(task); it->second.makeChildOf(parent); } // store task id length: determineTaskPaddingLength(); m_nameCache.setAllTasks(tasks); // notify adapters of changes for_each(m_adapters.begin(), m_adapters.end(), std::mem_fun(&CharmDataModelAdapterInterface::resetTasks)); emit resetGUIState(); } void CharmDataModel::addTask(const Task &task) { Q_ASSERT_X(!taskExists(task.id()), Q_FUNC_INFO, "New tasks need to have a unique task id"); if (task.isValid() && !taskExists(task.id())) { const TaskTreeItem &parent = taskTreeItem(task.parent()); Q_FOREACH (auto adapter, m_adapters) adapter->taskAboutToBeAdded(parent.task().id(), parent.childCount()); const TaskTreeItem item(task); m_tasks[ task.id() ] = item; m_nameCache.addTask(task); // the item in the map has a different address, let's find it: Q_ASSERT(taskExists(task.id())); // we just put it in const auto it = m_tasks.find(task.id()); it->second.makeChildOf(parentItem(task)); determineTaskPaddingLength(); // regenerateSmartNames(); Q_FOREACH (auto adapter, m_adapters) adapter->taskAdded(task.id()); } else { qCritical() << "CharmDataModel::addTask: duplicate task id" << task.id() << "ignored. THIS IS A BUG"; } } void CharmDataModel::modifyTask(const Task &task) { const auto it = m_tasks.find(task.id()); Q_ASSERT_X(it != m_tasks.end(), Q_FUNC_INFO, "Task to modify has to exist"); if (it == m_tasks.end()) return; const TaskId oldParentId = it->second.task().parent(); const bool parentChanged = task.parent() != oldParentId; if (parentChanged) { Q_FOREACH (auto adapter, m_adapters) adapter->taskParentChanged(task.id(), oldParentId, task.parent()); m_tasks[ task.id() ].makeChildOf(parentItem(task)); } m_tasks[ task.id() ].task() = task; m_nameCache.modifyTask(task); if (parentChanged) { Q_FOREACH (auto adapter, m_adapters) adapter->resetTasks(); } else { Q_FOREACH (auto adapter, m_adapters) adapter->taskModified(task.id()); } } void CharmDataModel::deleteTask(const Task &task) { Q_ASSERT_X(taskExists(task.id()), Q_FUNC_INFO, "Task to delete has to exist"); Q_ASSERT_X(taskTreeItem(task.id()).childCount() == 0, Q_FUNC_INFO, "Cannot delete a task that has children"); Q_FOREACH (auto adapter, m_adapters) adapter->taskAboutToBeDeleted(task.id()); const auto it = m_tasks.find(task.id()); if (it != m_tasks.end()) { TaskTreeItem tmpParent; it->second.makeChildOf(tmpParent); m_tasks.erase(it); } m_nameCache.deleteTask(task); Q_FOREACH (auto adapter, m_adapters) adapter->taskDeleted(task.id()); } void CharmDataModel::clearTasks() { // to clear the task list, all tasks have first to be changed to be children of the root item: for (TaskTreeItem::Map::iterator it = m_tasks.begin(); it != m_tasks.end(); ++it) it->second.makeChildOf(m_rootItem); m_tasks.clear(); m_nameCache.clearTasks(); m_rootItem = TaskTreeItem(); Q_FOREACH (auto adapter, m_adapters) adapter->resetTasks(); } void CharmDataModel::setAllEvents(const EventList &events) { m_events.clear(); for (int i = 0; i < events.size(); ++i) { if (!eventExists(events[i].id())) { m_events[ events[i].id() ] = events[i]; } else { qCritical() << "CharmDataModel::addTask: duplicate task id" << m_tasks[i].task().id() << "ignored. THIS IS A BUG"; } } Q_FOREACH (auto adapter, m_adapters) adapter->resetEvents(); } void CharmDataModel::addEvent(const Event &event) { Q_ASSERT_X(!eventExists(event.id()), Q_FUNC_INFO, "New event must have a unique id"); Q_FOREACH (auto adapter, m_adapters) adapter->eventAboutToBeAdded(event.id()); m_events[ event.id() ] = event; Q_FOREACH (auto adapter, m_adapters) adapter->eventAdded(event.id()); } void CharmDataModel::modifyEvent(const Event &newEvent) { Q_ASSERT_X(eventExists(newEvent.id()), Q_FUNC_INFO, "Event to modify has to exist"); const Event oldEvent = eventForId(newEvent.id()); m_events[ newEvent.id() ] = newEvent; Q_FOREACH (auto adapter, m_adapters) adapter->eventModified(newEvent.id(), oldEvent); } void CharmDataModel::deleteEvent(const Event &event) { Q_ASSERT_X(eventExists(event.id()), Q_FUNC_INFO, "Event to delete has to exist"); Q_ASSERT_X(!m_activeEventIds.contains(event.id()), Q_FUNC_INFO, "Cannot delete an active event"); Q_FOREACH (auto adapter, m_adapters) adapter->eventAboutToBeDeleted(event.id()); const auto it = m_events.find(event.id()); if (it != m_events.end()) m_events.erase(it); Q_FOREACH (auto adapter, m_adapters) adapter->eventDeleted(event.id()); } void CharmDataModel::clearEvents() { m_events.clear(); Q_FOREACH (auto adapter, m_adapters) adapter->resetEvents(); } const TaskTreeItem &CharmDataModel::taskTreeItem(TaskId id) const { if (id <= 0) return m_rootItem; const auto it = m_tasks.find(id); if (it == m_tasks.end()) { return m_rootItem; } else { return it->second; } } const Task &CharmDataModel::getTask(TaskId id) const { const TaskTreeItem &item = taskTreeItem(id); return item.task(); } TaskList CharmDataModel::getAllTasks() const { return m_rootItem.children(); } Task &CharmDataModel::findTask(TaskId id) { // in this (private) method, the task has to exist const TaskTreeItem::Map::iterator it = m_tasks.find(id); Q_ASSERT(it != m_tasks.end()); return it->second.task(); } const Event &CharmDataModel::eventForId(EventId id) const { static const Event InvalidEvent; EventMap::const_iterator it = m_events.find(id); if (it != m_events.end()) { return it->second; } else { return InvalidEvent; } } Event &CharmDataModel::findEvent(int id) { // in this method, the event has to exist const auto it = m_events.find(id); Q_ASSERT(it != m_events.end()); return it->second; } bool CharmDataModel::activateEvent(const Event &activeEvent) { const bool DoSanityChecks = true; if (DoSanityChecks) { TaskId taskId = activeEvent.taskId(); // this check may become obsolete: for (int i = 0; i < m_activeEventIds.size(); ++i) { if (m_activeEventIds[i] == activeEvent.id()) { Q_ASSERT(!"inconsistency (event already active)!"); return false; } const Event &e = eventForId(m_activeEventIds[i]); if (e.taskId() == taskId) { Q_ASSERT(!"inconsistency (event already active for task)!"); return false; } } } m_activeEventIds << activeEvent.id(); Q_FOREACH (auto adapter, m_adapters) adapter->eventActivated(activeEvent.id()); m_timer.start(10000); return true; } void CharmDataModel::determineTaskPaddingLength() { int maxTaskId = 0; for (TaskTreeItem::Map::iterator it = m_tasks.begin(); it != m_tasks.end(); ++it) maxTaskId = qMax(maxTaskId, it->second.task().id()); QString temp; temp.setNum(maxTaskId); CONFIGURATION.taskPaddingLength = temp.length(); } TaskTreeItem &CharmDataModel::parentItem(const Task &task) { TaskTreeItem &parent = m_tasks[ task.parent() ]; if (parent.isValid()) { return parent; } else { return m_rootItem; } } bool CharmDataModel::taskExists(TaskId id) { return m_tasks.find(id) != m_tasks.end(); } bool CharmDataModel::eventExists(EventId id) { return m_events.find(id) != m_events.end(); } bool CharmDataModel::isTaskActive(TaskId id) const { for (int i = 0; i < m_activeEventIds.size(); ++i) { const Event &e = eventForId(m_activeEventIds[i]); Q_ASSERT(e.isValid()); if (e.taskId() == id) return true; } return false; } const Event &CharmDataModel::activeEventFor(TaskId id) const { static Event InvalidEvent; for (int i = 0; i < m_activeEventIds.size(); ++i) { const Event &e = eventForId(m_activeEventIds[i]); if (e.taskId() == id) return e; } return InvalidEvent; } void CharmDataModel::startEventRequested(const Task &task) { // respect configuration: if (!m_activeEventIds.isEmpty()) endAllEventsRequested(); // clear the "last event editor datetime" so that the next manual "create event" // doesn't use some old date QSettings settings; settings.remove(MetaKey_LastEventEditorDateTime); emit makeAndActivateEvent(task); updateToolTip(); } void CharmDataModel::endEventRequested(const Task &task) { EventId eventId = 0; // find the event in the list of active events and remove it: for (int i = 0; i < m_activeEventIds.size(); ++i) { if (eventForId(m_activeEventIds[i]).taskId() == task.id()) { eventId = m_activeEventIds[i]; m_activeEventIds.removeAt(i); Q_FOREACH (auto adapter, m_adapters) adapter->eventDeactivated(eventId); break; } } Q_ASSERT(eventId != 0); Event &event = findEvent(eventId); Event old = event; event.setEndDateTime(QDateTime::currentDateTime()); emit requestEventModification(event, old); if (m_activeEventIds.isEmpty()) m_timer.stop(); updateToolTip(); } void CharmDataModel::endAllEventsRequested() { QDateTime currentDateTime = QDateTime::currentDateTime(); while (!m_activeEventIds.isEmpty()) { EventId eventId = m_activeEventIds.first(); m_activeEventIds.pop_front(); Q_FOREACH (auto adapter, m_adapters) adapter->eventDeactivated(eventId); Q_ASSERT(eventId != 0); Event &event = findEvent(eventId); Event old = event; event.setEndDateTime(currentDateTime); emit requestEventModification(event, old); } m_timer.stop(); updateToolTip(); } void CharmDataModel::eventUpdateTimerEvent() { Q_FOREACH (EventId id, m_activeEventIds) { // Not a ref (Event &), since we want to diff "old event" // and "new event" in *Adapter::eventModified Event event = findEvent(id); Event old = event; event.setEndDateTime(QDateTime::currentDateTime()); emit requestEventModification(event, old); } updateToolTip(); } QString CharmDataModel::fullTaskName(const Task &task) const { if (task.isValid()) { QString name = task.name().simplified(); if (task.parent() != 0) { const Task &parent = getTask(task.parent()); if (parent.isValid()) name = fullTaskName(parent) + QLatin1Char('/') + name; } return name; } else { // qWarning() << "CharmReport::tasknameWithParents: WARNING: invalid task" // << task.id(); return QString(); } } QString CharmDataModel::smartTaskName(const Task &task) const { return m_nameCache.smartName(task.id()); } QString CharmDataModel::eventsString() const { QStringList eStrList; Q_FOREACH (EventId eventId, activeEvents()) { Event event = eventForId(eventId); if (event.isValid()) { const Task &task = getTask(event.taskId()); const int taskIdLength = CONFIGURATION.taskPaddingLength; eStrList <%1 for %2 active events:
    %3") .arg(totalDurationString()).arg(numEvents).arg(eventsString()); break; } emit sysTrayUpdate(toolTip, numEvents != 0); } const EventMap &CharmDataModel::eventMap() const { return m_events; } bool CharmDataModel::isEventActive(EventId id) const { return m_activeEventIds.contains(id); } int CharmDataModel::activeEventCount() const { return m_activeEventIds.count(); } EventIdList CharmDataModel::eventsThatStartInTimeFrame(const QDate &start, const QDate &end) const { // do the comparisons in UTC, which is much faster as we only need to convert // start and end date then const QDateTime startUTC = QDateTime(start, QTime(0, 0, 0)).toUTC(); const QDateTime endUTC = QDateTime(end, QTime(0, 0, 0)).toUTC(); EventIdList events; EventMap::const_iterator it; for (it = m_events.begin(); it != m_events.end(); ++it) { const Event &event(it->second); if (event.startDateTime(Qt::UTC) >= startUTC && event.startDateTime(Qt::UTC) < endUTC) events << event.id(); } return events; } EventIdList CharmDataModel::eventsThatStartInTimeFrame(const TimeSpan &timeSpan) const { return eventsThatStartInTimeFrame(timeSpan.first, timeSpan.second); } bool CharmDataModel::isParentOf(TaskId parent, TaskId id) const { Q_ASSERT_X(parent != 0, Q_FUNC_INFO, "parent is invalid (0)"); if (id == parent) return false; // a task is not it's own child // get the item, make sure it is valid const TaskTreeItem &item(taskTreeItem(id)); Q_ASSERT_X(item.isValid(), Q_FUNC_INFO, "No such task"); if (!item.isValid()) return false; TaskId parentId = item.task().parent(); if (parentId == parent) return true; // found it on the path if (parentId == 0) return false; // the task has no parent return isParentOf(parent, parentId); } EventIdList CharmDataModel::activeEvents() const { return m_activeEventIds; } TaskIdList CharmDataModel::mostFrequentlyUsedTasks() const { std::unordered_map mfuMap; const EventMap& events = eventMap(); for( auto it : events) { mfuMap[it.second.taskId()]++; } const auto comp = []( quint32 a, quint32 b ){ return a > b; }; std::map mfu( comp ); for ( const auto kv : mfuMap ) { mfu[kv.second] = kv.first; } TaskIdList out; out.reserve( static_cast( mfu.size() ) ); std::transform( mfu.cbegin(), mfu.cend(), std::inserter( out, out.begin() ), []( const std::pair &in ) { return in.second; }); return out; } TaskIdList CharmDataModel::mostRecentlyUsedTasks() const { std::unordered_map mruMap; const EventMap& events = eventMap(); for( const auto &it : events ) { const TaskId id = it.second.taskId(); if ( id == 0 ) continue; // process use date // Note: for a relative order, the UTC time is sufficient and much faster const QDateTime date = it.second.startDateTime( Qt::UTC ); const auto old = mruMap.find( id ); if ( old != mruMap.cend() ) { mruMap[id]= qMax( old->second, date ); } else { mruMap[id]= date; } } const auto comp = [] ( const QDateTime &a, const QDateTime &b ) { return a > b; }; std::map mru( comp ); for ( const auto kv : mruMap ) { mru[kv.second] = kv.first; } TaskIdList out; out.reserve( static_cast( mru.size() ) ); std::transform( mru.cbegin(), mru.cend(), std::inserter( out, out.begin() ), []( const std::pair &in ) { return in.second; }); return out; } bool CharmDataModel::operator==(const CharmDataModel &other) const { // not compared: m_timer, m_adapters if (&other == this) return true; return getAllTasks() == other.getAllTasks() && m_events == other.m_events && m_activeEventIds == other.m_activeEventIds; } CharmDataModel *CharmDataModel::clone() const { auto c = new CharmDataModel(); c->setAllTasks(getAllTasks()); c->m_events = m_events; c->m_activeEventIds = m_activeEventIds; return c; } Charm-1.12.0/Core/CharmDataModel.h000066400000000000000000000147021331066577000165070ustar00rootroot00000000000000/* CharmDataModel.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: Allen Winter Author: David Faure This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARMDATAMODEL_H #define CHARMDATAMODEL_H #include #include #include "Task.h" #include "State.h" #include "Event.h" #include "TimeSpans.h" #include "TaskTreeItem.h" #include "CharmDataModelAdapterInterface.h" #include "SmartNameCache.h" class QAbstractItemModel; /** CharmDataModel is the application's model. CharmDataModel holds all data that makes up the application's current data space: the list of tasks, the list of events, and the list of active (currently timed) events. It will notify all registered CharmDataModelAdapterInterfaces about changes in the model. Those interfaces could, for example, implement QAbstractItemModel. */ class CharmDataModel : public QObject { Q_OBJECT friend class ImportExportTests; public: CharmDataModel(); ~CharmDataModel() override; void stateChanged(State previous, State next); /** Register a CharmDataModelAdapterInterface. */ void registerAdapter(CharmDataModelAdapterInterface *); /** Unregister a CharmDataModelAdapterInterface. */ void unregisterAdapter(CharmDataModelAdapterInterface *); /** Retrieve a task for the given task id. If called with Zero as the task id, it will return the imaginary root that has all top-levels as it's children. */ const TaskTreeItem &taskTreeItem(TaskId id) const; /** Convenience method: retrieve the task directly. */ const Task &getTask(TaskId id) const; /** Get all tasks as a TaskList. Warning: this might be slow. */ TaskList getAllTasks() const; /** Retrieve an event for the given event id. */ const Event &eventForId(EventId id) const; /** Constant access to the map of events. */ const EventMap &eventMap() const; /** * Get all events that start in a given time frame (e.g. a given day, a given week etc.) * More precisely, all events that start at or after @p start, and start before @p end (@p end excluded!) */ EventIdList eventsThatStartInTimeFrame(const QDate &start, const QDate &end) const; // convenience overload EventIdList eventsThatStartInTimeFrame(const TimeSpan &timeSpan) const; const Event &activeEventFor(TaskId id) const; EventIdList activeEvents() const; int activeEventCount() const; TaskTreeItem &parentItem(const Task &task); // FIXME const??? bool taskExists(TaskId id); /** True if task is in the subtree below parent. * parent is not element of the subtree, and thus not it's own child. */ bool isParentOf(TaskId parent, TaskId task) const; // handling of active events: /** Is an event active for the task with this id? */ bool isTaskActive(TaskId id) const; /** Is this event active? */ bool isEventActive(EventId id) const; /** Start a new event with this task. */ void startEventRequested(const Task &); /** Stop the active event for this task. */ void endEventRequested(const Task &); /** Stop all tasks. */ void endAllEventsRequested(); /** Activate this event. */ bool activateEvent(const Event &); /** Provide a list of the most frequently used tasks. * Only tasks that have been used so far will be taken into account, so the list might be empty. */ TaskIdList mostFrequentlyUsedTasks() const; /** Provide a list of the most recently used tasks. * Only tasks that have been used so far will be taken into account, so the list might be empty. */ TaskIdList mostRecentlyUsedTasks() const; /** Create a full task name from the specified TaskId. */ QString fullTaskName(const Task &) const; /** Create a "smart" task name (name and shortest path that makes the name unique) from the specified TaskId. */ QString smartTaskName(const Task &) const; /** Get the task id and full name as a single string. */ QString taskIdAndFullNameString(TaskId id) const; /** Get the task id and name as a single string. */ QString taskIdAndNameString(TaskId id) const; /** Get the task id and smart name as a single string. */ QString taskIdAndSmartNameString(TaskId id) const; bool operator==(const CharmDataModel &other) const; Q_SIGNALS: // these need to be implemented in the respective application to // be able to track time: void makeAndActivateEvent(const Task &); void requestEventModification(const Event &, const Event &); void sysTrayUpdate(const QString &, bool); void resetGUIState(); public Q_SLOTS: void setAllTasks(const TaskList &tasks); void addTask(const Task &); void modifyTask(const Task &); void deleteTask(const Task &); void clearTasks(); void setAllEvents(const EventList &events); void addEvent(const Event &); void modifyEvent(const Event &); void deleteEvent(const Event &); void clearEvents(); private: void determineTaskPaddingLength(); bool eventExists(EventId id); Task &findTask(TaskId id); Event &findEvent(EventId id); int totalDuration() const; QString eventsString() const; QString totalDurationString() const; void updateToolTip(); TaskTreeItem::Map m_tasks; TaskTreeItem m_rootItem; EventMap m_events; EventIdList m_activeEventIds; // adapters are notified when the model changes CharmDataModelAdapterList m_adapters; // event update timer: QTimer m_timer; SmartNameCache m_nameCache; private Q_SLOTS: void eventUpdateTimerEvent(); private: // functions only used for testing: CharmDataModel *clone() const; }; #endif Charm-1.12.0/Core/CharmDataModelAdapterInterface.h000066400000000000000000000040541331066577000216300ustar00rootroot00000000000000/* CharmDataModelAdapterInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARMDATAMODELADAPTERINTERFACE_H #define CHARMDATAMODELADAPTERINTERFACE_H #include "Task.h" #include "Event.h" #include class CharmDataModelAdapterInterface { public: // keep compiler happy: virtual ~CharmDataModelAdapterInterface() { } virtual void resetTasks() = 0; virtual void taskAboutToBeAdded(TaskId parent, int pos) = 0; virtual void taskAdded(TaskId id) = 0; virtual void taskModified(TaskId id) = 0; virtual void taskParentChanged(TaskId task, TaskId oldParent, TaskId newParent) = 0; virtual void taskAboutToBeDeleted(TaskId) = 0; virtual void taskDeleted(TaskId id) = 0; virtual void resetEvents() = 0; virtual void eventAboutToBeAdded(EventId id) = 0; virtual void eventAdded(EventId id) = 0; // we only pass an event because it is an outdated object: virtual void eventModified(EventId id, Event discardedEvent) = 0; virtual void eventAboutToBeDeleted(EventId id) = 0; virtual void eventDeleted(EventId id) = 0; virtual void eventActivated(EventId id) = 0; virtual void eventDeactivated(EventId id) = 0; }; typedef QList CharmDataModelAdapterList; #endif Charm-1.12.0/Core/CharmExceptions.cpp000066400000000000000000000032121331066577000173230ustar00rootroot00000000000000/* CharmExceptions.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include CharmException::CharmException(const QString &message) : m_message(message) { } QString CharmException::what() const throw() { return m_message; } ParseError::ParseError(const QString &text) : CharmException(text) { } XmlSerializationException:: XmlSerializationException(const QString &message) : CharmException(message) { } UnsupportedDatabaseVersionException::UnsupportedDatabaseVersionException(const QString &message) : CharmException(message) { } InvalidTaskListException::InvalidTaskListException(const QString &message) : CharmException(message) { } TransactionException::TransactionException(const QString &text) : CharmException(text) { } AlreadyRunningException::AlreadyRunningException() : CharmException(QString()) { } Charm-1.12.0/Core/CharmExceptions.h000066400000000000000000000035131331066577000167740ustar00rootroot00000000000000/* CharmExceptions.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARMEXCEPTIONS_H #define CHARMEXCEPTIONS_H #include class CharmException { public: explicit CharmException(const QString &message); QString what() const throw(); private: QString m_message; }; class ParseError : public CharmException { public: explicit ParseError(const QString &text); }; class XmlSerializationException : public CharmException { public: explicit XmlSerializationException(const QString &message); }; class UnsupportedDatabaseVersionException : public CharmException { public: explicit UnsupportedDatabaseVersionException(const QString &message); }; class InvalidTaskListException : public CharmException { public: explicit InvalidTaskListException(const QString &message); }; class TransactionException : public CharmException { public: explicit TransactionException(const QString &text = QString()); }; class AlreadyRunningException : public CharmException { public: explicit AlreadyRunningException(); }; #endif Charm-1.12.0/Core/CharmQtCompat.cpp000066400000000000000000000016121331066577000167340ustar00rootroot00000000000000/* CharmQtCompat.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2017-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Hannah von Reth This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmQtCompat.h" Charm-1.12.0/Core/CharmQtCompat.h000066400000000000000000000024701331066577000164040ustar00rootroot00000000000000/* CharmQtCompat.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2017-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Hannah von Reth This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARMQTCOMPAT_H #define CHARMQTCOMPAT_H #include #if QT_VERSION < QT_VERSION_CHECK(5, 7, 0) QT_BEGIN_NAMESPACE // this adds const to non-const objects (like std::as_const) template Q_DECL_CONSTEXPR typename std::add_const::type &qAsConst(T &t) Q_DECL_NOTHROW { return t; } // prevent rvalue arguments: template void qAsConst(const T &&) Q_DECL_EQ_DELETE; QT_END_NAMESPACE #endif #endif // CHARMQTCOMPAT_H Charm-1.12.0/Core/CommandEmitterInterface.h000066400000000000000000000021441331066577000204300ustar00rootroot00000000000000/* CommandEmitterInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDEMITTERINTERFACE_H #define COMMANDEMITTERINTERFACE_H class CharmCommand; class CommandEmitterInterface { public: virtual ~CommandEmitterInterface() { } virtual void commitCommand(CharmCommand *) = 0; }; #endif Charm-1.12.0/Core/Configuration.cpp000066400000000000000000000144041331066577000170430ustar00rootroot00000000000000/* Configuration.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: Mike McQuaid This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Configuration.h" #include "CharmConstants.h" #include #include #ifdef NDEBUG #define DEFAULT_CONFIG_GROUP QStringLiteral("default") #else #define DEFAULT_CONFIG_GROUP QStringLiteral("debug") #endif Configuration &Configuration::instance() { static Configuration configuration; return configuration; } Configuration::Configuration() : configurationName(DEFAULT_CONFIG_GROUP) { } Configuration::Configuration(TaskPrefilteringMode _taskPrefilteringMode, TimeTrackerFontSize _timeTrackerFontSize, DurationFormat _durationFormat, bool _detectIdling, Qt::ToolButtonStyle _buttonstyle, bool _showStatusBar, bool _warnUnuploadedTimesheets, bool _requestEventComment, bool _enableCommandInterface, int _numberOfTaskSelectorEntries) : taskPrefilteringMode(_taskPrefilteringMode) , timeTrackerFontSize(_timeTrackerFontSize) , durationFormat(_durationFormat) , toolButtonStyle(_buttonstyle) , showStatusBar(_showStatusBar) , detectIdling(_detectIdling) , warnUnuploadedTimesheets(_warnUnuploadedTimesheets) , requestEventComment(_requestEventComment) , enableCommandInterface(_enableCommandInterface) , numberOfTaskSelectorEntries(_numberOfTaskSelectorEntries) , configurationName(DEFAULT_CONFIG_GROUP) { } bool Configuration::operator==(const Configuration &other) const { return user == other.user && taskPrefilteringMode == other.taskPrefilteringMode && timeTrackerFontSize == other.timeTrackerFontSize && durationFormat == other.durationFormat && detectIdling == other.detectIdling && warnUnuploadedTimesheets == other.warnUnuploadedTimesheets && requestEventComment == other.requestEventComment && toolButtonStyle == other.toolButtonStyle && showStatusBar == other.showStatusBar && configurationName == other.configurationName && installationId == other.installationId && localStorageType == other.localStorageType && localStorageDatabase == other.localStorageDatabase && numberOfTaskSelectorEntries == other.numberOfTaskSelectorEntries; } void Configuration::writeTo(QSettings &settings) { settings.setValue(MetaKey_Key_InstallationId, installationId); settings.setValue(MetaKey_Key_UserId, user.id()); settings.setValue(MetaKey_Key_LocalStorageType, localStorageType); settings.setValue(MetaKey_Key_LocalStorageDatabase, localStorageDatabase); dump(QStringLiteral("(Configuration::writeTo stored configuration)")); } bool Configuration::readFrom(QSettings &settings) { bool complete = true; bool dirty = false; if (settings.contains(MetaKey_Key_InstallationId)) { bool ok; installationId = settings.value(MetaKey_Key_InstallationId).toUInt(&ok); if (!ok || installationId == 1) { const auto newId = createInstallationId(); qDebug() << "Migrating installationId" << installationId << "to" << newId; installationId = newId; dirty = true; } } else { complete = false; } if (settings.contains(MetaKey_Key_UserId)) { user.setId(settings.value(MetaKey_Key_UserId).toInt()); } else { complete = false; } if (settings.contains(MetaKey_Key_LocalStorageType)) { localStorageType = settings.value(MetaKey_Key_LocalStorageType).toString(); } else { complete = false; } if (settings.contains(MetaKey_Key_LocalStorageDatabase)) { localStorageDatabase = settings.value(MetaKey_Key_LocalStorageDatabase).toString(); } else { complete = false; } dump(QStringLiteral("(Configuration::readFrom loaded configuration)")); if (dirty && complete) { writeTo(settings); } return complete; } void Configuration::dump(const QString &why) { // dump configuration: return; // disable debug output qDebug() << "Configuration: configuration:" << (why.isEmpty() ? QString() : why) << endl << "--> installation id: " << installationId << endl << "--> userid: " << user.id() << endl << "--> local storage type: " << localStorageType << endl << "--> local storage database: " << localStorageDatabase << endl << "--> task prefiltering mode: " << taskPrefilteringMode << endl << "--> task tracker font size: " << timeTrackerFontSize << endl << "--> duration format: " << durationFormat << endl << "--> Idle Detection: " << detectIdling << endl << "--> toolButtonStyle: " << toolButtonStyle << endl << "--> showStatusBar: " << showStatusBar << endl << "--> warnUnuploadedTimesheets: " << warnUnuploadedTimesheets << endl << "--> requestEventComment: " << requestEventComment << endl << "--> enableCommandInterface: " << enableCommandInterface << "--> numberOfTaskSelectorEntries: " << numberOfTaskSelectorEntries; } quint32 Configuration::createInstallationId() const { qsrand(QDateTime::currentMSecsSinceEpoch()); return qrand() + 2; } Charm-1.12.0/Core/Configuration.h000066400000000000000000000074741331066577000165210ustar00rootroot00000000000000/* Configuration.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CONFIGURATION_H #define CONFIGURATION_H #include #include "User.h" class QSettings; class Configuration { public: // only append to that, to no break old configurations: enum TimeTrackerFontSize { TimeTrackerFont_Small, TimeTrackerFont_Regular, TimeTrackerFont_Large }; enum TaskPrefilteringMode { TaskPrefilter_ShowAll, TaskPrefilter_CurrentOnly, TaskPrefilter_SubscribedOnly, TaskPrefilter_SubscribedAndCurrentOnly, TaskPrefilter_NumberOfModes }; enum DurationFormat { Minutes = 0, Decimal }; bool operator==(const Configuration &other) const; static Configuration &instance(); void writeTo(QSettings &); // read the configuration from the settings object // this will not modify the settings group etc but just use it // returns true if all individual settings have been found (the // configuration is complete) bool readFrom(QSettings &); // helper method void dump(const QString &why = QString::null); quint32 createInstallationId() const; User user; // this user's id TaskPrefilteringMode taskPrefilteringMode = TaskPrefilter_ShowAll; TimeTrackerFontSize timeTrackerFontSize = TimeTrackerFont_Regular; DurationFormat durationFormat = Minutes; Qt::ToolButtonStyle toolButtonStyle = Qt::ToolButtonFollowStyle; bool showStatusBar = true; bool detectIdling = true; bool warnUnuploadedTimesheets = true; bool requestEventComment = false; bool enableCommandInterface = false; int numberOfTaskSelectorEntries = 5; // these are stored in QSettings, since we need this information to locate and open the database: QString configurationName; quint32 installationId = 0; QString localStorageType; // SqLite, MySql, ... QString localStorageDatabase; // database name (path, with sqlite) bool newDatabase = false; // true if the configuration has just been created bool failure = false; // used to reconfigure on failures QString failureMessage; // a message to show the user if something is wrong with the configuration // appearance properties int taskPaddingLength = 6; // arbitrary private: // allow test classes to create configuration objects (tests are // the only application that can have (test) multiple // configurations): friend class SqLiteStorageTests; friend class ControllerTests; // these are all the persisted metadata settings, and the constructor is only used during test runs: Configuration(TaskPrefilteringMode taskPrefilteringMode, TimeTrackerFontSize, DurationFormat durationFormat, bool detectIdling, Qt::ToolButtonStyle buttonstyle, bool showStatusBar, bool warnUnuploadedTimesheets, bool _requestEventComment, bool enableCommandInterface, int _numberOfTaskSelectorEntries); Configuration(); }; #endif Charm-1.12.0/Core/Controller.cpp000066400000000000000000000350751331066577000163660ustar00rootroot00000000000000/* Controller.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: Olivier JG Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Controller.h" #include "CharmCommand.h" #include "CharmConstants.h" #include "CharmExceptions.h" #include "Configuration.h" #include "Event.h" #include "SqLiteStorage.h" #include "SqlRaiiTransactor.h" #include "SqlStorage.h" #include "Task.h" #include Controller::Controller(QObject *parent_) : QObject(parent_) { } Controller::~Controller() { } Event Controller::makeEvent(const Task &task) { Event event = m_storage->makeEvent(); Q_ASSERT(event.isValid()); event.setTaskId(task.id()); if (m_storage->modifyEvent(event)) { emit eventAdded(event); } else { event = Event(); } return event; } Event Controller::cloneEvent(const Event &e) { Event event = m_storage->makeEvent(); Q_ASSERT(event.isValid()); int id = event.id(); event = e; event.setId(id); if (m_storage->modifyEvent(event)) { emit eventAdded(event); } else { event = Event(); } return event; } bool Controller::modifyEvent(const Event &e) { if (m_storage->modifyEvent(e)) { emit eventModified(e); return true; } else { return false; } } bool Controller::deleteEvent(const Event &e) { if (m_storage->deleteEvent(e)) { emit eventDeleted(e); return true; } else { return false; } } bool Controller::addTask(const Task &task) { if (m_storage->addTask(task)) { updateSubscriptionForTask(task); emit taskAdded(task); return true; } else { Q_ASSERT(false); // impossible return false; } } bool Controller::modifyTask(const Task &task) { // modify the task itself: bool result = m_storage->modifyTask(task); Q_ASSERT(result); if (!result) { qCritical() << Q_FUNC_INFO << "modifyTask failed!"; return result; } updateSubscriptionForTask(task); emit(taskUpdated(task)); return true; } bool Controller::deleteTask(const Task &task) { if (m_storage->deleteTask(task)) { m_storage->deleteSubscription(CONFIGURATION.user, task); emit taskDeleted(task); return true; } else { Q_ASSERT(false); // impossible return false; } } bool Controller::setAllTasks(const TaskList &tasks) { if (m_storage->setAllTasks(CONFIGURATION.user, tasks)) { const TaskList newTasks = m_storage->getAllTasks(); // tell the view about the existing tasks; emit definedTasks(newTasks); return true; } else { return false; } } void Controller::updateSubscriptionForTask(const Task &task) { if (task.subscribed()) { bool result = m_storage->addSubscription(CONFIGURATION.user, task); Q_ASSERT(result); Q_UNUSED(result); } else { bool result = m_storage->deleteSubscription(CONFIGURATION.user, task); Q_ASSERT(result); Q_UNUSED(result); } } void Controller::stateChanged(State previous, State next) { Q_UNUSED(previous); switch (next) { case Connected: { // yes, it is that simple: TaskList tasks = m_storage->getAllTasks(); // tell the view about the existing tasks; if (!Task::checkForUniqueTaskIds(tasks)) { throw CharmException(tr( "The Charm database is corrupted, it contains duplicate task ids. " "Please have it looked after by a professional.")); } if (!Task::checkForTreeness(tasks)) { throw CharmException(tr( "The Charm database is corrupted, the tasks do not form a tree. " "Please have it looked after by a professional.")); } emit definedTasks(tasks); EventList events = m_storage->getAllEvents(); emit allEvents(events); break; } case Disconnecting: { emit readyToQuit(); if (m_storage) { // this will still leave Qt complaining about a repeated connection m_storage->disconnect(); delete m_storage; m_storage = nullptr; } break; } default: break; } if (m_storage) { emit currentBackendStatus(m_storage->description()); m_storage->stateChanged(previous); } } struct Setting { QString key; QString value; }; void Controller::persistMetaData(Configuration &configuration) { Q_ASSERT_X(m_storage != nullptr, Q_FUNC_INFO, "No storage interface available"); Setting settings[] = { { MetaKey_Key_UserName, configuration.user.name() }, { MetaKey_Key_SubscribedTasksOnly, QString().setNum(configuration.taskPrefilteringMode) }, { MetaKey_Key_TimeTrackerFontSize, QString().setNum(configuration.timeTrackerFontSize) }, { MetaKey_Key_DurationFormat, QString::number(configuration.durationFormat) }, { MetaKey_Key_IdleDetection, stringForBool(configuration.detectIdling) }, { MetaKey_Key_WarnUnuploadedTimesheets, stringForBool(configuration.warnUnuploadedTimesheets) }, { MetaKey_Key_RequestEventComment, stringForBool(configuration.requestEventComment) }, { MetaKey_Key_ToolButtonStyle, QString().setNum(configuration.toolButtonStyle) }, { MetaKey_Key_ShowStatusBar, stringForBool(configuration.showStatusBar) }, { MetaKey_Key_EnableCommandInterface, stringForBool(configuration.enableCommandInterface) }, { MetaKey_Key_NumberOfTaskSelectorEntries, QString::number(configuration.numberOfTaskSelectorEntries) } }; int NumberOfSettings = sizeof settings / sizeof settings[0]; bool good = true; for (int i = 0; i < NumberOfSettings; ++i) good = good && m_storage->setMetaData(settings[i].key, settings[i].value); Q_ASSERT_X(good, Q_FUNC_INFO, "Controller assumes write " "permissions in meta data table if persistMetaData is called"); CONFIGURATION.dump(); } template void Controller::loadConfigValue(const QString &key, T &configValue) const { const QString storedValue = m_storage->getMetaData(key); if (storedValue.isNull()) return; configValue = strToT(storedValue); } void Controller::provideMetaData(Configuration &configuration) { Q_ASSERT_X(m_storage != nullptr, Q_FUNC_INFO, "No storage interface available"); configuration.user.setName(m_storage->getMetaData(MetaKey_Key_UserName)); loadConfigValue(MetaKey_Key_TimeTrackerFontSize, configuration.timeTrackerFontSize); loadConfigValue(MetaKey_Key_DurationFormat, configuration.durationFormat); loadConfigValue(MetaKey_Key_SubscribedTasksOnly, configuration.taskPrefilteringMode); loadConfigValue(MetaKey_Key_IdleDetection, configuration.detectIdling); loadConfigValue(MetaKey_Key_WarnUnuploadedTimesheets, configuration.warnUnuploadedTimesheets); loadConfigValue(MetaKey_Key_RequestEventComment, configuration.requestEventComment); loadConfigValue(MetaKey_Key_ToolButtonStyle, configuration.toolButtonStyle); loadConfigValue(MetaKey_Key_ShowStatusBar, configuration.showStatusBar); loadConfigValue(MetaKey_Key_EnableCommandInterface, configuration.enableCommandInterface); loadConfigValue(MetaKey_Key_NumberOfTaskSelectorEntries, configuration.numberOfTaskSelectorEntries); configuration.numberOfTaskSelectorEntries = qMax(0, configuration.numberOfTaskSelectorEntries); CONFIGURATION.dump(); } bool Controller::initializeBackEnd(const QString &name) { // make storage interface according to configuration // this is our local storage backend factory and may have to be // factored out into a factory method (now that is some serious // refucktoring): if (name == CHARM_SQLITE_BACKEND_DESCRIPTOR) { m_storage = new SqLiteStorage; return true; } else { Q_ASSERT_X(false, Q_FUNC_INFO, "Unknown local storage backend type"); return false; } } bool Controller::connectToBackend() { bool result = m_storage->connect(CONFIGURATION); // the user id in the database, and the installation id, do not // have to be 1 and 1, as we have guessed --> persist configuration if (result && !CONFIGURATION.newDatabase) provideMetaData(CONFIGURATION); return result; } bool Controller::disconnectFromBackend() { return m_storage->disconnect(); } void Controller::executeCommand(CharmCommand *command) { command->execute(this); // send it back to the view: emit commandCompleted(command); } void Controller::rollbackCommand(CharmCommand *command) { command->rollback(this); // send it back to the view: emit commandCompleted(command); } SqlStorage *Controller::storage() { return m_storage; } const QString MetaDataElement(QStringLiteral("metadata")); const QString ExportRootElement(QStringLiteral("charmdatabase")); const QString VersionElement(QStringLiteral("version")); const QString TasksElement(QStringLiteral("tasks")); const QString EventsElement(QStringLiteral("events")); QDomDocument Controller::exportDatabasetoXml() const { QDomDocument document(QStringLiteral("charmdatabase")); // root element: QDomElement root = document.createElement(ExportRootElement); root.setAttribute(VersionElement, CHARM_DATABASE_VERSION); document.appendChild(root); // metadata: QDomElement metadata = document.createElement(MetaDataElement); // I am not so sure what kind of metadata needs to be stored root.appendChild(metadata); // tasks element: QDomElement tasksElement = document.createElement(TasksElement); // FIXME there are generic methods for that now, in Task.h TaskList tasks = m_storage->getAllTasks(); Q_FOREACH (const Task &task, tasks) { QDomElement element = task.toXml(document); tasksElement.appendChild(element); } root.appendChild(tasksElement); // events element: QDomElement eventsElement = document.createElement(EventsElement); EventList events = m_storage->getAllEvents(); Q_FOREACH (const Event &event, events) { QDomElement element = event.toXml(document); eventsElement.appendChild(element); } root.appendChild(eventsElement); return document; } class MakeSureTheModelIsUpdated { public: explicit MakeSureTheModelIsUpdated(Controller *controller) : m_controller(controller) { } ~MakeSureTheModelIsUpdated() { // now tell the data model that things have changed: m_controller->updateModelEventsAndTasks(); } private: Controller *m_controller; }; QString Controller::importDatabaseFromXml(const QDomDocument &document) { MakeSureTheModelIsUpdated m(this); // first, parse the XML document, and break if there is an error // (not touching the DB contents): TaskList importedTasks; EventList importedEvents; int databaseSchemaVersion; // FIXME test for the file to be a database export, not (for example) a task definitions export try { QDomElement rootElement = document.documentElement(); bool ok; databaseSchemaVersion = rootElement.attribute(QStringLiteral("version")).toInt(&ok); if (!ok) throw XmlSerializationException(QObject::tr( "Syntax error, no version attribute found.")); QDomElement tasksElement = rootElement.firstChildElement(TasksElement); for (QDomElement element = tasksElement.firstChildElement(Task::tagName()); !element.isNull(); element = element.nextSiblingElement(Task::tagName())) { Task task = Task::fromXml(element, databaseSchemaVersion); if (!task.isValid()) { qDebug() << "The following task is invalid and will not be added:"; task.dump(); // return tr( "The Export file contains at least one invalid task." ); } else { importedTasks.append(task); } } QDomElement eventsElement = rootElement.firstChildElement(EventsElement); for (QDomElement element = eventsElement.firstChildElement(Event::tagName()); !element.isNull(); element = element.nextSiblingElement(Event::tagName())) { Event event = Event::fromXml(element, databaseSchemaVersion); if (!event.isValid()) { qDebug() << "The following event is invalid and will not be added:"; event.dump(); // return tr( "The Export file contains at least one invalid event." ); } else { importedEvents.append(event); } } } catch (const XmlSerializationException &e) { qDebug() << "Controller::importDatabaseFromXml: things fucked up:" << e.what(); return tr("The export file is invalid: %1").arg(e.what()); } const QString error = m_storage->setAllTasksAndEvents(CONFIGURATION.user, importedTasks, importedEvents); if (!error.isEmpty()) { // the database should be unchanged, and the model will update on return return tr("Error importing tasks and events from the file:
    %1") .arg(error); } // FIXME needed? // tell the model that the tasks and events have vanished: emit allEvents(EventList()); emit definedTasks(TaskList()); return QString(); } void Controller::updateModelEventsAndTasks() { TaskList tasks = m_storage->getAllTasks(); // tell the view about the existing tasks; emit definedTasks(tasks); EventList events = m_storage->getAllEvents(); emit allEvents(events); } Charm-1.12.0/Core/Controller.h000066400000000000000000000111421331066577000160200ustar00rootroot00000000000000/* Controller.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Olivier JG Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CONTROLLER_H #define CONTROLLER_H #include #include "Event.h" #include "Task.h" #include "State.h" class CharmCommand; class Configuration; class SqlStorage; class Controller : public QObject { Q_OBJECT public: explicit Controller(QObject *parent = nullptr); ~Controller(); // application: // react on application state changes void stateChanged(State previous, State next); // persist meta data portions of Configuration void persistMetaData(Configuration &); // load meta data and store appropriate portions in configuration void provideMetaData(Configuration &); /** Create the backend. */ bool initializeBackEnd(const QString &name); /** Connect to the backend (make it available). */ bool connectToBackend(); /** Disconnect from the backend (shut it down). */ bool disconnectFromBackend(); /** The currently used backend. */ SqlStorage *storage(); // FIXME add the add/modify/delete functions will not be slots anymore /** Add an event. Return a valid event if successful. */ Event makeEvent(const Task &); /** Add an event, copying data from another event. */ Event cloneEvent(const Event &); /** Modify an event. */ bool modifyEvent(const Event &); /** Delete an event. */ bool deleteEvent(const Event &); /** Add a task, and send the result to the view as a signal. */ bool addTask(const Task &parent); /** Modify the task, the user has changed it in the view. */ bool modifyTask(const Task &); /** Delete the task. Send a signal to the view confirming it. */ bool deleteTask(const Task &); /** Set all tasks. Updates the view, after. */ bool setAllTasks(const TaskList &); /** Export the database contents into a XML document. */ QDomDocument exportDatabasetoXml() const; /** Import the content of the Xml document into the currently open database. * This will modify the database. * @return An empty string on no error, an human-readable error message otherwise. */ QString importDatabaseFromXml(const QDomDocument &); void updateModelEventsAndTasks(); public Q_SLOTS: /** Receive a command from the view. */ void executeCommand(CharmCommand *); /** Receive an undo command from the view. */ void rollbackCommand(CharmCommand *); Q_SIGNALS: /** Added an event. */ void eventAdded(const Event &event); /** Modified an event. */ void eventModified(const Event &event); /** Deleted an event. */ void eventDeleted(const Event &event); void allEvents(const EventList &); /** This sends out the current task list. */ void definedTasks(const TaskList &); /** Add a task. */ void taskAdded(const Task &); /** Update a task in the view. */ void taskUpdated(const Task &); /** Delete a task from the view completely. */ void taskDeleted(const Task &); /** This tells the application that the controller is ready to quit. When the user quits the application, the application will tell the controller to end and commit all active events. The controller will emit readyToQuit() when all data is stored. The controller will leave Disconnecting state when it receives the signal. */ void readyToQuit(); void currentBackendStatus(const QString &text); /** A command has been completed from the controller's point of view. */ void commandCompleted(CharmCommand *); private: void updateSubscriptionForTask(const Task &); template void loadConfigValue(const QString &key, T &configValue) const; SqlStorage *m_storage = nullptr; }; #endif Charm-1.12.0/Core/Dates.cpp000066400000000000000000000047271331066577000153030ustar00rootroot00000000000000/* Dates.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld Author: Olivier JG This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Dates.h" #include //find date for a certain weekday in week no/year QDate Charm::dateByWeekNumberAndWeekDay(int year, int week, int day) { QDate start(year, 1, 1); if (start.weekNumber() != 1) // if Jan 1st is not in the week 1 (but week 53 of the previous year), add a week start = start.addDays(7); int wdyear = 0; const int wdweek = start.weekNumber(&wdyear); // now we really should be in week 1 of year Q_ASSERT(wdweek == 1); Q_UNUSED(wdweek); Q_ASSERT(wdyear == year); //now go to the requested weekday, in week 1: start = start.addDays(day - start.dayOfWeek()); //now go forward to the requested week no. const QDate date = start.addDays(7 * (week - 1)); return date; } QDate Charm::weekDayInWeekOf(Qt::DayOfWeek dayOfWeek, const QDate &date) { return date.addDays(dayOfWeek - date.dayOfWeek()); } int Charm::numberOfWeeksInYear(int year) { QDate d(year, 1, 1); d = d.addDays(d.daysInYear() - 1); int weeksInYear = d.weekNumber() == 1 ? 52 : d.weekNumber(); Q_ASSERT(weeksInYear == 52 || weeksInYear == 53); return weeksInYear; } int Charm::weekDifference(const QDate &from, const QDate &to) { int fromWeekYear, toWeekYear; const int fromWeekNumber = from.weekNumber(&fromWeekYear); const int toWeekNumber = to.weekNumber(&toWeekYear); int weeksForInterveningYears = 0; for (int year = fromWeekYear; year < toWeekYear; ++year) weeksForInterveningYears += numberOfWeeksInYear(year); return toWeekNumber + weeksForInterveningYears - fromWeekNumber; } Charm-1.12.0/Core/Dates.h000066400000000000000000000024741331066577000147450ustar00rootroot00000000000000/* Dates.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld Author: Olivier JG This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_DATES_H #define CHARM_DATES_H #include namespace Charm { QDate dateByWeekNumberAndWeekDay(int year, int week, int weekday); /** * returns the date for a week day @p dayOfWeek, in the week of date @p date. */ QDate weekDayInWeekOf(Qt::DayOfWeek dayOfWeek, const QDate &date); int numberOfWeeksInYear(int year); int weekDifference(const QDate &from, const QDate &to); } #endif // CHARM_DATES_H Charm-1.12.0/Core/Event.cpp000066400000000000000000000152041331066577000153140ustar00rootroot00000000000000/* Event.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Event.h" #include "CharmExceptions.h" #include #include Event::Event() { } bool Event::operator ==(const Event &other) const { return other.id() == id() && other.taskId() == taskId() && other.comment() == comment() && other.startDateTime() == startDateTime() && other.endDateTime() == endDateTime() && other.userId() == userId() && other.reportId() == reportId(); } EventId Event::id() const { return m_id; } void Event::setId(EventId id) { m_id = id; } int Event::userId() const { return m_userid; } void Event::setUserId(int userId) { m_userid = userId; } int Event::reportId() const { return m_reportid; } void Event::setReportId(int reportId) { m_reportid = reportId; } bool Event::isValid() const { // negative values are allowed and indicate calculated values return id() != 0; } TaskId Event::taskId() const { return m_taskId; } void Event::setTaskId(TaskId taskId) { m_taskId = taskId; } QString Event::comment() const { return m_comment; } void Event::setComment(const QString &comment) { m_comment = comment; } QDateTime Event::startDateTime(Qt::TimeSpec timeSpec) const { return m_start.toTimeSpec(timeSpec); } void Event::setStartDateTime(const QDateTime &start) { m_start = start.toUTC(); // strip milliseconds, this is necessary for the precision of serialization: m_start = m_start.addMSecs(-m_start.time().msec()); Q_ASSERT(qAbs(m_start.secsTo(start)) <= 1); Q_ASSERT(m_start.time().msec() == 0); } QDateTime Event::endDateTime(Qt::TimeSpec timeSpec) const { return m_end.toTimeSpec(timeSpec); } void Event::setEndDateTime(const QDateTime &end) { m_end = end.toUTC(); // strip milliseconds, this is necessary for the precision of serialization: m_end = m_end.addMSecs(-m_end.time().msec()); Q_ASSERT(qAbs(m_end.secsTo(end)) <= 1); Q_ASSERT(m_end.time().msec() == 0); } int Event::duration() const { if (m_start.isValid() && m_end.isValid()) { return m_start.secsTo(m_end); } else { return 0; } } void Event::dump() const { qDebug() << "[Event" << id() << "] - task " << taskId() << " - start: " << startDateTime() << " - end: " << endDateTime() << " - duration: " << duration() << "seconds - comment:" << comment(); } void dumpEvents(const EventList &events) { for (int i = 0; i < events.size(); ++i) events[i].dump(); } const QString EventElement(QStringLiteral("event")); const QString EventIdAttribute(QStringLiteral("eventid")); const QString EventTaskIdAttribute(QStringLiteral("taskid")); const QString EventUserIdAttribute(QStringLiteral("userid")); const QString EventReportIdAttribute(QStringLiteral("reportid")); const QString EventStartAttribute(QStringLiteral("start")); const QString EventEndAttribute(QStringLiteral("end")); QDomElement Event::toXml(QDomDocument document) const { QDomElement element = document.createElement(EventElement); element.setAttribute(EventIdAttribute, QString().setNum(id())); element.setAttribute(EventTaskIdAttribute, QString().setNum(taskId())); element.setAttribute(EventUserIdAttribute, QString().setNum(userId())); element.setAttribute(EventReportIdAttribute, QString().setNum(reportId())); if (m_start.isValid()) element.setAttribute(EventStartAttribute, m_start.toString(Qt::ISODate)); if (m_end.isValid()) element.setAttribute(EventEndAttribute, m_end.toString(Qt::ISODate)); if (!comment().isEmpty()) { QDomText commentText = document.createTextNode(comment()); element.appendChild(commentText); } return element; } QString Event::tagName() { static const QString tag(QStringLiteral("event")); return tag; } Event Event::fromXml(const QDomElement &element, int databaseSchemaVersion) { // in case any event object creates trouble with // serialization/deserialization, add an object of it to // void XmlSerializationTests::testEventSerialization() Event event; bool ok; event.setComment(element.text()); event.setId(element.attribute(EventIdAttribute).toInt(&ok)); if (!ok) throw XmlSerializationException(QObject::tr("Event::fromXml: invalid event id")); event.setTaskId(element.attribute(EventTaskIdAttribute).toInt(&ok)); if (!ok) throw XmlSerializationException(QObject::tr("Event::fromXml: invalid task id")); event.setUserId(element.attribute(EventUserIdAttribute).toInt(&ok)); if (!ok && databaseSchemaVersion > 1) { throw XmlSerializationException(QObject::tr("Event::fromXml: invalid user id")); event.setUserId(0); } event.setReportId(element.attribute(EventReportIdAttribute).toInt(&ok)); if (!ok && databaseSchemaVersion > 1) { throw XmlSerializationException(QObject::tr("Event::fromXml: invalid report id")); event.setReportId(0); } if (element.hasAttribute(EventStartAttribute)) { QDateTime start = QDateTime::fromString(element.attribute(EventStartAttribute), Qt::ISODate); if (!start.isValid()) throw XmlSerializationException(QObject::tr( "Event::fromXml: invalid start date")); start.setTimeSpec(Qt::UTC); event.setStartDateTime(start); } if (element.hasAttribute(EventEndAttribute)) { QDateTime end = QDateTime::fromString(element.attribute(EventEndAttribute), Qt::ISODate); if (!end.isValid()) throw XmlSerializationException(QObject::tr( "Event::fromXml: invalid end date")); end.setTimeSpec(Qt::UTC); event.setEndDateTime(end.toLocalTime()); } event.setComment(element.text()); return event; } Charm-1.12.0/Core/Event.h000066400000000000000000000057241331066577000147670ustar00rootroot00000000000000/* Event.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_EVENT_H #define CHARM_EVENT_H #include #include #include #include #include #include #include "Task.h" typedef int EventId; /** An event is a recorded time for a task. It records the according task, the duration and a possible comment. */ class Event { public: Event(); bool operator ==(const Event &other) const; bool operator !=(const Event &other) const { return !operator==(other); } EventId id() const; void setId(EventId id); int userId() const; void setUserId(int userId); int reportId() const; void setReportId(int userId); bool isValid() const; TaskId taskId() const; void setTaskId(TaskId id); QString comment() const; void setComment(const QString &); QDateTime startDateTime(Qt::TimeSpec timeSpec = Qt::LocalTime) const; void setStartDateTime(const QDateTime &start = QDateTime::currentDateTime()); QDateTime endDateTime(Qt::TimeSpec timeSpec = Qt::LocalTime) const; void setEndDateTime(const QDateTime &end = QDateTime::currentDateTime()); /** Returns the duration of this event in seconds. */ int duration() const; void dump() const; QDomElement toXml(QDomDocument) const; static Event fromXml(const QDomElement &, int databaseSchemaVersion = 1); static QString tagName(); private: /** The id of the user who owns the event. */ int m_userid = {}; /** The report id. This field is only useful * if the event is imported from a report. */ int m_reportid = {}; int m_id = {}; /** The task this event belongs to. */ TaskId m_taskId = {}; /** A possible user comment. May be empty. */ QString m_comment; /** The start datetime of the event. */ QDateTime m_start; /** The end datetime of the event. */ QDateTime m_end; }; /** A list of events. */ typedef QList EventList; /** A list of event ids. */ typedef QList EventIdList; /** A map of events. */ typedef std::map EventMap; void dumpEvents(const EventList &events); #endif Charm-1.12.0/Core/EventModelInterface.h000066400000000000000000000022571331066577000175670ustar00rootroot00000000000000/* EventModelInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef EVENTMODELINTERFACE_H #define EVENTMODELINTERFACE_H class Event; class QModelIndex; class EventModelInterface { public: virtual ~EventModelInterface() { } virtual const Event &eventForIndex(const QModelIndex &) const = 0; virtual QModelIndex indexForEvent(const Event &) const = 0; }; #endif Charm-1.12.0/Core/MySqlStorage.cpp000066400000000000000000000173051331066577000166310ustar00rootroot00000000000000/* MySqlStorage.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "MySqlStorage.h" #include "CharmConstants.h" #include #include #include // DATABASE STRUCTURE DEFINITION FOR MYSQL static const QString Tables[] = { QStringLiteral("MetaData"), QStringLiteral("Installations"), QStringLiteral("Tasks"), QStringLiteral("Events"), QStringLiteral("Subscriptions"), QStringLiteral("Users") }; static const int NumberOfTables = sizeof Tables / sizeof Tables[0]; struct Field { QString name; QString type; }; typedef Field Fields; const Field LastField = { QString::null, QString::null}; static const Fields MetaData_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER AUTO_INCREMENT PRIMARY KEY") }, { QStringLiteral("key"), QStringLiteral("VARCHAR( 128 ) NOT NULL") }, { QStringLiteral("value"), QStringLiteral("VARCHAR( 128 )") }, LastField }; static const Fields Installations_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER AUTO_INCREMENT PRIMARY KEY") }, { QStringLiteral("inst_id"), QStringLiteral("INTEGER") }, { QStringLiteral("user_id"), QStringLiteral("INTEGER") }, { QStringLiteral("name"), QStringLiteral("varchar(256)") }, LastField }; static const Fields Tasks_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER AUTO_INCREMENT PRIMARY KEY") }, { QStringLiteral("task_id"), QStringLiteral("INTEGER UNIQUE") }, { QStringLiteral("parent"), QStringLiteral("INTEGER") }, { QStringLiteral("validfrom"), QStringLiteral("timestamp") }, { QStringLiteral("validuntil"), QStringLiteral("timestamp") }, { QStringLiteral("trackable"), QStringLiteral("INTEGER") }, { QStringLiteral("comment"), QStringLiteral("varchar(256)") }, { QStringLiteral("name"), QStringLiteral("varchar(256)") }, LastField }; static const Fields Event_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER AUTO_INCREMENT PRIMARY KEY") }, { QStringLiteral("user_id"), QStringLiteral("INTEGER") }, { QStringLiteral("event_id"), QStringLiteral("INTEGER") }, { QStringLiteral("installation_id"), QStringLiteral("INTEGER") }, { QStringLiteral("report_id"), QStringLiteral("INTEGER NULL") }, { QStringLiteral("task"), QStringLiteral("INTEGER") }, { QStringLiteral("comment"), QStringLiteral("varchar(256)") }, { QStringLiteral("start"), QStringLiteral("timestamp") }, { QStringLiteral("end"), QStringLiteral("timestamp") }, LastField }; static const Fields Subscriptions_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER AUTO_INCREMENT PRIMARY KEY") }, { QStringLiteral("user_id"), QStringLiteral("INTEGER") }, { QStringLiteral("task"), QStringLiteral("INTEGER") }, LastField }; static const Fields Users_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER AUTO_INCREMENT PRIMARY KEY") }, { QStringLiteral("user_id"), QStringLiteral("INTEGER UNIQUE") }, { QStringLiteral("name"), QStringLiteral("varchar(256)") }, LastField }; static const Fields *Database_Fields[NumberOfTables] = { MetaData_Fields, Installations_Fields, Tasks_Fields, Event_Fields, Subscriptions_Fields, Users_Fields }; const QString DatabaseName = QStringLiteral("mysql.charm.kdab.com"); MySqlStorage::MySqlStorage() : SqlStorage() , m_database(QSqlDatabase::addDatabase(QStringLiteral("QMYSQL"), DatabaseName)) { } MySqlStorage::~MySqlStorage() { } bool MySqlStorage::createDatabaseTables() { Q_ASSERT_X(database().open(), Q_FUNC_INFO, "Connection to database must be established first"); bool error = false; // create tables: for (int i = 0; i < NumberOfTables; ++i) { if (!database().tables().contains(Tables[i])) { QString statement; QTextStream stream(&statement, QIODevice::WriteOnly); stream << "CREATE table `" << Tables[i] << "` ("; const Field *field = Database_Fields[i]; while (field->name != QString::null) { stream << " `" << field->name << "` " << field->type; ++field; if (field->name != QString::null) stream << ", "; } stream << ");"; QSqlQuery query(database()); qDebug() << statement; query.prepare(statement); if (!runQuery(query)) error = true; } } error = error || !setMetaData(CHARM_DATABASE_VERSION_DESCRIPTOR, QString().setNum(CHARM_DATABASE_VERSION)); return !error; } QString MySqlStorage::lastInsertRowFunction() const { return QString::fromLocal8Bit("last_insert_id"); } QSqlDatabase &MySqlStorage::database() { return m_database; } QString MySqlStorage::description() const { return QObject::tr("Remote MySql Database"); } bool MySqlStorage::connect(Configuration &) { return false; // not implemented, needs the right information in Configuration } bool MySqlStorage::disconnect() { return false; // not implemented } bool MySqlStorage::createDatabase(Configuration &) { return createDatabaseTables(); } MySqlStorage::Parameters MySqlStorage::parseParameterEnvironmentVariable() { // read configuration from the environment: const QByteArray databaseConfigurationString = qgetenv("CHARM_DATABASE_CONFIGURATION"); if (!databaseConfigurationString.isEmpty()) { Parameters p; // the string is supposed to be of the format "hostname;port;username;password" // if port is 0 or empty, the default port is used QStringList elements = QString::fromLocal8Bit(databaseConfigurationString).split(QLatin1Char( ';')); if (elements.count() != 4) { throw ParseError(QObject::tr("Bad database configuration string format")); } else { p.host = elements.at(0); p.name = elements.at(2); p.password = elements.at(3); bool ok; if (!elements.at(1).isEmpty()) { p.port = elements.at(1).toUInt(&ok); if (ok != true) { throw ParseError(QObject::tr( "The port must be a non-negative integer number in the database configuration string format")); } } } return p; } else { throw ParseError(QObject::tr( "Timesheet processor configuration not found (CHARM_DATABASE_CONFIGURATION). Aborting.")); } } void MySqlStorage::configure(const Parameters ¶meters) { database().setHostName(parameters.host); database().setDatabaseName(parameters.database); database().setUserName(parameters.name); database().setPassword(parameters.password); if (parameters.port != 0) database().setPort(parameters.port); } Charm-1.12.0/Core/MySqlStorage.h000066400000000000000000000034171331066577000162750ustar00rootroot00000000000000/* MySqlStorage.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MYSQLSTORAGE_H #define MYSQLSTORAGE_H #include "SqlStorage.h" #include class MySqlStorage : public SqlStorage { public: struct Parameters { Parameters() : port(3309) , database(QStringLiteral("Charm")) { } unsigned int port; QString database; QString name; QString password; QString host; }; MySqlStorage(); virtual ~MySqlStorage(); QSqlDatabase &database() override; QString description() const override; bool connect(Configuration &) override; bool disconnect() override; bool createDatabase(Configuration &) override; bool createDatabaseTables() override; static Parameters parseParameterEnvironmentVariable(); void configure(const Parameters &); protected: QString lastInsertRowFunction() const override; private: QSqlDatabase m_database; }; #endif /* MYSQLSTORAGE_H */ Charm-1.12.0/Core/SmartNameCache.cpp000066400000000000000000000113551331066577000170510ustar00rootroot00000000000000/* SmartNameCache.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "SmartNameCache.h" struct IdLessThan { bool operator()(const Task &lhs, const Task &rhs) const { return lhs.id() < rhs.id(); } }; void SmartNameCache::setAllTasks(const TaskList &taskList) { m_tasks = taskList; sortTasks(); regenerateSmartNames(); } void SmartNameCache::sortTasks() { qSort(m_tasks.begin(), m_tasks.end(), IdLessThan()); } //single add/modify/delete rebuild the cache each time. //If necessary, this could be optimized by keeping the tasks in a "inverse" tree //with the task names below the root and their parents further down void SmartNameCache::modifyTask(const Task &task) { const TaskList::Iterator it = qBinaryFind(m_tasks.begin(), m_tasks.end(), Task(task.id(), QString()), IdLessThan()); if (it != m_tasks.end()) *it = task; sortTasks(); regenerateSmartNames(); } void SmartNameCache::deleteTask(const Task &task) { const auto it = qBinaryFind(m_tasks.begin(), m_tasks.end(), Task(task.id(), QString()), IdLessThan()); if (it != m_tasks.end()) { m_tasks.erase(it); regenerateSmartNames(); } } void SmartNameCache::clearTasks() { m_tasks.clear(); m_smartTaskNamesById.clear(); } Task SmartNameCache::findTask(TaskId id) const { const auto it = qBinaryFind(m_tasks.begin(), m_tasks.end(), Task(id, QString()), IdLessThan()); if (it != m_tasks.constEnd()) { return *it; } else { return Task(); } } void SmartNameCache::addTask(const Task &task) { m_tasks.append(task); sortTasks(); regenerateSmartNames(); } QString SmartNameCache::smartName(const TaskId &id) const { return m_smartTaskNamesById.value(id); } QString SmartNameCache::makeCombined(const Task &task) const { Q_ASSERT(task.isValid() || task.name().isEmpty()); // an invalid task (id == 0) must not have a name if (!task.isValid()) return QString(); const Task parent = findTask(task.parent()); if (parent.isValid()) { return QObject::tr("%1/%2", "parent task name/task name").arg(parent.name(), task.name()); } else { return task.name(); } } void SmartNameCache::regenerateSmartNames() { m_smartTaskNamesById.clear(); typedef QPair TaskParentPair; QMap > byName; Q_FOREACH (const Task &task, m_tasks) byName[makeCombined(task)].append(qMakePair(task.id(), task.parent())); QSet cannotMakeUnique; while (!byName.isEmpty()) { QMap > newByName; for (QMap >::ConstIterator it = byName.constBegin(); it != byName.constEnd(); ++it) { const QString currentName = it.key(); const auto &taskPairs = it.value(); Q_ASSERT(!taskPairs.isEmpty()); if (taskPairs.size() == 1 || cannotMakeUnique.contains(currentName)) { Q_FOREACH (const TaskParentPair &i, taskPairs) m_smartTaskNamesById.insert(i.first, currentName); } else { Q_FOREACH (const TaskParentPair &taskPair, taskPairs) { const Task parent = findTask(taskPair.second); if (parent.isValid()) { const QString newName = parent.name() + QLatin1Char('/') + currentName; newByName[newName].append(qMakePair(taskPair.first, parent.parent())); } else { const auto existing = newByName.constFind(currentName); if (existing != newByName.constEnd()) cannotMakeUnique.insert(currentName); newByName[currentName].append(taskPair); } } } } byName = newByName; } } Charm-1.12.0/Core/SmartNameCache.h000066400000000000000000000026551331066577000165210ustar00rootroot00000000000000/* SmartNameCache.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef SMARTNAMECACHE_H #define SMARTNAMECACHE_H #include "Task.h" class SmartNameCache { public: void setAllTasks(const TaskList &taskList); QString smartName(const TaskId &id) const; void addTask(const Task &task); void modifyTask(const Task &task); void deleteTask(const Task &task); void clearTasks(); private: void regenerateSmartNames(); void sortTasks(); Task findTask(TaskId id) const; QString makeCombined(const Task &task) const; private: QMap m_smartTaskNamesById; TaskList m_tasks; }; #endif Charm-1.12.0/Core/SqLiteStorage.cpp000066400000000000000000000232321331066577000167610ustar00rootroot00000000000000/* SqLiteStorage.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: Mike McQuaid This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "SqLiteStorage.h" #include "CharmConstants.h" #include "CharmExceptions.h" #include "Configuration.h" #include "Event.h" #include #include #include #include #include #include // DATABASE STRUCTURE DEFINITION static const QString Tables[] = { QStringLiteral("MetaData"), QStringLiteral("Installations"), QStringLiteral("Tasks"), QStringLiteral("Events"), QStringLiteral("Subscriptions"), QStringLiteral("Users") }; static const int NumberOfTables = sizeof Tables / sizeof Tables[0]; struct Field { QString name; QString type; }; typedef Field Fields; const Field LastField = { QString::null, QString::null}; static const Fields MetaData_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER PRIMARY KEY") }, { QStringLiteral("key"), QStringLiteral("VARCHAR( 128 ) NOT NULL") }, { QStringLiteral("value"), QStringLiteral("VARCHAR( 128 )") }, LastField }; static const Fields Installations_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER PRIMARY KEY") }, { QStringLiteral("inst_id"), QStringLiteral("INTEGER") }, { QStringLiteral("user_id"), QStringLiteral("INTEGER") }, { QStringLiteral("name"), QStringLiteral("varchar(256)") }, LastField }; static const Fields Tasks_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER PRIMARY KEY") }, { QStringLiteral("task_id"), QStringLiteral("INTEGER UNIQUE") }, { QStringLiteral("parent"), QStringLiteral("INTEGER") }, { QStringLiteral("validfrom"), QStringLiteral("timestamp") }, { QStringLiteral("validuntil"), QStringLiteral("timestamp") }, { QStringLiteral("trackable"), QStringLiteral("INTEGER") }, { QStringLiteral("comment"), QStringLiteral("varchar(256)")}, { QStringLiteral("name"), QStringLiteral("varchar(256)") }, LastField }; static const Fields Event_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER PRIMARY KEY") }, { QStringLiteral("user_id"), QStringLiteral("INTEGER") }, { QStringLiteral("event_id"), QStringLiteral("INTEGER") }, { QStringLiteral("installation_id"), QStringLiteral("INTEGER") }, { QStringLiteral("report_id"), QStringLiteral("INTEGER NULL") }, { QStringLiteral("task"), QStringLiteral("INTEGER") }, { QStringLiteral("comment"), QStringLiteral("varchar(256)") }, { QStringLiteral("start"), QStringLiteral("date") }, { QStringLiteral("end"), QStringLiteral("date") }, LastField }; static const Fields Subscriptions_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER PRIMARY KEY") }, { QStringLiteral("user_id"), QStringLiteral("INTEGER") }, { QStringLiteral("task"), QStringLiteral("INTEGER") }, LastField }; static const Fields Users_Fields[] = { { QStringLiteral("id"), QStringLiteral("INTEGER PRIMARY KEY") }, { QStringLiteral("user_id"), QStringLiteral("INTEGER UNIQUE") }, { QStringLiteral("name"), QStringLiteral("varchar(256)") }, LastField }; static const Fields *Database_Fields[NumberOfTables] = { MetaData_Fields, Installations_Fields, Tasks_Fields, Event_Fields, Subscriptions_Fields, Users_Fields }; const QString DatabaseName = QStringLiteral("charm.kdab.com"); const QString DriverName = QStringLiteral("QSQLITE"); SqLiteStorage::SqLiteStorage() : SqlStorage() , m_database(QSqlDatabase::addDatabase(DriverName, DatabaseName)) { if (!QSqlDatabase::isDriverAvailable(DriverName)) throw CharmException(QObject::tr("QSQLITE driver not available")); } SqLiteStorage::~SqLiteStorage() { } QString SqLiteStorage::lastInsertRowFunction() const { return QStringLiteral("last_insert_rowid"); } QString SqLiteStorage::description() const { return QObject::tr("local database"); } bool SqLiteStorage::createDatabaseTables() { Q_ASSERT_X(database().open(), Q_FUNC_INFO, "Connection to database must be established first"); bool error = false; // create tables: for (int i = 0; i < NumberOfTables; ++i) { if (!database().tables().contains(Tables[i])) { QString statement; QTextStream stream(&statement, QIODevice::WriteOnly); stream << "CREATE table `" << Tables[i] << "` ("; const Field *field = Database_Fields[i]; while (field->name != QString::null) { stream << " `" << field->name << "` " << field->type; ++field; if (field->name != QString::null) stream << ", "; } stream << ");"; QSqlQuery query(database()); query.prepare(statement); if (!runQuery(query)) error = true; } } error = error || !setMetaData(CHARM_DATABASE_VERSION_DESCRIPTOR, QString().setNum(CHARM_DATABASE_VERSION)); return !error; } bool SqLiteStorage::connect(Configuration &configuration) { // make sure the database folder exits: configuration.failure = true; const QFileInfo fileInfo(configuration.localStorageDatabase); // this is the full path // make sure the path exists, file will be created by sqlite if (!QDir().mkpath(fileInfo.absolutePath())) { configuration.failureMessage = QObject::tr("Cannot make database directory: %1").arg(qt_error_string( errno)); return false; } if (!QDir(fileInfo.absolutePath()).exists()) { configuration.failureMessage = QObject::tr("I made a directory, but it is not there. Weird."); return false; } const QDir oldDatabaseDirectory(QDir::homePath() + QDir::separator() + QStringLiteral(".Charm")); if (oldDatabaseDirectory.exists()) migrateDatabaseDirectory(oldDatabaseDirectory, fileInfo.dir()); m_database.setHostName(QStringLiteral("localhost")); const QString databaseName = fileInfo.absoluteFilePath(); m_database.setDatabaseName(databaseName); bool error = false; if (!fileInfo.exists() && !configuration.newDatabase) { error = true; configuration.failureMessage = QObject::tr( "" "

    The configuration seems to be valid, but the database " "file does not exist.

    " "

    The file will automatically be generated. Please verify " "the configuration.

    " "

    If the configuration is correct, just close the dialog.

    " ""); } if (!m_database.open()) { configuration.failureMessage = QObject::tr("Could not open SQLite database %1").arg( databaseName); return false; } if (!verifyDatabase()) { if (!createDatabase(configuration)) { configuration.failureMessage = QObject::tr( "SqLiteStorage::connect: error creating default database contents"); return false; } } if (!configuration.newDatabase) { const int userid = configuration.user.id(); const User user = getUser(userid); if (!user.isValid()) return false; configuration.user = user; } // FIXME verify that a database user id has been generated if (error) return false; configuration.failure = false; return true; } bool SqLiteStorage::migrateDatabaseDirectory(QDir oldDirectory, const QDir &newDirectory) const { if (oldDirectory == newDirectory) return true; qDebug() << "Application::configure: migrating Charm database directory contents from" << oldDirectory.absolutePath() << "to" << newDirectory.absolutePath(); oldDirectory.setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); Q_FOREACH (const QString &entry, oldDirectory.entryList()) oldDirectory.rename(entry, newDirectory.path() + QDir::separator() + entry); QDir oldDirectoryParent(oldDirectory); oldDirectoryParent.cdUp(); return oldDirectoryParent.rmpath(oldDirectory.dirName()); } bool SqLiteStorage::disconnect() { m_database.removeDatabase(DatabaseName); m_database.close(); return true; // neither of the two methods return a value } QSqlDatabase &SqLiteStorage::database() { return m_database; } bool SqLiteStorage::createDatabase(Configuration &configuration) { bool success = createDatabaseTables(); if (!success) return false; // add installation id and user id: const QString userName = configuration.user.name(); configuration.user = makeUser(userName); if (!configuration.user.isValid()) { qDebug() << "SqLiteStorage::createDatabase: cannot store user name"; return false; } return true; } Charm-1.12.0/Core/SqLiteStorage.h000066400000000000000000000030451331066577000164260ustar00rootroot00000000000000/* SqLiteStorage.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Mike McQuaid This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef SQLITESTORAGE_H #define SQLITESTORAGE_H #include #include #include "SqlStorage.h" class Configuration; class SqLiteStorage : public SqlStorage { public: SqLiteStorage(); ~SqLiteStorage(); QString description() const override; bool connect(Configuration &) override; bool disconnect() override; QSqlDatabase &database() override; protected: bool createDatabase(Configuration &) override; bool createDatabaseTables() override; bool migrateDatabaseDirectory(QDir, const QDir &) const; QString lastInsertRowFunction() const override; private: QSqlDatabase m_database; }; #endif Charm-1.12.0/Core/SqlRaiiTransactor.cpp000066400000000000000000000043601331066577000176410ustar00rootroot00000000000000/* SqlRaiiTransactor.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "SqlRaiiTransactor.h" #include "CharmExceptions.h" #include #include #include #include SqlRaiiTransactor::SqlRaiiTransactor(QSqlDatabase &database) : m_database(database) { if (!database.driver()->hasFeature(QSqlDriver::Transactions)) throw TransactionException(QObject::tr("Database driver does not support transactions.")); m_active = m_database.transaction(); if (!m_active) { throw TransactionException(QObject::tr("Starting a transaction failed: %1").arg(m_database. lastError(). text())); } } SqlRaiiTransactor::~SqlRaiiTransactor() { if (m_active) { if (!m_database.rollback()) qWarning() << "Failed to rollback transaction: " << m_database.lastError().text(); } } bool SqlRaiiTransactor::isActive() const { return m_active; } bool SqlRaiiTransactor::commit() { if (m_active) { if (m_database.commit()) { m_active = false; return true; } throw TransactionException(QObject::tr( "Failed to commit transaction: ") + m_database.lastError().text()); } return false; } Charm-1.12.0/Core/SqlRaiiTransactor.h000066400000000000000000000022501331066577000173020ustar00rootroot00000000000000/* SqlRaiiTransactor.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef SQLRAIITRANSACTOR_H #define SQLRAIITRANSACTOR_H class QSqlDatabase; class SqlRaiiTransactor { public: explicit SqlRaiiTransactor(QSqlDatabase &database); ~SqlRaiiTransactor(); bool isActive() const; bool commit(); private: bool m_active = false; QSqlDatabase &m_database; }; #endif Charm-1.12.0/Core/SqlStorage.cpp000066400000000000000000000577231331066577000163330ustar00rootroot00000000000000/* SqlStorage.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "SqlStorage.h" #include "CharmConstants.h" #include "CharmExceptions.h" #include "Event.h" #include "SqlRaiiTransactor.h" #include "State.h" #include "Task.h" #include #include #include #include #include #include #include #include #include #include #include // SqlStorage class SqlStorage::SqlStorage() { } SqlStorage::~SqlStorage() { } bool SqlStorage::verifyDatabase() { // if the database is empty, it is not ok :-) if (database().tables().isEmpty()) return false; // check database metadata, throw an exception in case the version does not match: int version = 1; QString versionString = getMetaData(CHARM_DATABASE_VERSION_DESCRIPTOR); if (versionString != QString::null) { int value; bool ok; value = versionString.toInt(&ok); if (ok) version = value; } if (version == CHARM_DATABASE_VERSION) return true; if (version > CHARM_DATABASE_VERSION) throw UnsupportedDatabaseVersionException(QObject::tr("Database version is too new.")); if (version == CHARM_DATABASE_VERSION_BEFORE_TRACKABLE) { return migrateDB(QStringLiteral( "ALTER TABLE Tasks ADD trackable INTEGER"), CHARM_DATABASE_VERSION_BEFORE_TRACKABLE); } else if (version == CHARM_DATABASE_VERSION_BEFORE_COMMENT) { return migrateDB(QStringLiteral( "ALTER TABLE Tasks ADD comment varchar(256)"), CHARM_DATABASE_VERSION_BEFORE_COMMENT); } throw UnsupportedDatabaseVersionException(QObject::tr("Database version is not supported.")); return true; } TaskList SqlStorage::getAllTasks() { TaskList tasks; QSqlQuery query(database()); query.prepare(QStringLiteral( "select * from Tasks left join Subscriptions on Tasks.task_id = Subscriptions.task;")); // FIXME merge record retrieval with getTask: if (runQuery(query)) { while (query.next()) { Task task = makeTaskFromRecord(query.record()); tasks.append(task); } } return tasks; } bool SqlStorage::setAllTasks(const User &user, const TaskList &tasks) { SqlRaiiTransactor transactor(database()); const TaskList oldTasks = getAllTasks(); // clear tasks deleteAllTasks(transactor); // add tasks Q_FOREACH (Task task, tasks) { task.setSubscribed(false); addTask(task, transactor); } // try to restore subscriptions where possible Q_FOREACH (const Task &oldTask, oldTasks) { const Task task = getTask(oldTask.id()); if (task.isValid()) { if (oldTask.subscribed()) addSubscription(user, task); } } transactor.commit(); return true; } bool SqlStorage::addTask(const Task &task) { SqlRaiiTransactor t(database()); if (addTask(task, t)) { t.commit(); return true; } else { return false; } } bool SqlStorage::addTask(const Task &task, const SqlRaiiTransactor &) { QSqlQuery query(database()); query.prepare(QLatin1String( "INSERT into Tasks (task_id, name, parent, validfrom, validuntil, trackable, comment) " "values ( :task_id, :name, :parent, :validfrom, :validuntil, :trackable, :comment);")); query.bindValue(QStringLiteral(":task_id"), task.id()); query.bindValue(QStringLiteral(":name"), task.name()); query.bindValue(QStringLiteral(":parent"), task.parent()); query.bindValue(QStringLiteral(":validfrom"), task.validFrom()); query.bindValue(QStringLiteral(":validuntil"), task.validUntil()); query.bindValue(QStringLiteral(":trackable"), task.trackable() ? 1 : 0); query.bindValue(QStringLiteral(":comment"), task.comment()); return runQuery(query); } Task SqlStorage::getTask(int taskid) { QSqlQuery query(database()); query.prepare(QStringLiteral( "SELECT * FROM Tasks LEFT JOIN Subscriptions ON Tasks.task_id = Subscriptions.task WHERE task_id = :id;")); query.bindValue(QStringLiteral(":id"), taskid); if (runQuery(query) && query.next()) { Task task = makeTaskFromRecord(query.record()); return task; } else { return Task(); } } bool SqlStorage::modifyTask(const Task &task) { QSqlQuery query(database()); query.prepare(QLatin1String("UPDATE Tasks set name = :name, parent = :parent, " "validfrom = :validfrom, validuntil = :validuntil, trackable = :trackable " "where task_id = :task_id;")); query.bindValue(QStringLiteral(":task_id"), task.id()); query.bindValue(QStringLiteral(":name"), task.name()); query.bindValue(QStringLiteral(":parent"), task.parent()); query.bindValue(QStringLiteral(":validfrom"), task.validFrom()); query.bindValue(QStringLiteral(":validuntil"), task.validUntil()); query.bindValue(QStringLiteral(":trackable"), task.trackable() ? 1 : 0); return runQuery(query); } bool SqlStorage::deleteTask(const Task &task) { SqlRaiiTransactor transactor(database()); QSqlQuery query(database()); query.prepare(QStringLiteral("DELETE from Tasks where task_id = :task_id;")); query.bindValue(QStringLiteral(":task_id"), task.id()); bool rc = runQuery(query); QSqlQuery query2(database()); query2.prepare(QStringLiteral("DELETE from Events where task = :task_id;")); query2.bindValue(QStringLiteral(":task_id"), task.id()); bool rc2 = runQuery(query2); if (rc && rc2) { transactor.commit(); return true; } else { return false; } } bool SqlStorage::deleteAllTasks() { SqlRaiiTransactor t(database()); if (deleteAllTasks(t)) { t.commit(); return true; } else { return false; } } bool SqlStorage::deleteAllTasks(const SqlRaiiTransactor &) { QSqlQuery query(database()); query.prepare(QStringLiteral("DELETE from Tasks;")); return runQuery(query); } Event SqlStorage::makeEventFromRecord(const QSqlRecord &record) { Event event; int idField = record.indexOf(QStringLiteral("event_id")); int userIdField = record.indexOf(QStringLiteral("user_id")); int reportIdField = record.indexOf(QStringLiteral("report_id")); int taskField = record.indexOf(QStringLiteral("task")); int commentField = record.indexOf(QStringLiteral("comment")); int startField = record.indexOf(QStringLiteral("start")); int endField = record.indexOf(QStringLiteral("end")); event.setId(record.field(idField).value().toInt()); event.setUserId(record.field(userIdField).value().toInt()); event.setReportId(record.field(reportIdField).value().toInt()); event.setTaskId(record.field(taskField).value().toInt()); event.setComment(record.field(commentField).value().toString()); if (!record.field(startField).isNull()) { event.setStartDateTime (record.field(startField).value().toDateTime()); } if (!record.field(endField).isNull()) { event.setEndDateTime (record.field(endField).value().toDateTime()); } return event; } EventList SqlStorage::getAllEvents() { EventList events; QSqlQuery query(database()); query.prepare(QStringLiteral("SELECT * from Events;")); if (runQuery(query)) { while (query.next()) events.append(makeEventFromRecord(query.record())); } return events; } Event SqlStorage::makeEvent() { SqlRaiiTransactor transactor(database()); Event event = makeEvent(transactor); if (event.isValid()) transactor.commit(); return event; } Event SqlStorage::makeEvent(const SqlRaiiTransactor &) { bool result; Event event; { // insert a new record in the database QSqlQuery query(database()); query.prepare(QLatin1String("INSERT into Events values " "( NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL );")); result = runQuery(query); Q_ASSERT(result); // this has to suceed } if (result) { // retrieve the AUTOINCREMENT id value of it const QString statement = QString::fromLocal8Bit( "SELECT id from Events WHERE id = %1();").arg( lastInsertRowFunction()); QSqlQuery query(database()); query.prepare(statement); result = runQuery(query); if (result && query.next()) { int indexField = query.record().indexOf(QStringLiteral("id")); event.setId(query.value(indexField).toInt()); Q_ASSERT(event.id() > 0); } else { Q_ASSERT_X(false, Q_FUNC_INFO, "database implementation error (SELECT)"); } } if (result) { // modify the created record to make sure event_id is unique // within the installation: QSqlQuery query(database()); query.prepare(QLatin1String("UPDATE Events SET event_id = :event_id, " "installation_id = :installation_id, report_id = :report_id WHERE id = :id;")); query.bindValue(QStringLiteral(":event_id"), event.id()); query.bindValue(QStringLiteral(":installation_id"), 1); query.bindValue(QStringLiteral(":report_id"), event.reportId()); query.bindValue(QStringLiteral(":id"), event.id()); result = runQuery(query); Q_ASSERT_X(result, Q_FUNC_INFO, "database implementation error (UPDATE)"); } if (result) { Q_ASSERT(event.isValid()); return event; } else { return Event(); } } Event SqlStorage::getEvent(int id) { QSqlQuery query(database()); query.prepare(QStringLiteral("SELECT * FROM Events WHERE event_id = :id;")); query.bindValue(QStringLiteral(":id"), id); if (runQuery(query) && query.next()) { Event event = makeEventFromRecord(query.record()); // FIXME this is going to fail with multiple installations Q_ASSERT(!query.next()); // eventid has to be unique Q_ASSERT(event.isValid()); // only valid events in database return event; } else { return Event(); } } bool SqlStorage:: modifyEvent(const Event &event) { SqlRaiiTransactor transactor(database()); if (modifyEvent(event, transactor)) { transactor.commit(); return true; } else { return false; } } bool SqlStorage::modifyEvent(const Event &event, const SqlRaiiTransactor &) { QSqlQuery query(database()); query.prepare(QLatin1String("UPDATE Events set task = :task, comment = :comment, " "start = :start, end = :end, user_id = :user, report_id = :report " "where event_id = :id;")); query.bindValue(QStringLiteral(":id"), event.id()); query.bindValue(QStringLiteral(":user"), event.userId()); query.bindValue(QStringLiteral(":task"), event.taskId()); query.bindValue(QStringLiteral(":report"), event.reportId()); query.bindValue(QStringLiteral(":comment"), event.comment()); query.bindValue(QStringLiteral(":start"), event.startDateTime()); query.bindValue(QStringLiteral(":end"), event.endDateTime()); return runQuery(query); } bool SqlStorage::deleteEvent(const Event &event) { QSqlQuery query(database()); query.prepare(QStringLiteral("DELETE from Events where event_id = :id;")); query.bindValue(QStringLiteral(":id"), event.id()); return runQuery(query); } bool SqlStorage::deleteAllEvents() { SqlRaiiTransactor transactor(database()); if (deleteAllEvents(transactor)) { transactor.commit(); return true; } else { return false; } } bool SqlStorage::deleteAllEvents(const SqlRaiiTransactor &) { QSqlQuery query(database()); query.prepare(QStringLiteral("DELETE from Events;")); return runQuery(query); } bool SqlStorage::runQuery(QSqlQuery &query) { #if 0 const auto MARKER = "============================================================"; qDebug() << MARKER << endl << "SqlStorage::runQuery: executing query:" << endl << query.executedQuery(); bool result = query.exec(); if (result) { qDebug() << "SqlStorage::runQuery: SUCCESS" << endl << MARKER; } else { qDebug() << "SqlStorage::runQuery: FAILURE" << endl << "Database says: " << query.lastError().databaseText() << endl << "Driver says: " << query.lastError().driverText() << endl << MARKER; } return result; #else return query.exec(); #endif } bool SqlStorage::migrateDB(const QString &queryString, int oldVersion) { const QFileInfo info(Configuration::instance().localStorageDatabase); if (info.exists()) { QFile::copy(info.fileName(), info.fileName().append(QStringLiteral("-backup-version-%1") .arg(oldVersion))); } SqlRaiiTransactor transactor(database()); QSqlQuery query(database()); query.prepare(queryString); if (!runQuery(query)) { throw UnsupportedDatabaseVersionException(QObject::tr( "Could not upgrade database from version %1 to version %2: %3").arg( QString::number( oldVersion), QString :: number(oldVersion + 1), query . lastError().text())); } setMetaData(CHARM_DATABASE_VERSION_DESCRIPTOR, QString::number(oldVersion + 1), transactor); transactor.commit(); return verifyDatabase(); } void SqlStorage::stateChanged(State previous) { Q_UNUSED(previous) // atm, SqlStorage does not care about state } User SqlStorage::getUser(int userid) { User user; QSqlQuery query(database()); query.prepare(QStringLiteral("SELECT * from Users WHERE user_id = :user_id;")); query.bindValue(QStringLiteral(":user_id"), userid); if (runQuery(query)) { if (query.next()) { int userIdPosition = query.record().indexOf(QStringLiteral("user_id")); int namePosition = query.record().indexOf(QStringLiteral("name")); Q_ASSERT(userIdPosition != -1 && namePosition != -1); user.setId(query.value(userIdPosition).toInt()); user.setName(query.value(namePosition).toString()); Q_ASSERT(user.isValid()); } else { qCritical() << "SqlStorage::getUser: no user with id" << userid; } } return user; } User SqlStorage::makeUser(const QString &name) { SqlRaiiTransactor transactor(database()); bool result; User user; user.setName(name); { // create a new record: QSqlQuery query(database()); query.prepare(QStringLiteral( "INSERT into Users ( id, user_id, name ) VALUES (NULL, NULL, :name);")); query.bindValue(QStringLiteral(":name"), user.name()); result = runQuery(query); if (!result) { qDebug() << "SqlStorage::makeUser: FAILED to create new user"; return user; } } if (result) { // find it and determine key: QSqlQuery query(database()); const QString statement = QString::fromLocal8Bit( "SELECT id from Users WHERE id = %1();").arg( lastInsertRowFunction()); query.prepare(statement); result = runQuery(query); if (result && query.next()) { int idField = query.record().indexOf(QStringLiteral("id")); user.setId(query.value(idField).toInt()); Q_ASSERT(user.id() != 0); } else { qCritical() << "SqlStorage::makeUser: FAILED to find newly created user"; return user; } } if (result) { // make a unique user id: QSqlQuery query(database()); query.prepare(QStringLiteral("UPDATE Users SET user_id = :id WHERE id = :idx;")); query.bindValue(QStringLiteral(":id"), user.id()); query.bindValue(QStringLiteral(":idx"), user.id()); result = runQuery(query); if (!result) user.setId(0); // make invalid } if (result) transactor.commit(); return user; } bool SqlStorage::modifyUser(const User &user) { QSqlQuery query(database()); query.prepare(QStringLiteral("UPDATE Users SET name = :name WHERE user_id = :id;")); query.bindValue(QStringLiteral(":name"), user.name()); query.bindValue(QStringLiteral(":id"), user.id()); return runQuery(query); } bool SqlStorage::deleteUser(const User &user) { QSqlQuery query(database()); query.prepare(QStringLiteral("DELETE from Users WHERE user_id = :id;")); query.bindValue(QStringLiteral(":id"), user.id()); return runQuery(query); } bool SqlStorage::addSubscription(User user, Task task) { Task dbTask = getTask(task.id()); if (!dbTask.isValid() || (dbTask.isValid() && !dbTask.subscribed())) { QSqlQuery query(database()); query.prepare(QStringLiteral("INSERT into Subscriptions VALUES (NULL, :user_id, :task);")); query.bindValue(QStringLiteral(":user_id"), user.id()); query.bindValue(QStringLiteral(":task"), task.id()); return runQuery(query); } else { return true; } } bool SqlStorage::deleteSubscription(User user, Task task) { QSqlQuery query(database()); query.prepare(QStringLiteral( "DELETE from Subscriptions WHERE user_id = :user_id AND task = :task;")); query.bindValue(QStringLiteral(":user_id"), user.id()); query.bindValue(QStringLiteral(":task"), task.id()); return runQuery(query); } bool SqlStorage::setMetaData(const QString &key, const QString &value) { SqlRaiiTransactor transactor(database()); if (!setMetaData(key, value, transactor)) { return false; } else { return transactor.commit(); } } bool SqlStorage::setMetaData(const QString &key, const QString &value, const SqlRaiiTransactor &) { // find out if the key is in the database: bool result; { QSqlQuery query(database()); query.prepare(QStringLiteral("SELECT * FROM MetaData WHERE MetaData.key = :key;")); query.bindValue(QStringLiteral(":key"), key); if (runQuery(query) && query.next()) { result = true; } else { result = false; } } if (result) { // key exists, let's update: QSqlQuery query(database()); query.prepare(QStringLiteral("UPDATE MetaData SET value = :value WHERE key = :key;")); query.bindValue(QStringLiteral(":value"), value); query.bindValue(QStringLiteral(":key"), key); return runQuery(query); } else { // key does not exist, let's insert: QSqlQuery query(database()); query.prepare(QStringLiteral("INSERT INTO MetaData VALUES ( NULL, :key, :value );")); query.bindValue(QStringLiteral(":key"), key); query.bindValue(QStringLiteral(":value"), value); return runQuery(query); } return false; // never reached } QString SqlStorage::getMetaData(const QString &key) { QSqlQuery query(database()); query.prepare(QStringLiteral("SELECT * FROM MetaData WHERE key = :key;")); query.bindValue(QStringLiteral(":key"), key); if (runQuery(query) && query.next()) { int valueField = query.record().indexOf(QStringLiteral("value")); return query.value(valueField).toString(); } else { return QString::null; } } Task SqlStorage::makeTaskFromRecord(const QSqlRecord &record) { Task task; int idField = record.indexOf(QStringLiteral("task_id")); int nameField = record.indexOf(QStringLiteral("name")); int parentField = record.indexOf(QStringLiteral("parent")); int useridField = record.indexOf(QStringLiteral("user_id")); int validfromField = record.indexOf(QStringLiteral("validfrom")); int validuntilField = record.indexOf(QStringLiteral("validuntil")); int trackableField = record.indexOf(QStringLiteral("trackable")); int commentField = record.indexOf(QStringLiteral("comment")); task.setId(record.field(idField).value().toInt()); task.setName(record.field(nameField).value().toString()); task.setParent(record.field(parentField).value().toInt()); task.setSubscribed(!record.field(useridField).value().toString().isEmpty()); QString from = record.field(validfromField).value().toString(); if (!from.isEmpty()) { task.setValidFrom (record.field(validfromField).value().toDateTime()); } QString until = record.field(validuntilField).value().toString(); if (!until.isEmpty()) { task.setValidUntil (record.field(validuntilField).value().toDateTime()); } const QVariant trackableValue = record.field(trackableField).value(); if (!trackableValue.isNull() && trackableValue.isValid()) task.setTrackable(trackableValue.toInt() == 1); const QVariant commentValue = record.field(commentField).value(); if (!commentValue.isNull() && commentValue.isValid()) task.setComment(commentValue.toString()); return task; } QString SqlStorage::setAllTasksAndEvents(const User &user, const TaskList &tasks, const EventList &events) { SqlRaiiTransactor transactor(database()); // clear subscriptions, tasks and events: if (!deleteAllEvents(transactor)) return QObject::tr("Error deleting the existing events."); Q_ASSERT(getAllEvents().isEmpty()); if (!deleteAllTasks(transactor)) return QObject::tr("Error deleting the existing tasks."); Q_ASSERT(getAllTasks().isEmpty()); // now import Events and Tasks from the XML document: Q_FOREACH (const Task &task, tasks) { // don't use our own addTask method, it emits signals and that // confuses the model, because the task tree is not inserted depth-first: if (addTask(task, transactor)) { if (task.subscribed()) { bool result = addSubscription(user, task); Q_ASSERT(result); Q_UNUSED(result); } else { bool result = deleteSubscription(user, task); Q_ASSERT(result); Q_UNUSED(result); } } else { return QObject::tr("Cannot add imported tasks."); } } Q_FOREACH (const Event &event, events) { if (!event.isValid()) continue; Task task = getTask(event.taskId()); if (!task.isValid()) { // semantical error continue; } Event newEvent = makeEvent(transactor); int id = newEvent.id(); newEvent = event; newEvent.setId(id); if (!modifyEvent(newEvent, transactor)) return QObject::tr("Error adding imported event."); } transactor.commit(); return QString(); } Charm-1.12.0/Core/SqlStorage.h000066400000000000000000000106201331066577000157610ustar00rootroot00000000000000/* SqlStorage.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef SQLSTORAGE_H #define SQLSTORAGE_H #include #include "Task.h" #include "User.h" #include "State.h" #include "Event.h" #include "CharmExceptions.h" class QSqlDatabase; class QSqlQuery; class QSqlRecord; class Configuration; class SqlRaiiTransactor; class SqlStorage { public: SqlStorage(); virtual ~SqlStorage(); // a readable description for the user virtual QString description() const = 0; // backend availability virtual bool connect(Configuration &) = 0; virtual bool disconnect() = 0; virtual QSqlDatabase &database() = 0; // application: void stateChanged(State previous); // user database functions: User getUser(int userid); User makeUser(const QString &name); bool modifyUser(const User &user); bool deleteUser(const User &user); // task database functions: TaskList getAllTasks(); bool setAllTasks(const User &user, const TaskList &tasks); bool addTask(const Task &task); bool addTask(const Task &task, const SqlRaiiTransactor &); Task getTask(int taskid); bool modifyTask(const Task &task); bool deleteTask(const Task &task); bool deleteAllTasks(); bool deleteAllTasks(const SqlRaiiTransactor &); // event database functions: EventList getAllEvents(); // all events are created by the storage interface Event makeEvent(); Event makeEvent(const SqlRaiiTransactor &); Event getEvent(int eventid); bool modifyEvent(const Event &event); bool modifyEvent(const Event &event, const SqlRaiiTransactor &); bool deleteEvent(const Event &event); bool deleteAllEvents(); bool deleteAllEvents(const SqlRaiiTransactor &); // subscription management functions // (subscriptions cannot be modified, they are just boolean flags) // (subscription status is retrieved with the tasks) bool addSubscription(User, Task); bool deleteSubscription(User, Task); // implement metadata management functions: bool setMetaData(const QString &, const QString &); bool setMetaData(const QString &, const QString &, const SqlRaiiTransactor &); // database metadata management functions QString getMetaData(const QString &); /*! @brief update all tasks and events in a single-transaction during imports @return an empty String on success, an error message otherwise */ QString setAllTasksAndEvents(const User &, const TaskList &, const EventList &); /** * @throws UnsupportedDatabaseVersionException */ bool verifyDatabase(); virtual bool createDatabaseTables() = 0; // run the query and process possible errors static bool runQuery(QSqlQuery &); protected: // Put the basic database structure into the database. // This includes creating the tables et cetera. // Different backends will have to reimplement this function to // get special requirements in. // return true if successful, false otherwise virtual bool createDatabase(Configuration &) = 0; /** Verify database content and database version. * Will return false if the database is found, but for some reason does not contain * the complete structure (which is a very unusual odd case). * It will throw an UnsupportedDatabaseVersionException if the database version does * not match the one the client was compiled against. */ virtual QString lastInsertRowFunction() const = 0; private: bool migrateDB(const QString &queryString, int oldVersion); Event makeEventFromRecord(const QSqlRecord &); Task makeTaskFromRecord(const QSqlRecord &); }; #endif Charm-1.12.0/Core/State.cpp000066400000000000000000000020561331066577000153140ustar00rootroot00000000000000/* State.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "State.h" const char *StateNames[] = { "Constructed", "StartingUp", "Configuring", "Connecting", "Connected", "Disconnecting", "ShuttingDown", "Dead", // bad :-) }; Charm-1.12.0/Core/State.h000066400000000000000000000022661331066577000147640ustar00rootroot00000000000000/* State.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_STATE_H #define CHARM_STATE_H #include enum State { Constructed, StartingUp, Configuring, Connecting, Connected, Disconnecting, ShuttingDown, Dead, NumberOfCharmApplicationStates }; Q_DECLARE_METATYPE(State) extern const char *StateNames[NumberOfCharmApplicationStates]; #endif Charm-1.12.0/Core/Task.cpp000066400000000000000000000262301331066577000151360ustar00rootroot00000000000000/* Task.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: Mike McQuaid This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Task.h" #include "CharmConstants.h" #include "CharmExceptions.h" #include #include #include Task::Task() { } Task::Task(TaskId id, const QString &name, TaskId parent, bool subscribed) : m_id(id) , m_parent(parent) , m_name(name) , m_subscribed(subscribed) { } bool Task::isValid() const { return id() != 0; } QString Task::tagName() { static const QString tag(QStringLiteral("task")); return tag; } QString Task::taskListTagName() { static const QString tag(QStringLiteral("tasks")); return tag; } bool Task::operator ==(const Task &other) const { return other.id() == id() && other.parent() == parent() && other.name() == name() && other.subscribed() == subscribed() && other.m_trackable == m_trackable && other.validFrom() == validFrom() && other.validUntil() == validUntil(); } TaskId Task::id() const { return m_id; } void Task::setId(TaskId id) { m_id = id; } QString Task::name() const { return m_name; } void Task::setName(const QString &name) { m_name = name; } int Task::parent() const { return m_parent; } void Task::setParent(int parent) { m_parent = parent; } bool Task::subscribed() const { return m_subscribed; } void Task::setSubscribed(bool value) { m_subscribed = value; } bool Task::trackable() const { return m_trackable; } void Task::setTrackable(bool trackable) { m_trackable = trackable; } QDateTime Task::validFrom() const { return m_validFrom; } void Task::setValidFrom(const QDateTime &stamp) { m_validFrom = stamp; QTime time(m_validFrom.time()); time.setHMS(time.hour(), time.minute(), time.second()); m_validFrom.setTime(time); } QDateTime Task::validUntil() const { return m_validUntil; } void Task::setValidUntil(const QDateTime &stamp) { m_validUntil = stamp; QTime time(m_validUntil.time()); time.setHMS(time.hour(), time.minute(), time.second()); m_validUntil.setTime(time); } bool Task::isCurrentlyValid() const { return isValid() && (!validFrom().isValid() || validFrom() < QDateTime::currentDateTime()) && (!validUntil().isValid() || validUntil() > QDateTime::currentDateTime()); } void Task::dump() const { qDebug() << "[Task " << this << "] task id:" << id() << "- name:" << name() << " - parent:" << parent() << " - subscribed:" << subscribed() << " - valid from:" << validFrom() << " - valid until:" << validUntil() << " - trackable:" << trackable(); } void dumpTaskList(const TaskList &tasks) { qDebug() << "dumpTaskList: task list of" << tasks.size() << "elements"; for (int i = 0; i < tasks.size(); ++i) tasks[i].dump(); } // FIXME make XmlSerializable interface, with tagName/toXml/fromXml: const QString TaskIdElement(QStringLiteral("taskid")); const QString TaskParentId(QStringLiteral("parentid")); const QString TaskSubscribed(QStringLiteral("subscribed")); const QString TaskTrackable(QStringLiteral("trackable")); const QString TaskComment(QStringLiteral("comment")); const QString TaskValidFrom(QStringLiteral("validfrom")); const QString TaskValidUntil(QStringLiteral("validuntil")); QDomElement Task::toXml(QDomDocument document) const { QDomElement element = document.createElement(tagName()); element.setAttribute(TaskIdElement, id()); element.setAttribute(TaskParentId, parent()); element.setAttribute(TaskSubscribed, (subscribed() ? 1 : 0)); element.setAttribute(TaskTrackable, (trackable() ? 1 : 0)); if (!name().isEmpty()) { QDomText taskName = document.createTextNode(name()); element.appendChild(taskName); } if (validFrom().isValid()) element.setAttribute(TaskValidFrom, validFrom().toString(Qt::ISODate)); if (validUntil().isValid()) element.setAttribute(TaskValidUntil, validUntil().toString(Qt::ISODate)); return element; } Task Task::fromXml(const QDomElement &element, int databaseSchemaVersion) { // in case any task object creates trouble with // serialization/deserialization, add an object of it to // void XmlSerializationTests::testTaskSerialization() if (element.tagName() != tagName()) throw XmlSerializationException(QObject::tr( "Task::fromXml: judging from the tag name, this is not a task tag")); Task task; bool ok; task.setName(element.text()); task.setId(element.attribute(TaskIdElement).toInt(&ok)); if (!ok) throw XmlSerializationException(QObject::tr("Task::fromXml: invalid task id")); task.setParent(element.attribute(TaskParentId).toInt(&ok)); if (!ok) throw XmlSerializationException(QObject::tr("Task::fromXml: invalid parent task id")); task.setSubscribed(element.attribute(TaskSubscribed).toInt(&ok) == 1); if (!ok) throw XmlSerializationException(QObject::tr("Task::fromXml: invalid subscription setting")); if (databaseSchemaVersion > CHARM_DATABASE_VERSION_BEFORE_TASK_EXPIRY) { if (element.hasAttribute(TaskValidFrom)) { QDateTime start = QDateTime::fromString(element.attribute(TaskValidFrom), Qt::ISODate); if (!start.isValid()) throw XmlSerializationException(QObject::tr( "Task::fromXml: invalid valid-from date")); task.setValidFrom(start); } if (element.hasAttribute(TaskValidUntil)) { QDateTime end = QDateTime::fromString(element.attribute(TaskValidUntil), Qt::ISODate); if (!end.isValid()) throw XmlSerializationException(QObject::tr( "Task::fromXml: invalid valid-until date")); task.setValidUntil(end); } } if (databaseSchemaVersion > CHARM_DATABASE_VERSION_BEFORE_TRACKABLE) { task.setTrackable(element.attribute(TaskTrackable, QStringLiteral("1")).toInt(&ok) == 1); if (!ok) throw XmlSerializationException(QObject::tr("Task::fromXml: invalid trackable settings")); } if (element.hasAttribute(TaskComment)) task.setComment(element.attribute(TaskComment)); return task; } TaskList Task::readTasksElement(const QDomElement &element, int databaseSchemaVersion) { if (element.tagName() == taskListTagName()) { TaskList tasks; for (QDomElement child = element.firstChildElement(); !child.isNull(); child = child.nextSiblingElement(tagName())) { if (child.tagName() != tagName()) throw XmlSerializationException(QObject::tr( "Task::readTasksElement: parent-child mismatch")); Task task = fromXml(child, databaseSchemaVersion); tasks << task; } return tasks; } else { throw XmlSerializationException(QObject::tr( "Task::readTasksElement: judging by the tag name, this is not a tasks element")); } } QDomElement Task::makeTasksElement(QDomDocument document, const TaskList &tasks) { QDomElement element = document.createElement(taskListTagName()); Q_FOREACH (const Task &task, tasks) element.appendChild(task.toXml(document)); return element; } bool Task::lowerTaskId(const Task &left, const Task &right) { return left.id() < right.id(); } QString Task::comment() const { return m_comment; } void Task::setComment(const QString &comment) { m_comment = comment; } bool Task::checkForUniqueTaskIds(const TaskList &tasks) { std::set ids; for (TaskList::const_iterator it = tasks.begin(); it != tasks.end(); ++it) ids.insert((*it).id()); return static_cast(ids.size()) == tasks.size(); } /** collectTaskIds visits the task and all subtasks recursively, and * adds all visited task ids to visitedIds. * @returns false if any visited task id is already in visitedIds * @param id the parent task to traverse * @param visitedIds reference to a TaskId set that contains already * visited task ids * @param tasks the tasklist to process */ bool collectTaskIds(std::set &visitedIds, TaskId id, const TaskList &tasks) { bool foundSelf = false; TaskIdList children; // find children and the task itself (the parameter tasks is not sorted) for (TaskList::const_iterator it = tasks.begin(); it != tasks.end(); ++it) { if ((*it).parent() == id) children << (*it).id(); if ((*it).id() == id) { // just checking that it really exists... if (std::find(visitedIds.begin(), visitedIds.end(), id) != visitedIds.end()) { return false; } else { if ((*it).isValid()) { visitedIds.insert(id); foundSelf = true; } else { return false; } } } } if (!foundSelf) return false; Q_FOREACH (const TaskId i, children) collectTaskIds(visitedIds, i, tasks); return true; } /** checkForTreeness checks a task list against cycles in the * parent-child relationship, and for orphans (tasks where the parent * task does not exist). If the task list contains invalid tasks, * false is returned as well. * * @return false, if cycles in the task tree or orphans have been found * @param tasks the tasklist to verify */ bool Task::checkForTreeness(const TaskList &tasks) { std::set ids; for (TaskList::const_iterator it = tasks.begin(); it != tasks.end(); ++it) { if (!(*it).isValid()) return false; if ((*it).parent() == 0) { if (!collectTaskIds(ids, (*it).id(), tasks)) return false; } } // the count of ids now must be equal to the count of tasks, // otherwise tasks contains elements that are not in the subtrees // of toplevel elements if (ids.size() != static_cast(tasks.size())) { #ifndef NDEBUG Q_FOREACH (const Task &task, tasks) { if (find(ids.begin(), ids.end(), task.id()) == ids.end()) { qDebug() << "Orphan task:"; task.dump(); } } #endif return false; } return true; } Charm-1.12.0/Core/Task.h000066400000000000000000000063651331066577000146120ustar00rootroot00000000000000/* Task.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TASK_H #define TASK_H #include #include #include #include #include #include #include #include typedef int TaskId; Q_DECLARE_METATYPE(TaskId) class Task; /** A task list is a list of tasks that belong together. Example: All tasks for one user. */ typedef QList TaskList; typedef QList TaskIdList; /** A task is a category under which events are filed. It has a unique identifier and a name. */ class Task { public: Task(); /** Convenience constructor. */ Task(TaskId id, const QString &name, TaskId parent = 0, bool subscribed = false); bool isValid() const; bool operator ==(const Task &other) const; bool operator !=(const Task &other) const { return !operator==(other); } TaskId id() const; void setId(TaskId id); QString name() const; void setName(const QString &name); TaskId parent() const; void setParent(TaskId parent); bool subscribed() const; void setSubscribed(bool value); QDateTime validFrom() const; void setValidFrom(const QDateTime &); QDateTime validUntil() const; void setValidUntil(const QDateTime &); bool isCurrentlyValid() const; void setTrackable(bool trackable); bool trackable() const; QString comment() const; void setComment(const QString &comment); void dump() const; static QString tagName(); static QString taskListTagName(); QDomElement toXml(QDomDocument) const; static Task fromXml(const QDomElement &, int databaseSchemaVersion = 1); static TaskList readTasksElement(const QDomElement &, int databaseSchemaVersion = 1); static QDomElement makeTasksElement(QDomDocument, const TaskList &); static bool checkForUniqueTaskIds(const TaskList &tasks); static bool checkForTreeness(const TaskList &tasks); static bool lowerTaskId(const Task &left, const Task &right); private: int m_id = 0; int m_parent = 0; QString m_name; bool m_subscribed = false; bool m_trackable = true; /** The timestamp from which the task is valid. */ QDateTime m_validFrom; /** The timestamp after which the task becomes invalid. */ QDateTime m_validUntil; QString m_comment; }; Q_DECLARE_METATYPE(TaskIdList) Q_DECLARE_METATYPE(TaskList) Q_DECLARE_METATYPE(Task) void dumpTaskList(const TaskList &tasks); #endif Charm-1.12.0/Core/TaskListMerger.cpp000066400000000000000000000103751331066577000171370ustar00rootroot00000000000000/* TaskListMerger.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TaskListMerger.h" #include "CharmExceptions.h" TaskListMerger::TaskListMerger() { } void TaskListMerger::setOldTasks(const TaskList &tasks) { verifyTaskList(tasks); m_oldTasks = tasks; for (TaskList::iterator it = m_oldTasks.begin(); it != m_oldTasks.end(); ++it) (*it).setSubscribed(false); qSort(m_oldTasks.begin(), m_oldTasks.end(), Task::lowerTaskId); m_resultsValid = false; } void TaskListMerger::setNewTasks(const TaskList &tasks) { verifyTaskList(tasks); m_newTasks = tasks; for (TaskList::iterator it = m_newTasks.begin(); it != m_newTasks.end(); ++it) (*it).setSubscribed(false); qSort(m_newTasks.begin(), m_newTasks.end(), Task::lowerTaskId); m_resultsValid = false; } void TaskListMerger::calculateResults() const { if (m_resultsValid) return; // insert sentinels at end of list: const TaskId maxId = qMax( m_oldTasks.isEmpty() ? 0 : m_oldTasks.last().id(), m_newTasks.isEmpty() ? 0 : m_newTasks.last().id()); const TaskId sentinelId = maxId + 1; const Task sentinel(sentinelId, QObject::tr("Sentinel Task")); TaskList oldTasks(m_oldTasks); TaskList newTasks(m_newTasks); oldTasks << sentinel; newTasks << sentinel; TaskList::iterator oldIt = oldTasks.begin(); TaskList::iterator newIt = newTasks.begin(); do { if ((*oldIt).id() < (*newIt).id()) { // there is a task in the old task list that is not an // element of the new task list, ignore (the user added it // manually) ++oldIt; } else if ((*oldIt).id() == (*newIt).id()) { // if (*oldIt != *newIt) { m_modifiedTasks << (*oldIt); *oldIt = *newIt; } ++oldIt; ++newIt; } else { // there are tasks in newtasks that are not in oldtasks, // so they are new m_addedTasks << *newIt; ++newIt; } } while (oldIt != oldTasks.end() || newIt != newTasks.end()); oldTasks.pop_back(); // remove sentinel m_results = oldTasks + m_addedTasks; // one last check: if tasks where modified through the new task // lists, maybe local-only tasks have become orphans? if (!Task::checkForUniqueTaskIds(m_results)) throw InvalidTaskListException(QObject::tr( "the merged task list is invalid, it contains duplicate task ids")); if (!Task::checkForTreeness(m_results)) throw InvalidTaskListException(QObject::tr( "the merged tasks database is not a directed graph, this is seriously bad, go fix it")); m_resultsValid = true; } void TaskListMerger::verifyTaskList(const TaskList &tasks) { if (!Task::checkForUniqueTaskIds(tasks)) throw InvalidTaskListException(QObject::tr("task list contains duplicate task ids")); if (!Task::checkForTreeness(tasks)) throw InvalidTaskListException(QObject::tr( "task list is not a directed graph, this is seriously bad, go fix it")); } TaskList TaskListMerger::addedTasks() const { calculateResults(); return m_addedTasks; } TaskList TaskListMerger::modifiedTasks() const { calculateResults(); return m_modifiedTasks; } TaskList TaskListMerger::mergedTaskList() const { calculateResults(); return m_results; } Charm-1.12.0/Core/TaskListMerger.h000066400000000000000000000044121331066577000165770ustar00rootroot00000000000000/* TaskListMerger.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TASKLISTMERGER_H #define TASKLISTMERGER_H #include "Task.h" /** TaskListMerger merges two task lists into a single one. * * It merges the existing tasks in oldtasks with the possibly new * state in newtasks. * * There are a number of assumptions in the merge algorithm: First of * all, it assumes tasks never get deleted. Instead of deleting them, * they would be marked as expired by setting their valid-until * date. Second (based on the first assumption), tasks that exist in * oldtasks but not in newtasks are assumed to be locally added, and * left untouched. Third, tasks are recognized by their task id. * * If a task id exists in newtasks, but not in oldtasks, the task is * assumed new. If a task id exists in both lists, but differs, * newtasks is assumed to contain the newer state, and the returned * list will contain the task as it was in newtasks. */ class TaskListMerger { public: TaskListMerger(); void setOldTasks(const TaskList &tasks); void setNewTasks(const TaskList &tasks); TaskList mergedTaskList() const; TaskList addedTasks() const; TaskList modifiedTasks() const; private: void verifyTaskList(const TaskList &tasks); void calculateResults() const; mutable bool m_resultsValid = false; TaskList m_oldTasks; TaskList m_newTasks; mutable TaskList m_results; mutable TaskList m_addedTasks; mutable TaskList m_modifiedTasks; }; #endif Charm-1.12.0/Core/TaskModelInterface.h000066400000000000000000000031341331066577000174030ustar00rootroot00000000000000/* TaskModelInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MODELINTERFACE_H #define MODELINTERFACE_H class QModelIndex; #include "Task.h" #include "Event.h" class TaskModelInterface { public: virtual ~TaskModelInterface() { } virtual Task taskForIndex(const QModelIndex &) const = 0; virtual QModelIndex indexForTaskId(TaskId) const = 0; virtual bool taskIsActive(const Task &task) const = 0; virtual bool taskHasChildren(const Task &task) const = 0; virtual bool taskIdExists(TaskId taskId) const = 0; // relayed model signals, in lack of notification in the view: // eventActivated was already taken by CharmDataModelAdapterInterface virtual void eventActivationNotice(EventId id) = 0; virtual void eventDeactivationNotice(EventId id) = 0; }; #endif Charm-1.12.0/Core/TaskTreeItem.cpp000066400000000000000000000071601331066577000165760ustar00rootroot00000000000000/* TaskTreeItem.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TaskTreeItem.h" #include TaskTreeItem::TaskTreeItem() { } TaskTreeItem::TaskTreeItem(const Task &task, TaskTreeItem *parent) : m_parent(parent) , m_task(task) { if (m_parent) m_parent->m_children.append(this); } TaskTreeItem::TaskTreeItem(const TaskTreeItem &other) { if (this != &other) *this = other; } TaskTreeItem &TaskTreeItem::operator=(const TaskTreeItem &other) { if (this != &other) { m_children = other.m_children; m_parent = other.m_parent; m_task = other.m_task; if (m_parent) m_parent->m_children.append(this); } return *this; } TaskTreeItem::~TaskTreeItem() { if (m_parent) m_parent->m_children.removeAt(row()); } void TaskTreeItem::makeChildOf(TaskTreeItem &parent) { if (m_parent != &parent) { Q_ASSERT(!parent.m_children.contains(this)); // that would be wrong // if there is an existing parent, unregister with it: // parent can only be zero if there never was a parent so far if (m_parent != 0) { m_parent->m_children.removeAt(row()); m_parent = nullptr; } // register with the new parent m_parent = &parent; parent.m_children.append(this); } else { // hm, should this be allowed? // done } } Task &TaskTreeItem::task() { return m_task; } const Task &TaskTreeItem::task() const { return m_task; } bool TaskTreeItem::isValid() const { return m_parent != 0 && m_task.isValid(); } const TaskTreeItem &TaskTreeItem::child(int row) const { static TaskTreeItem InvalidItem; if (row >= 0 && row < m_children.size()) { return *m_children.at(row); } else { Q_ASSERT_X(false, Q_FUNC_INFO, "Invalid item position"); return InvalidItem; } } int TaskTreeItem::row() const { if (m_parent) { int row = m_parent->m_children.indexOf(this); Q_ASSERT_X(row != -1, Q_FUNC_INFO, "Internal error - cannot find myself in my parents family"); return row; } else { Q_ASSERT_X(false, Q_FUNC_INFO, "Calling row() on an invalid item"); return -1; } } int TaskTreeItem::childCount() const { return m_children.size(); } TaskList TaskTreeItem::children() const { TaskList tasks; for (int i = 0; i < m_children.size(); ++i) { tasks << m_children[i]->task() << m_children[i]->children(); } return tasks; } TaskIdList TaskTreeItem::childIds() const { TaskIdList idList; // get the list of children, and sort by task id: Q_FOREACH (const TaskTreeItem *item, m_children) idList.append(item->task().id()); return idList; } Charm-1.12.0/Core/TaskTreeItem.h000066400000000000000000000045711331066577000162460ustar00rootroot00000000000000/* TaskTreeItem.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TASKTREEITEM_H #define TASKTREEITEM_H #include #include "Task.h" /** TaskTreeItem is a node in the task tree. The tasks form a tree, since tasks can belong to a parent task. This structure is modeled by storing TaskTreeItems in a map that associates them with the respective task id. Every TaskTreeItem has a parent that represents the parent task. If a task has no parent, it is a child of the root TaskTreeItem stored in the model. Every TaskTreeItem keeps a list of children. Every TaskTreeItem also has a position in it's parents list of children. This integer position can be retrieved by calling row on the item. */ class TaskTreeItem { public: typedef QList ConstPointerList; typedef std::map Map; TaskTreeItem(); explicit TaskTreeItem(const Task &task, TaskTreeItem *parent = nullptr); TaskTreeItem(const TaskTreeItem &other); TaskTreeItem &operator=(const TaskTreeItem &other); ~TaskTreeItem(); void makeChildOf(TaskTreeItem &parent); bool isValid() const; Task &task(); const Task &task() const; const TaskTreeItem &child(int row) const; int row() const; int childCount() const; // recursively find all children of this item // warning: SLOW TaskList children() const; TaskIdList childIds() const; private: TaskTreeItem *m_parent = nullptr; ConstPointerList m_children; Task m_task; }; #endif Charm-1.12.0/Core/TimeSpans.cpp000066400000000000000000000130101331066577000161270ustar00rootroot00000000000000/* TimeSpans.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TimeSpans.h" #include #include TimeSpans::TimeSpans(const QDate &today) { m_today.name = tr("Today"); m_today.timespan = TimeSpan(today, today.addDays(1)); m_today.timeSpanType = Day; m_yesterday.name = tr("Yesterday"); m_yesterday.timespan = TimeSpan(m_today.timespan.first.addDays(-1), m_today.timespan.second.addDays(-1)); m_yesterday.timeSpanType = Day; m_dayBeforeYesterday.name = tr("The Day Before Yesterday"); m_dayBeforeYesterday.timespan = TimeSpan(m_today.timespan.first.addDays(-2), m_today.timespan.second.addDays(-2)); m_dayBeforeYesterday.timeSpanType = Day; m_thisWeek.name = tr("This Week"); m_thisWeek.timespan = TimeSpan(today.addDays(-today.dayOfWeek() + 1), today.addDays(7 - today.dayOfWeek() + 1)); m_thisWeek.timeSpanType = Week; m_lastWeek.name = tr("Last Week"); m_lastWeek.timespan = TimeSpan(m_thisWeek.timespan.first.addDays(-7), m_thisWeek.timespan.second.addDays(-7)); m_lastWeek.timeSpanType = Week; m_theWeekBeforeLast.name = tr("The Week Before Last Week"); m_theWeekBeforeLast.timespan = TimeSpan(m_thisWeek.timespan.first.addDays(-14), m_thisWeek.timespan.second.addDays(-14)); m_theWeekBeforeLast.timeSpanType = Week; m_3WeeksAgo.name = tr("3 Weeks Ago"); m_3WeeksAgo.timespan = TimeSpan(m_thisWeek.timespan.first.addDays(-21), m_thisWeek.timespan.second.addDays(-21)); m_3WeeksAgo.timeSpanType = Week; m_thisMonth.name = tr("This Month"); m_thisMonth.timespan = TimeSpan(today.addDays(-today.day() + 1), today.addDays(today.daysInMonth() - today.day() + 1)); m_thisMonth.timeSpanType = Month; m_lastMonth.name = tr("Last Month"); m_lastMonth.timespan = TimeSpan(m_thisMonth.timespan.first.addMonths(-1), m_thisMonth.timespan.second.addMonths(-1)); m_lastMonth.timeSpanType = Month; m_theMonthBeforeLast.name = tr("The Month Before Last Month"); m_theMonthBeforeLast.timespan = TimeSpan(m_thisMonth.timespan.first.addMonths(-2), m_thisMonth.timespan.second.addMonths(-2)); m_theMonthBeforeLast.timeSpanType = Month; m_3MonthsAgo.name = tr("3 Months Ago"); m_3MonthsAgo.timespan = TimeSpan(m_thisMonth.timespan.first.addMonths(-3), m_thisMonth.timespan.second.addMonths(-3)); m_3MonthsAgo.timeSpanType = Month; m_thisYear.name = tr("This year"); m_thisYear.timespan = TimeSpan(QDate(today.year(), 1, 1), QDate(today.year(), 12, 31)); m_thisYear.timeSpanType = Year; } QList TimeSpans::standardTimeSpans() const { QList spans; spans << m_today << m_yesterday << m_dayBeforeYesterday << m_thisWeek << m_lastWeek << m_theWeekBeforeLast << m_thisMonth << m_lastMonth << m_thisYear; return spans; } QList TimeSpans::last4Weeks() const { QList spans; spans << m_thisWeek << m_lastWeek << m_theWeekBeforeLast << m_3WeeksAgo; return spans; } QList TimeSpans::last4Months() const { QList spans; spans << m_thisMonth << m_lastMonth << m_theMonthBeforeLast << m_3MonthsAgo; return spans; } NamedTimeSpan TimeSpans::today() const { return m_today; } NamedTimeSpan TimeSpans::yesterday() const { return m_yesterday; } NamedTimeSpan TimeSpans::dayBeforeYesterday() const { return m_dayBeforeYesterday; } NamedTimeSpan TimeSpans::thisWeek() const { return m_thisWeek; } NamedTimeSpan TimeSpans::lastWeek() const { return m_lastWeek; } NamedTimeSpan TimeSpans::theWeekBeforeLast() const { return m_theWeekBeforeLast; } NamedTimeSpan TimeSpans::thisMonth() const { return m_thisMonth; } NamedTimeSpan TimeSpans::lastMonth() const { return m_lastMonth; } NamedTimeSpan TimeSpans::theMonthBeforeLast() const { return m_theMonthBeforeLast; } NamedTimeSpan TimeSpans::thisYear() const { return m_thisYear; } bool NamedTimeSpan::contains(const QDate &date) const { return date >= timespan.first && date < timespan.second; } DateChangeWatcher::DateChangeWatcher(QObject *parent) : QObject(parent) { connect(&m_timer, SIGNAL(timeout()), SLOT(slotTimeout())); m_timer.start(1000 * 60); slotTimeout(); } void DateChangeWatcher::slotTimeout() { const QDate today = QDate::currentDate(); if (m_today == today) return; m_today = today; emit dateChanged(); } Charm-1.12.0/Core/TimeSpans.h000066400000000000000000000062351331066577000156070ustar00rootroot00000000000000/* TimeSpans.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TIMESPANS_H #define TIMESPANS_H #include #include #include #include #include #include typedef QPair TimeSpan; enum TimeSpanType { Day = 0, Week, Month, Range, Year }; struct NamedTimeSpan { QString name; TimeSpan timespan; bool contains(const QDate &date) const; TimeSpanType timeSpanType; }; /** Provides commonly used time spans for a given date. The spans are measured from a start time to *before* an end time. today() looks like this: today, 0:00 - tomorrow, 0:00 To see of a date (or datetime) is within the span, test for x >= start && x < end. TimeSpan only deals with days, not with anything of finer granularity. */ class TimeSpans { Q_DECLARE_TR_FUNCTIONS(TimeSpans) public: /** * Creates a collection of timespans with @p referenceDate as reference date. * * @param referenceDate the reference date ("today") to calculate time spans for */ explicit TimeSpans(const QDate &referenceDate = QDate::currentDate()); QList standardTimeSpans() const; QList last4Weeks() const; QList last4Months() const; NamedTimeSpan today() const; NamedTimeSpan yesterday() const; NamedTimeSpan dayBeforeYesterday() const; NamedTimeSpan thisWeek() const; NamedTimeSpan lastWeek() const; NamedTimeSpan theWeekBeforeLast() const; NamedTimeSpan thisMonth() const; NamedTimeSpan thisYear() const; NamedTimeSpan lastMonth() const; NamedTimeSpan theMonthBeforeLast() const; private: NamedTimeSpan m_today; NamedTimeSpan m_yesterday; NamedTimeSpan m_dayBeforeYesterday; NamedTimeSpan m_thisWeek; NamedTimeSpan m_lastWeek; NamedTimeSpan m_theWeekBeforeLast; NamedTimeSpan m_3WeeksAgo; NamedTimeSpan m_thisMonth; NamedTimeSpan m_thisYear; NamedTimeSpan m_lastMonth; NamedTimeSpan m_theMonthBeforeLast; NamedTimeSpan m_3MonthsAgo; }; class DateChangeWatcher : public QObject { Q_OBJECT public: explicit DateChangeWatcher(QObject *parent = nullptr); Q_SIGNALS: void dateChanged(); private Q_SLOTS: void slotTimeout(); private: QTimer m_timer; QDate m_today; }; #endif Charm-1.12.0/Core/UIStateInterface.h000066400000000000000000000027371331066577000170460ustar00rootroot00000000000000/* ViewModeInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_UISTATEINTERFACE_H #define CHARM_UISTATEINTERFACE_H #include #include class UIStateInterface : public CommandEmitterInterface { public: virtual ~UIStateInterface() { } virtual void saveGuiState() = 0; virtual void restoreGuiState() = 0; virtual void stateChanged(State previous) = 0; virtual void configurationChanged() = 0; virtual void emitCommand(CharmCommand *) = 0; virtual void emitCommandRollback(CharmCommand *) = 0; // CommandEmitterInterface virtual void commitCommand(CharmCommand *) override = 0; }; #endif Charm-1.12.0/Core/User.h000066400000000000000000000030201331066577000146070ustar00rootroot00000000000000/* User.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef USER_H #define USER_H #include class User { public: User() { } User(const QString &name, int id) : m_name(name) , m_id(id) { } bool operator==(const User &other) const { return m_name == other.m_name && m_id == other.m_id; } bool isValid() const { return m_id != 0; } const QString &name() const { return m_name; } void setName(const QString &newname) { m_name = newname; } int id() const { return m_id; } void setId(int newid) { m_id = newid; } private: QString m_name; int m_id = 0; }; #endif Charm-1.12.0/Core/XmlSerialization.cpp000066400000000000000000000153741331066577000175410ustar00rootroot00000000000000/* XmlSerialization.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "XmlSerialization.h" #include "CharmConstants.h" #include "CharmExceptions.h" #include "Configuration.h" #include #include static QHash readMetadata(const QDomElement &metadata) { QHash l; const QDomNodeList cs = metadata.childNodes(); for (int i = 0; i < cs.count(); ++i) { QDomElement e = cs.at(i).toElement(); if (e.isNull()) continue; l.insert(e.tagName(), e.text()); } return l; } namespace XmlSerialization { QString reportTagName() { return QStringLiteral("charmreport"); } QString reportTypeAttribute() { return QStringLiteral("type"); } QDomDocument createXmlTemplate(const QString &docClass) { QDomDocument doc(reportTagName()); // root element: QDomElement root = doc.createElement(reportTagName()); root.setAttribute(reportTypeAttribute(), docClass); doc.appendChild(root); // metadata: { QDomElement metadata = doc.createElement(QStringLiteral("metadata")); root.appendChild(metadata); QDomElement username = doc.createElement(QStringLiteral("username")); metadata.appendChild(username); QDomText text = doc.createTextNode(Configuration::instance().user.name()); username.appendChild(text); QDomElement creationTime = doc.createElement(QStringLiteral("creation-time")); metadata.appendChild(creationTime); QDomText time = doc.createTextNode( QDateTime::currentDateTimeUtc().toString(Qt::ISODate)); creationTime.appendChild(time); // FIXME installation id and stuff are probably necessary } QDomElement report = doc.createElement(QStringLiteral("report")); root.appendChild(report); return doc; } QDomElement reportElement(const QDomDocument &document) { QDomElement root = document.documentElement(); return root.firstChildElement(QStringLiteral("report")); } QDomElement metadataElement(const QDomDocument &document) { QDomElement root = document.documentElement(); return root.firstChildElement(QStringLiteral("metadata")); } QDateTime creationTime(const QDomElement &metaDataElement) { QDomElement creationTimeElement = metaDataElement.firstChildElement(QStringLiteral("creation-time")); if (!creationTimeElement.isNull()) { return QDateTime::fromString(creationTimeElement.text(), Qt::ISODate); } else { return QDateTime(); } } QString userName(const QDomElement &metaDataElement) { QDomElement usernameElement = metaDataElement.firstChildElement(QStringLiteral("username")); return usernameElement.text(); } } QString TaskExport::reportType() { return QStringLiteral("taskdefinitions"); } void TaskExport::writeTo(const QString &filename, const TaskList &tasks) { QDomDocument document = XmlSerialization::createXmlTemplate(reportType()); QDomElement report = XmlSerialization::reportElement(document); // write tasks { QDomElement tasksElement = Task::makeTasksElement(document, tasks); report.appendChild(tasksElement); } // all done, write to file: QFile file(filename); if (file.open(QIODevice::WriteOnly)) { QTextStream stream(&file); document.save(stream, 4); } else { throw XmlSerializationException(QObject::tr("Cannot write to file: %1").arg( file.errorString())); } } void TaskExport::readFrom(const QString &filename) { // load the time sheet: QFile file(filename); if (!file.exists()) throw XmlSerializationException(QObject::tr("File does not exist.")); // load the XML into a DOM tree: if (!file.open(QIODevice::ReadOnly)) throw XmlSerializationException(QObject::tr("Cannot open file for reading: %1").arg(file. errorString())); return readFrom(&file); } void TaskExport::readFrom(QIODevice *device) { QDomDocument document; QString errorMessage; int errorLine = 0; int errorColumn = 0; if (!document.setContent(device, &errorMessage, &errorLine, &errorColumn)) { throw XmlSerializationException(QObject::tr("Invalid XML: [%1:%2] %3").arg(QString::number( errorLine), QString::number( errorColumn), errorMessage)); } // now read and check for the correct report type QDomElement rootElement = document.documentElement(); const QString tagName = rootElement.tagName(); const QString typeAttribute = rootElement.attribute(XmlSerialization::reportTypeAttribute()); if (tagName != XmlSerialization::reportTagName() || typeAttribute != reportType()) throw XmlSerializationException(QObject::tr( "This file is not a Charm task definition file. Please double-check.")); QDomElement metadata = XmlSerialization::metadataElement(document); QDomElement report = XmlSerialization::reportElement(document); m_metadata = readMetadata(metadata); // from metadata, read the export time stamp: m_exportTime = XmlSerialization::creationTime(metadata); // from report, read tasks: QDomElement tasksElement = report.firstChildElement(Task::taskListTagName()); m_tasks = Task::readTasksElement(tasksElement, CHARM_DATABASE_VERSION); } TaskList TaskExport::tasks() const { return m_tasks; } QDateTime TaskExport::exportTime() const { return m_exportTime; } QString TaskExport::metadata(const QString &key) const { return m_metadata.value(key, QString()); } Charm-1.12.0/Core/XmlSerialization.h000066400000000000000000000035201331066577000171740ustar00rootroot00000000000000/* XmlSerialization.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARM_XMLSERIALIZATION_H #define CHARM_XMLSERIALIZATION_H #include #include #include #include "Task.h" namespace XmlSerialization { QDomDocument createXmlTemplate(const QString &docClass); QDomElement reportElement(const QDomDocument &doc); QDomElement metadataElement(const QDomDocument &doc); QDateTime creationTime(const QDomElement &metaDataElement); QString userName(const QDomElement &metaDataElement); } class TaskExport { public: // the only method that deals with writing: static void writeTo(const QString &filename, const TaskList &tasks); void readFrom(const QString &filename); void readFrom(QIODevice *device); TaskList tasks() const; QString metadata(const QString &key) const; static QString reportType(); QDateTime exportTime() const; private: TaskList m_tasks; QHash m_metadata; QDateTime m_exportTime; }; #endif Charm-1.12.0/License.txt000066400000000000000000000432541331066577000147700ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. Charm-1.12.0/ReadMe.md000066400000000000000000000061121331066577000143140ustar00rootroot00000000000000Charm - the Cross-Platform Time Tracker [![Windows Build status](https://ci.appveyor.com/api/projects/status/cxr8oijfuya778c8/branch/master?svg=true)](https://ci.appveyor.com/project/KDAB/charm/branch/master) ====================================== Whether you are a busy professional who needs to keep track time spent on projects or a student who wants to plan their studies: knowing how your time is spent is a good idea. Charm is a program for OS X, Linux and Windows that helps to keep track of time. It is built around two major ideas - tasks and events. Tasks are the things time is spend on, repeatedly. For example, ironing laundry is a task. The laundry done for two hours on last Tuesday is an event in that task. When doing laundry multiple times, the events will be accumulated, and can later be printed in activity reports or weekly time sheets. So in case laundry would be done for three hours on Wednesday again, the activity report for the "Ironing Laundry" task would list the event on tuesday, the event on wednesday and a total of five hours. Tasks ----- By default, the list of tasks known to Charm is empty. They have to be created manually. So the first time work is done on a task, the task entry needs to be created in the task list. Then, by selecting a task and starting it, time will be recorded on that task. A comment can be added that may help to identify later what the individual event was for. Switching from one task to another is a matter of starting the other task. Tasks can have subtasks, and create a hierarchy this way. It is recommended to create rather few top level tasks, since tasks are also used to group in reports. Other tasks can then be created as children or even grandchildren of the top level tasks. Events ------ Every time time is recorded for a task, an event is created. What events exist can be seen in the event editor (View->Event Editor). Also, new events can be created there (without recording them), modified or deleted. Reports ------- ### Activity Reports Activity Reports group all events that happened in a certain time frame, like a day or a week. They are handy to get an overview of what was worked on during that time. ### Time Sheets Time Sheets are created per week, and group event time to tasks and week days. Time Sheets are great to report to the boss, you see. Downloads --------- For released versions, binary packages are available: For various Linux distributions, packages are available via the [openSUSE Build Service](https://build.opensuse.org/package/show/isv:KDAB/charmtimetracker/). For Windows and OS X, [find installers here](https://github.com/KDAB/Charm/releases). Authors and License ------------------- Charm has been developed by Mirko Boehm (mirko@kde.org), as a work of fun and experimentation. The current maintainers are Frank Osterfeld and Guillermo A. Amaral B. Charm is Free Software, developed under the terms of the GPL. While we hope it is of good use, there is no guaranty of function or usefulness of any kind. Feedback is encouraged and always welcome. Feel free to suggest improvements, or point out bugs in the software. Charm-1.12.0/Tests/000077500000000000000000000000001331066577000137375ustar00rootroot00000000000000Charm-1.12.0/Tests/BackendIntegrationTests.cpp000066400000000000000000000131241331066577000212220ustar00rootroot00000000000000/* BackendIntegrationTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "BackendIntegrationTests.h" #include "Core/CharmConstants.h" #include "Core/Controller.h" #include "Core/TaskTreeItem.h" #include "Core/Configuration.h" #include "Core/CharmDataModel.h" #include "Core/SqlStorage.h" #include #include #include #include BackendIntegrationTests::BackendIntegrationTests() : TestApplication(QStringLiteral("./BackendIntegrationTestDatabase.db")) { } void BackendIntegrationTests::initTestCase() { initialize(); } void BackendIntegrationTests::initialValuesTest() { // storage: QVERIFY(controller()->storage()->getAllTasks().isEmpty()); QVERIFY(controller()->storage()->getAllEvents().isEmpty()); QVERIFY(controller()->storage()->getUser(testUserId()).isValid()); // model: QVERIFY(model()->taskTreeItem(0).childCount() == 0); } void BackendIntegrationTests::simpleCreateModifyDeleteTaskTest() { Task task1(1000, QStringLiteral("Task 1")); Task task1b(task1); task1b.setName(QStringLiteral("Task 1, modified")); // add: controller()->addTask(task1); QVERIFY(controller()->storage()->getAllTasks().size() == 1); QVERIFY(controller()->storage()->getAllTasks().first() == task1); QVERIFY(model()->taskTreeItem(task1.id()).task() == task1); // modify: controller()->modifyTask(task1b); QVERIFY(controller()->storage()->getAllTasks().size() == 1); QVERIFY(controller()->storage()->getAllTasks().first() == task1b); QVERIFY(model()->taskTreeItem(task1.id()).task() == task1b); // delete: controller()->deleteTask(task1); QVERIFY(controller()->storage()->getAllTasks().size() == 0); QVERIFY(model()->taskTreeItem(0).childCount() == 0); } void BackendIntegrationTests::biggerCreateModifyDeleteTaskTest() { const TaskList &tasks = referenceTasks(); // make sure everything is cleaned up: QVERIFY(controller()->storage()->getAllTasks().isEmpty()); QVERIFY(model()->taskTreeItem(0).childCount() == 0); // add one task after the other, and compare the lists in storage // and in the model: TaskList currentTasks; for (int i = 0; i < tasks.size(); ++i) { currentTasks.append(tasks[i]); controller()->addTask(tasks[i]); QVERIFY(contentsEqual(controller()->storage()->getAllTasks(), currentTasks)); QVERIFY(contentsEqual(model()->getAllTasks(), currentTasks)); } // modify the tasks: for (int i = 0; i < currentTasks.size(); ++i) { currentTasks[i].setName(currentTasks[i].name() + QStringLiteral(" - modified")); controller()->modifyTask(currentTasks[i]); QVERIFY(contentsEqual(controller()->storage()->getAllTasks(), currentTasks)); QVERIFY(contentsEqual(model()->getAllTasks(), currentTasks)); } // delete the tasks (in reverse, because they depend on each // other): for (int i = currentTasks.size(); i > 0; --i) { controller()->deleteTask(currentTasks[i-1]); currentTasks.removeAt(i-1); QVERIFY(contentsEqual(controller()->storage()->getAllTasks(), currentTasks)); QVERIFY(contentsEqual(model()->getAllTasks(), currentTasks)); } // all gone? QVERIFY(controller()->storage()->getAllTasks().isEmpty()); QVERIFY(model()->taskTreeItem(0).childCount() == 0); } void BackendIntegrationTests::cleanupTestCase() { destroy(); } const TaskList &BackendIntegrationTests::referenceTasks() { static TaskList Tasks; if (Tasks.isEmpty()) { Task task1(1000, QStringLiteral("Task 1")); Task task1_1(1001, QStringLiteral("Task 1-1"), task1.id()); Task task1_2(1002, QStringLiteral("Task 1-2"), task1.id()); Task task1_3(1003, QStringLiteral("Task 1-3"), task1.id()); Task task2(2000, QStringLiteral("Task 2")); Task task2_1(2100, QStringLiteral("Task 2-1"), task2.id()); Task task2_1_1(2110, QStringLiteral("Task 2-1-1"), task2_1.id()); Task task2_1_2(2120, QStringLiteral("Task 2-1-2"), task2_1.id()); Task task2_2(2200, QStringLiteral("Task 2-2"), task2.id()); Task task2_2_1(2210, QStringLiteral("Task 2-2-1"), task2_2.id()); Task task2_2_2(2220, QStringLiteral("Task 2-2-2"), task2_2.id()); Tasks << task1 << task1_1 << task1_2 << task1_3 << task2 << task2_1 << task2_1_1 << task2_1_2 << task2_2 << task2_2_1 << task2_2_2; } return Tasks; } bool BackendIntegrationTests::contentsEqual(const TaskList &listref1, const TaskList &listref2) { TaskList list1(listref1); TaskList list2(listref2); for (int i = 0; i < list1.size(); ++i) { for (int j = 0; j < list2.size(); ++j) { if (list2[j] == list1[i]) list2.removeAt(j); } } return list2.isEmpty(); } QTEST_MAIN(BackendIntegrationTests) Charm-1.12.0/Tests/BackendIntegrationTests.h000066400000000000000000000027731331066577000206770ustar00rootroot00000000000000/* BackendIntegrationTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef BACKENDINTEGRATIONTESTS_H #define BACKENDINTEGRATIONTESTS_H #include "TestApplication.h" #include "Core/Task.h" class BackendIntegrationTests : public TestApplication { Q_OBJECT public: BackendIntegrationTests(); private Q_SLOTS: void initTestCase(); void initialValuesTest(); void simpleCreateModifyDeleteTaskTest(); void biggerCreateModifyDeleteTaskTest(); void cleanupTestCase(); private: const TaskList &referenceTasks(); // returns true if both lists contain the same tasks, even if not // in the same order: bool contentsEqual(const TaskList &list1, const TaskList &list2); }; #endif Charm-1.12.0/Tests/CMakeLists.txt000066400000000000000000000105721331066577000165040ustar00rootroot00000000000000INCLUDE_DIRECTORIES( ${Charm_SOURCE_DIR} ) if(POLICY CMP0020) CMAKE_POLICY(SET CMP0020 NEW) endif() SET( TestApplication_SRCS TestApplication.cpp ) SET( TEST_LIBRARIES CharmCore Qt5::Test Qt5::Network) QT5_ADD_RESOURCES( TestDataResources_SRCS TestData.qrc ) IF( APPLE ) FIND_LIBRARY( SECURITY_FRAMEWORK FRAMEWORK required NAMES Security ) LIST( APPEND TEST_LIBRARIES ${SECURITY_FRAMEWORK} ) ENDIF() SET( SqLiteStorageTests_SRCS SqLiteStorageTests.cpp ) ADD_EXECUTABLE( SqLiteStorageTests ${SqLiteStorageTests_SRCS} ) TARGET_LINK_LIBRARIES( SqLiteStorageTests ${TEST_LIBRARIES} ) ADD_TEST( NAME SqLiteStorageTests COMMAND SqLiteStorageTests ) SET( ControllerTests_SRCS ControllerTests.cpp ) ADD_EXECUTABLE( ControllerTests ${ControllerTests_SRCS} ) TARGET_LINK_LIBRARIES( ControllerTests ${TEST_LIBRARIES} ) ADD_TEST( NAME ControllerTests COMMAND ControllerTests ) SET( EventModelFilterTests_SRCS ${Charm_SOURCE_DIR}/Charm/EventModelAdapter.cpp ${Charm_SOURCE_DIR}/Charm/EventModelFilter.cpp EventModelFilterTests.cpp ) ADD_EXECUTABLE( EventModelFilterTests ${EventModelFilterTests_SRCS} ) TARGET_LINK_LIBRARIES( EventModelFilterTests ${TEST_LIBRARIES} ) SET( DatesTests_SRCS DatesTests.cpp ) ADD_EXECUTABLE( DatesTests ${DatesTests_SRCS} ) TARGET_LINK_LIBRARIES( DatesTests ${TEST_LIBRARIES} ) SET( SmartNameCacheTests_SRCS SmartNameCacheTests.cpp ) ADD_EXECUTABLE( SmartNameCacheTests ${SmartNameCacheTests_SRCS} ) TARGET_LINK_LIBRARIES( SmartNameCacheTests ${TEST_LIBRARIES} ) SET( CharmDataModelTests_SRCS CharmDataModelTests.cpp ) ADD_EXECUTABLE( CharmDataModelTests ${CharmDataModelTests_SRCS} ) TARGET_LINK_LIBRARIES( CharmDataModelTests ${TEST_LIBRARIES} ) ADD_TEST( NAME CharmDataModelTests COMMAND CharmDataModelTests ) SET( BackendIntegrationTests_SRCS BackendIntegrationTests.cpp ${TestApplication_SRCS} ) ADD_EXECUTABLE( BackendIntegrationTests ${BackendIntegrationTests_SRCS} ) TARGET_LINK_LIBRARIES( BackendIntegrationTests ${TEST_LIBRARIES} ) ADD_TEST( NAME BackendIntegrationTests COMMAND BackendIntegrationTests ) SET( TaskStructureTests_SRCS TaskStructureTests.cpp ) ADD_EXECUTABLE( TaskStructureTests ${TaskStructureTests_SRCS} ${TestDataResources_SRCS} ) TARGET_LINK_LIBRARIES( TaskStructureTests ${TEST_LIBRARIES} ) ADD_TEST( NAME TaskStructureTests COMMAND TaskStructureTests ) SET( TimeSpanTests_SRCS TimeSpanTests.cpp ) ADD_EXECUTABLE( TimeSpanTests ${TimeSpanTests_SRCS} ) TARGET_LINK_LIBRARIES( TimeSpanTests ${TEST_LIBRARIES} ) SET( XmlSerializationTests_SRCS XmlSerializationTests.cpp ) ADD_EXECUTABLE( XmlSerializationTests ${XmlSerializationTests_SRCS} ${TestDataResources_SRCS} ) TARGET_LINK_LIBRARIES( XmlSerializationTests ${TEST_LIBRARIES} ) ADD_TEST( NAME XmlSerializationTests COMMAND XmlSerializationTests ) SET( ImportExportTests_SRCS ImportExportTests.cpp ${TestApplication_SRCS} ) ADD_EXECUTABLE( ImportExportTests ${ImportExportTests_SRCS} ${TestDataResources_SRCS} ) TARGET_LINK_LIBRARIES( ImportExportTests ${TEST_LIBRARIES} ) ADD_TEST( NAME ImportExportTests COMMAND ImportExportTests ) SET( SqlTransactionTests_SRCS SqlTransactionTests.cpp ) ADD_EXECUTABLE( SqlTransactionTests ${SqlTransactionTests_SRCS} ) TARGET_LINK_LIBRARIES( SqlTransactionTests ${TEST_LIBRARIES} ) IF( CHARM_DATABASE_CONFIGURATION ) ADD_TEST( NAME SqlTransactionTests COMMAND SqlTransactionTests ) SET_PROPERTY( TEST SqlTransactionTests PROPERTY ENVIRONMENT "CHARM_DATABASE_CONFIGURATION=${CHARM_DATABASE_CONFIGURATION}" ) SET(TimeSheetProcessorTests_SRCS TimeSheetProcessorTests.cpp ${Charm_SOURCE_DIR}/Tools/TimesheetProcessor/Operations.cpp ${Charm_SOURCE_DIR}/Tools/TimesheetProcessor/CommandLine.cpp ${Charm_SOURCE_DIR}/Tools/TimesheetProcessor/Database.cpp ) ADD_EXECUTABLE(TimeSheetProcessorTests ${TimeSheetProcessorTests_SRCS} ${TestDataResources_SRCS} ) TARGET_LINK_LIBRARIES(TimeSheetProcessorTests ${TEST_LIBRARIES} ) ADD_TEST(NAME TimeSheetProcessorTests COMMAND TimeSheetProcessorTests ) ENDIF() SET( UpdateCheckerTests_SRCS ${Charm_SOURCE_DIR}/Charm/HttpClient/CheckForUpdatesJob.cpp UpdateCheckerTests.cpp ) ADD_EXECUTABLE( UpdateCheckerTests ${UpdateCheckerTests_SRCS} ) TARGET_LINK_LIBRARIES( UpdateCheckerTests ${TEST_LIBRARIES} ) Charm-1.12.0/Tests/CharmDataModelTests.cpp000066400000000000000000000167201331066577000203010ustar00rootroot00000000000000/* CharmDataModelTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CharmDataModelTests.h" #include "Core/Task.h" #include "Core/TaskTreeItem.h" #include "Core/CharmDataModel.h" #include #include CharmDataModelTests::CharmDataModelTests() : QObject() { } void CharmDataModelTests::initTestCase() { // set up a model that the other tests can clone to use: m_referenceModel = new CharmDataModel; Task task1(1000, QStringLiteral("Task 1")); Task task1_1(1001, QStringLiteral("Task 1-1"), task1.id()); Task task1_2(1002, QStringLiteral("Task 1-2"), task1.id()); Task task1_3(1003, QStringLiteral("Task 1-3"), task1.id()); Task task2(2000, QStringLiteral("Task 2")); Task task2_1(2100, QStringLiteral("Task 2-1"), task2.id()); Task task2_1_1(2110, QStringLiteral("Task 2-1-1"), task2_1.id()); Task task2_1_2(2120, QStringLiteral("Task 2-1-2"), task2_1.id()); Task task2_2(2200, QStringLiteral("Task 2-2"), task2.id()); Task task2_2_1(2210, QStringLiteral("Task 2-2-1"), task2_2.id()); Task task2_2_2(2220, QStringLiteral("Task 2-2-2"), task2_2.id()); TaskList tasks; tasks << task1 << task1_1 << task1_2 << task1_3 << task2 << task2_1 << task2_1_1 << task2_1_2 << task2_2 << task2_2_1 << task2_2_2; // test setAllTasks, only slightly, more detailed tests follow m_referenceModel->setAllTasks(tasks); QVERIFY(m_referenceModel->taskTreeItem(task2_2.id()).childCount() == 2); QVERIFY(m_referenceModel->taskTreeItem(task2.id()).childCount() == 2); } void CharmDataModelTests::createAndDestroyTest() { CharmDataModel model; } void CharmDataModelTests::addAndRemoveTasksTest() { // set up a structure of tasks: Task task1(1000, QStringLiteral("Task 1")); Task task1_1(1001, QStringLiteral("Task 1-1"), task1.id()); Task task1_2(1002, QStringLiteral("Task 1-2"), task1.id()); Task task1_3(1003, QStringLiteral("Task 1-3"), task1.id()); Task task2(2000, QStringLiteral("Task 2")); Task task2_1(2100, QStringLiteral("Task 2-1"), task2.id()); Task task2_1_1(2110, QStringLiteral("Task 2-1-1"), task2_1.id()); Task task2_1_2(2120, QStringLiteral("Task 2-1-2"), task2_1.id()); Task task2_2(2200, QStringLiteral("Task 2-2"), task2.id()); Task task2_2_1(2210, QStringLiteral("Task 2-2-1"), task2_2.id()); Task task2_2_2(2220, QStringLiteral("Task 2-2-2"), task2_2.id()); // set up a data model, and add all the tasks to it, step by step: CharmDataModel model; QVERIFY(model.taskTreeItem(0).childCount() == 0); model.addTask(task1); QVERIFY(model.taskTreeItem(0).childCount() == 1); QVERIFY(model.taskTreeItem(task1.id()).childCount() == 0); model.addTask(task1_1); QVERIFY(model.taskTreeItem(task1.id()).childCount() == 1); model.addTask(task1_2); QVERIFY(model.taskTreeItem(task1.id()).childCount() == 2); model.addTask(task1_3); QVERIFY(model.taskTreeItem(0).childCount() == 1); QVERIFY(model.taskTreeItem(task1.id()).childCount() == 3); model.addTask(task2); QVERIFY(model.taskTreeItem(0).childCount() == 2); model.addTask(task2_1); QVERIFY(model.taskTreeItem(task2.id()).childCount() == 1); model.addTask(task2_2); QVERIFY(model.taskTreeItem(task2.id()).childCount() == 2); QVERIFY(model.taskTreeItem(task2_1.id()).childCount() == 0); model.addTask(task2_1_1); QVERIFY(model.taskTreeItem(task2_1.id()).childCount() == 1); model.addTask(task2_1_2); QVERIFY(model.taskTreeItem(task2_1.id()).childCount() == 2); QVERIFY(model.taskTreeItem(task2_2.id()).childCount() == 0); model.addTask(task2_2_1); QVERIFY(model.taskTreeItem(task2_2.id()).childCount() == 1); model.addTask(task2_2_2); QVERIFY(model.taskTreeItem(task2_2.id()).childCount() == 2); QVERIFY(model.taskTreeItem(task2.id()).childCount() == 2); // now the whole game in reverse: remove tasks, one by one: model.deleteTask(task2_2_2); QVERIFY(model.taskTreeItem(task2_2.id()).childCount() == 1); QVERIFY(model.taskTreeItem(task2.id()).childCount() == 2); model.deleteTask(task2_2_1); QVERIFY(model.taskTreeItem(task2_2.id()).childCount() == 0); QVERIFY(model.taskTreeItem(task2.id()).childCount() == 2); model.deleteTask(task2_1_1); QVERIFY(model.taskTreeItem(task2_1.id()).childCount() == 1); QVERIFY(model.taskTreeItem(task2.id()).childCount() == 2); model.deleteTask(task2_1_2); QVERIFY(model.taskTreeItem(task2_1.id()).childCount() == 0); QVERIFY(model.taskTreeItem(task2.id()).childCount() == 2); model.deleteTask(task2_2); QVERIFY(model.taskTreeItem(task2.id()).childCount() == 1); model.deleteTask(task2_1); QVERIFY(model.taskTreeItem(task2.id()).childCount() == 0); model.deleteTask(task2); QVERIFY(model.taskTreeItem(0).childCount() == 1); model.deleteTask(task1_2); QVERIFY(model.taskTreeItem(task1.id()).childCount() == 2); model.deleteTask(task1_3); QVERIFY(model.taskTreeItem(task1.id()).childCount() == 1); model.deleteTask(task1_1); QVERIFY(model.taskTreeItem(task1.id()).childCount() == 0); QVERIFY(model.taskTreeItem(0).childCount() == 1); model.deleteTask(task1); QVERIFY(model.taskTreeItem(0).childCount() == 0); } void CharmDataModelTests::modifyTaskTest() { CharmDataModel model; Task task1(1000, QStringLiteral("Task 1")); Task task1_1(1001, QStringLiteral("Task 1-1"), task1.id()); Task task1_2(1002, QStringLiteral("Task 1-2"), task1.id()); Task task1_3(1003, QStringLiteral("Task 1-3"), task1.id()); model.addTask(task1); model.addTask(task1_3); model.addTask(task1_1); model.addTask(task1_2); QVERIFY(model.taskTreeItem(0).childCount() == 1); QVERIFY(model.taskTreeItem(task1.id()).childCount() == 3); // new values: Task task1b(task1); task1b.setName(QStringLiteral("Task 1, modified")); Task task1_1b(task1_1); task1_1b.setParent(0); QVERIFY(model.taskTreeItem(task1.id()).task() == task1); model.modifyTask(task1b); QVERIFY(model.taskTreeItem(task1.id()).task() == task1b); QVERIFY(model.taskTreeItem(task1_1.id()).task() == task1_1); QVERIFY(model.taskTreeItem(0).childCount() == 1); model.modifyTask(task1_1b); // now a top level QVERIFY(model.taskTreeItem(task1_1.id()).task() == task1_1b); QVERIFY(model.taskTreeItem(0).childCount() == 2); model.clearTasks(); QVERIFY(model.taskTreeItem(0).childCount() == 0); } void CharmDataModelTests::cleanupTestCase() { m_referenceModel->clearTasks(); QVERIFY(m_referenceModel->taskTreeItem(0).childCount() == 0); delete m_referenceModel; m_referenceModel = nullptr; } QTEST_MAIN(CharmDataModelTests) Charm-1.12.0/Tests/CharmDataModelTests.h000066400000000000000000000024331331066577000177420ustar00rootroot00000000000000/* CharmDataModelTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CHARMDATAMODELTESTS_H #define CHARMDATAMODELTESTS_H #include class CharmDataModel; class CharmDataModelTests : public QObject { Q_OBJECT public: CharmDataModelTests(); private Q_SLOTS: void initTestCase(); void createAndDestroyTest(); void addAndRemoveTasksTest(); void modifyTaskTest(); void cleanupTestCase(); private: CharmDataModel *m_referenceModel = nullptr; }; #endif Charm-1.12.0/Tests/ControllerTests.cpp000066400000000000000000000236051331066577000176170ustar00rootroot00000000000000/* ControllerTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: Mike McQuaid Author: Guillermo A. Amaral This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ControllerTests.h" #include "Core/SqlStorage.h" #include "Core/CharmConstants.h" #include "Core/Controller.h" #include #include #include #include ControllerTests::ControllerTests() : QObject() , m_configuration(Configuration::instance()) , m_localPath(QStringLiteral("./ControllerTestDatabase.db")) { } void ControllerTests::initTestCase() { QFileInfo file(m_localPath); if (file.exists()) { qDebug() << "test database file exists, deleting"; QDir dir(file.absoluteDir()); QVERIFY(dir.remove(file.fileName())); } m_configuration.installationId = 1; m_configuration.user.setId(1); m_configuration.localStorageType = CHARM_SQLITE_BACKEND_DESCRIPTOR; m_configuration.localStorageDatabase = m_localPath; m_configuration.newDatabase = true; auto controller = new Controller; m_controller = controller; // connect( controller, SIGNAL(currentEvents(EventList)), // SLOT(slotCurrentEvents(EventList)) ); connect(controller, SIGNAL(definedTasks(TaskList)), SLOT(slotDefinedTasks(TaskList))); connect(controller, SIGNAL(taskAdded(Task)), SLOT(slotTaskAdded(Task))); connect(controller, SIGNAL(taskUpdated(Task)), SLOT(slotTaskUpdated(Task))); connect(controller, SIGNAL(taskDeleted(Task)), SLOT(slotTaskDeleted(Task))); } void ControllerTests::initializeConnectBackendTest() { QVERIFY(m_controller->initializeBackEnd(CHARM_SQLITE_BACKEND_DESCRIPTOR)); QVERIFY(m_controller->connectToBackend()); } void ControllerTests:: persistProvideMetaDataTest() { Configuration configs[] = { Configuration(Configuration::TaskPrefilter_ShowAll, Configuration::TimeTrackerFont_Small, Configuration::Minutes, true, Qt::ToolButtonIconOnly, true, true, true, false, 5), Configuration(Configuration::TaskPrefilter_CurrentOnly, Configuration::TimeTrackerFont_Regular, Configuration::Minutes, false, Qt::ToolButtonTextOnly, false, false, false, false, 5), Configuration(Configuration::TaskPrefilter_SubscribedAndCurrentOnly, Configuration::TimeTrackerFont_Large, Configuration::Minutes, true, Qt::ToolButtonTextBesideIcon, true, true, true, false, 5), }; const int NumberOfConfigurations = sizeof configs / sizeof configs[0]; for (int i = 0; i < NumberOfConfigurations; ++i) { m_controller->persistMetaData(configs[i]); m_configuration = Configuration(); m_controller->provideMetaData(m_configuration); m_configuration.dump(); configs[i].dump(); QVERIFY(m_configuration == configs[i]); // and repeat, with some different values } } void ControllerTests::slotCurrentEvents(const EventList &events) { m_currentEvents = events; m_eventListReceived = true; } void ControllerTests::slotDefinedTasks(const TaskList &tasks) { m_definedTasks = tasks; m_taskListReceived = true; } void ControllerTests::slotTaskAdded(const Task &task) { m_definedTasks << task; } void ControllerTests::slotTaskUpdated(const Task &task) { int i; for (i = 0; i < m_definedTasks.size(); ++i) { if (m_definedTasks[i].id() == task.id()) { m_definedTasks[i] = task; break; } } // it is a failure if we receive the signal for nonexisting tasks QVERIFY(i != m_definedTasks.size()); } void ControllerTests::slotTaskDeleted(const Task &task) { // task.dump(); int i; for (i = 0; i < m_definedTasks.size(); ++i) { if (m_definedTasks[i].id() == task.id()) { m_definedTasks.removeAt(i); return; } } // it is a failure if we receive the signal for nonexisting tasks QVERIFY(i != m_definedTasks.size()); // always true, give more // feedback } void ControllerTests::getDefinedTasksTest() { // get the controller to load the initial task list, which // is supposed to be empty m_controller->stateChanged(Connecting, Connected); // QTest::qWait( 100 ); // go back to event loop // verify that this triggers the definedTasks signal, but not the // currentEvents signal // (it is fine if that changes at some point, this tests the // status quo) QVERIFY(m_currentEvents.isEmpty() && m_eventListReceived == false); QVERIFY(m_definedTasks.isEmpty() && m_taskListReceived == true); m_eventListReceived = false; m_taskListReceived = false; } void ControllerTests::addModifyDeleteTaskTest() { // make two tasks, add them, and verify the expected results: const int Task1Id = 1000; const QString Task1Name(QStringLiteral("Task-1-Name")); Task task1; task1.setId(Task1Id); task1.setName(Task1Name); task1.setSubscribed(true); task1.setValidFrom(QDateTime::currentDateTime()); const int Task2Id = 2000; const QString Task2Name(QStringLiteral("Task-2-Name")); Task task2; task2.setId(Task2Id); task2.setName(Task1Name); task2.setParent(task1.id()); task2.setValidUntil(QDateTime::currentDateTime()); m_controller->addTask(task1); // QTest::qWait( 1 ); // only necessary if we do this in threads QVERIFY(m_currentEvents.isEmpty() && m_eventListReceived == false); QVERIFY(m_definedTasks.size() == 1 && m_definedTasks[0] == task1); m_controller->addTask(task2); QVERIFY(m_definedTasks.size() == 2); // both tasks must be in the list, but the order is unspecified: int task1Position, task2Position; if (m_definedTasks[0].id() == task1.id()) { task1Position = 0; task2Position = 1; } else { task1Position = 1; task2Position = 0; } QVERIFY(m_definedTasks[task1Position] == task1); QVERIFY(m_definedTasks[task2Position] == task2); // modify one of the tasks: const QString Task1_1Name(QStringLiteral("Task-1-1-Name")); task1.setName(Task1_1Name); task1.setSubscribed(false); m_controller->modifyTask(task1); QVERIFY(m_definedTasks.size() == 2); QVERIFY(m_definedTasks[task1Position] == task1); QVERIFY(m_definedTasks[task2Position] == task2); const QString Task2_1Name(QStringLiteral("Task-2-1-Name")); task2.setName(Task2_1Name); task2.setSubscribed(true); m_controller->modifyTask(task2); QVERIFY(m_definedTasks[task1Position] == task1); QVERIFY(m_definedTasks[task2Position] == task2); // delete the tasks: m_controller->deleteTask(task1); QVERIFY(m_definedTasks.size() == 1); QVERIFY(m_definedTasks[0] == task2); m_controller->deleteTask(task2); QVERIFY(m_definedTasks.isEmpty()); // leave both tasks in for later tests: m_controller->addTask(task2); QVERIFY(m_definedTasks.size() == 1); m_controller->addTask(task1); QVERIFY(m_definedTasks.size() == 2); } void ControllerTests::toAndFromXmlTest() { // make sure we have some tasks and associated events: TaskList tasks = m_controller->storage()->getAllTasks(); QVERIFY(tasks.size() > 0); // just to be sure nobody fucks it up Event e1 = m_controller->storage()->makeEvent(); e1.setTaskId(tasks[0].id()); e1.setComment(QStringLiteral("Event-1-Comment")); e1.setStartDateTime(); m_controller->modifyEvent(e1); Event e2 = m_controller->storage()->makeEvent(); e2.setTaskId(tasks.last().id()); e2.setComment(QStringLiteral("Event-2-Comment")); e2.setStartDateTime(); m_controller->modifyEvent(e2); Q_ASSERT(m_controller); // just to be sure TaskList tasksBefore = m_controller->storage()->getAllTasks(); EventList eventsBefore = m_controller->storage()->getAllEvents(); QVERIFY(tasksBefore == tasks); QDomDocument document = m_controller->exportDatabasetoXml(); if (!m_controller->importDatabaseFromXml(document).isEmpty()) { QFAIL("Cannot reimport exported Xml Database Dump"); } else { TaskList tasksAfter = m_controller->storage()->getAllTasks(); if (tasksBefore != tasksAfter) { qDebug() << "XML Document created during failed test:" << endl << document.toString(); Q_FOREACH (const Task &task, tasksBefore) task.dump(); Q_FOREACH (const Task &task, tasksAfter) task.dump(); } QVERIFY(tasksBefore == tasksAfter); EventList eventsAfter = m_controller->storage()->getAllEvents(); } QDomDocument document2 = m_controller->exportDatabasetoXml(); } void ControllerTests::disconnectFromBackendTest() { QVERIFY(m_controller->disconnectFromBackend()); } void ControllerTests::cleanupTestCase() { if (QDir::home().exists(m_localPath)) { bool result = QDir::home().remove(m_localPath); QVERIFY(result); } delete m_controller; m_controller = nullptr; } QTEST_MAIN(ControllerTests) Charm-1.12.0/Tests/ControllerTests.h000066400000000000000000000036601331066577000172630ustar00rootroot00000000000000/* ControllerTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CONTROLLERTESTS_H #define CONTROLLERTESTS_H #include #include "Core/Configuration.h" #include "Core/Controller.h" class ControllerTests : public QObject { Q_OBJECT public: ControllerTests(); public Q_SLOTS: // not test cases void slotCurrentEvents(const EventList &); void slotDefinedTasks(const TaskList &); void slotTaskAdded(const Task &); void slotTaskUpdated(const Task &); void slotTaskDeleted(const Task &); private Q_SLOTS: void initTestCase(); void initializeConnectBackendTest(); void persistProvideMetaDataTest(); void getDefinedTasksTest(); void addModifyDeleteTaskTest(); void toAndFromXmlTest(); // this is now done by the model: // void startModifyEndEventTest(); void disconnectFromBackendTest(); void cleanupTestCase(); private: Controller *m_controller = nullptr; Configuration &m_configuration; QString m_localPath; EventList m_currentEvents; bool m_eventListReceived = false; TaskList m_definedTasks; bool m_taskListReceived = false; }; #endif Charm-1.12.0/Tests/Data/000077500000000000000000000000001331066577000146105ustar00rootroot00000000000000Charm-1.12.0/Tests/Data/simple-tasklists.xml000066400000000000000000000023761331066577000206520ustar00rootroot00000000000000 top first-child project project task 1 project task 2 project task 3 top first-child project project task 1 project task 2 project task 3 Charm-1.12.0/Tests/Data/tasklist-merges.xml000066400000000000000000000204631331066577000204550ustar00rootroot00000000000000 top first-child project project task 1 project task 2 project task 3 top first-child project project task 1 project task 2 project task 3 top first-child project project task 1 project task 2 project task 3 top first-child project project task 1 project task 2 project task 3 top project top first-child project top first-child project top project first-child top project top first-child project top project top project first-child top first-child project top first-child project top project top first-child project first-child top project top project top first-child project top project first-child top project top first-child project top first-child project top first-child project top first-child first-child project top still toplevel project top new now first-child second child project top new now first-child second child project Charm-1.12.0/Tests/Data/tasklist-treeness.xml000066400000000000000000000016241331066577000210210ustar00rootroot00000000000000 task 1 task 2 task 3 task 4 task 5 task 3 Charm-1.12.0/Tests/Data/test-database-export.charmdatabaseexport000066400000000000000000022164121331066577000246230ustar00rootroot00000000000000 1 2 3 4 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 30 31 32 33 34 35 36 40 41 42 43 44 45 46 50 51 52 60 61 62 63 64 70 71 72 73 74 75 76 77 78 80 81 82 83 90 91 92 93 94 95 96 101 110 111 120 121 122 123 130 140 141 142 143 145 150 151 152 153 155 160 170 171 172 180 181 190 200 201 202 203 204 205 206 207 209 210 211 212 213 214 220 221 230 240 241 242 243 244 245 246 247 248 250 251 260 261 262 270 280 281 282 283 284 285 286 287 290 295 296 297 300 301 302 303 305 306 307 310 311 312 313 314 320 321 322 323 324 325 326 327 330 331 332 333 335 336 337 350 351 352 353 355 360 370 371 380 381 382 383 390 391 392 395 396 397 398 399 400 401 402 403 404 410 411 412 413 414 420 421 422 423 424 425 426 427 428 429 430 435 436 450 451 452 453 454 460 461 462 463 464 465 466 467 468 469 480 490 491 492 493 494 495 500 510 511 512 513 520 521 522 523 550 551 552 600 610 611 612 613 650 651 652 653 654 655 660 661 662 700 701 702 720 721 722 725 730 740 741 742 743 750 751 752 760 761 762 763 770 771 772 773 774 775 776 777 778 780 781 790 791 792 793 794 795 796 797 798 799 800 801 802 803 820 821 822 823 830 831 832 833 840 841 842 843 844 845 846 850 851 852 853 854 855 860 861 862 863 870 871 872 873 874 880 881 882 883 890 891 892 893 900 901 902 910 911 912 920 921 922 925 930 2000 2001 2010 2011 2012 2013 2020 2021 2022 2023 2030 2031 2032 2033 2040 2041 2042 2043 2050 2051 2052 2053 2060 2061 2062 2063 2070 2071 2072 2073 2080 2081 2082 2083 2090 2091 2092 2093 2100 2101 2102 2103 2105 2106 2107 2108 2110 2111 2112 2113 2115 2116 2117 2118 2120 2121 2122 2123 2130 2131 2132 2133 2135 2136 2137 2138 2140 2141 2142 2143 2145 2146 2147 2148 2150 2151 2152 2153 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2225 2230 2231 2232 2233 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 3000 3001 3002 3003 3911 3920 3925 4010 4011 4021 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4900 4901 4902 4903 4989 5000 5001 5002 6000 6100 7000 7001 7002 7003 7004 7005 7006 7010 7011 7012 7013 7014 7015 7016 7017 7050 7051 7052 7053 7054 7055 7056 7100 7101 7102 7103 7104 7105 7106 7150 7151 7152 7153 7154 7155 7156 7160 7161 7162 7163 7164 7165 7170 7175 7180 7181 7182 7183 7184 7185 7186 7190 7191 7192 7193 7194 7195 7196 7900 7910 7920 8000 8001 8002 8050 8100 8101 8102 8103 8104 8105 8106 8107 8108 8109 8111 8112 8113 8121 8131 8132 8150 8151 8152 8161 8162 8163 8171 8172 8173 8174 8175 8176 8177 8201 8211 8300 8301 8302 8303 8304 8305 8351 8401 8402 8403 8404 8410 8411 8412 8413 8500 8501 8502 8503 8510 8511 8512 8520 8521 8522 8530 8531 8532 8540 8541 8542 8550 8551 8552 8560 8561 8562 8600 8610 8611 8612 8613 8621 8630 8631 8632 8633 8634 8640 8641 8650 8700 8710 8711 8712 8713 8714 8721 8731 8741 8742 8743 8751 8761 8771 8781 8782 8791 8801 8810 8811 8812 8850 8860 8900 8910 8911 8912 8913 8915 8916 8917 8920 8921 8922 8923 9000 9001 9002 9003 9004 9005 9006 9007 9050 9100 9110 9120 9130 9200 9210 9220 9230 9300 9310 9320 9330 9400 9410 9420 9430 9500 9510 9520 9530 9600 9610 9620 9630 9990 9991 9995 9996 9997 9998 9999 10000 11000 11100 11200 11300 12000 12001 12002 13000 13100 13200 13300 13400 14000 15000 15100 15200 3930 7200 7201 7202 7203 7204 7205 7206 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 Charm-1.12.0/Tests/Data/test-tasklistexport.xml000066400000000000000000000013211331066577000214040ustar00rootroot00000000000000 Mirko Boehm 2008-09-20T23:53:12 top first-child project project task 1 project task 2 project task 3 Charm-1.12.0/Tests/Data/test-timesheet-report.charmreport000066400000000000000000000012511331066577000233340ustar00rootroot00000000000000 2015-01-08T11:32:52Z 1.8.0-136-gbe24 2015 1 National holiday Charm-1.12.0/Tests/DatesTests.cpp000066400000000000000000000116141331066577000165310ustar00rootroot00000000000000/* DatesTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld Author: Olivier JG This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "DatesTests.h" #include "Core/Dates.h" #include void DatesTests::testDateByWeekNumberAndWorkDay() { QCOMPARE(Charm::dateByWeekNumberAndWeekDay(2009, 53, Qt::Friday), QDate(2010, 1, 1)); QCOMPARE(Charm::dateByWeekNumberAndWeekDay(2009, 52, Qt::Monday), QDate(2009, 12, 21)); QCOMPARE(Charm::dateByWeekNumberAndWeekDay(2010, 1, Qt::Wednesday), QDate(2010, 1, 6)); QCOMPARE(Charm::dateByWeekNumberAndWeekDay(2010, 30, Qt::Tuesday), QDate(2010, 7, 27)); QCOMPARE(Charm::dateByWeekNumberAndWeekDay(2010, 52, Qt::Saturday), QDate(2011, 1, 1)); QCOMPARE(Charm::dateByWeekNumberAndWeekDay(2011, 1, Qt::Monday), QDate(2011, 1, 3)); QCOMPARE(Charm::dateByWeekNumberAndWeekDay(2012, 52, Qt::Sunday), QDate(2012, 12, 30)); QCOMPARE(Charm::dateByWeekNumberAndWeekDay(2013, 1, Qt::Monday), QDate(2012, 12, 31)); } void DatesTests::testWeekDayInWeekOf() { QCOMPARE(Charm::weekDayInWeekOf(Qt::Monday, QDate(2011, 12, 31)), QDate(2011, 12, 26)); QCOMPARE(Charm::weekDayInWeekOf(Qt::Tuesday, QDate(2011, 12, 31)), QDate(2011, 12, 27)); QCOMPARE(Charm::weekDayInWeekOf(Qt::Wednesday, QDate(2011, 12, 31)), QDate(2011, 12, 28)); QCOMPARE(Charm::weekDayInWeekOf(Qt::Thursday, QDate(2011, 12, 31)), QDate(2011, 12, 29)); QCOMPARE(Charm::weekDayInWeekOf(Qt::Friday, QDate(2011, 12, 31)), QDate(2011, 12, 30)); QCOMPARE(Charm::weekDayInWeekOf(Qt::Saturday, QDate(2011, 12, 31)), QDate(2011, 12, 31)); QCOMPARE(Charm::weekDayInWeekOf(Qt::Sunday, QDate(2011, 12, 31)), QDate(2012, 1, 1)); } void DatesTests::testNumberOfWeeksInYear_data() { QTest::addColumn("year"); QTest::addColumn("numWeeks"); QTest::newRow("Weeks in 2010") << 2010 << 52; QTest::newRow("Weeks in 2011") << 2011 << 52; QTest::newRow("Weeks in 2012") << 2012 << 52; QTest::newRow("Weeks in 2013") << 2013 << 52; QTest::newRow("Weeks in 2014") << 2014 << 52; QTest::newRow("Weeks in 2015") << 2015 << 53; QTest::newRow("Weeks in 2016") << 2016 << 52; QTest::newRow("Weeks in 2017") << 2017 << 52; QTest::newRow("Weeks in 2018") << 2018 << 52; QTest::newRow("Weeks in 2019") << 2019 << 52; QTest::newRow("Weeks in 2020") << 2020 << 53; QTest::newRow("Weeks in 2021") << 2021 << 52; QTest::newRow("Weeks in 2022") << 2022 << 52; QTest::newRow("Weeks in 2023") << 2023 << 52; QTest::newRow("Weeks in 2024") << 2024 << 52; QTest::newRow("Weeks in 2025") << 2025 << 52; QTest::newRow("Weeks in 2026") << 2026 << 53; QTest::newRow("Weeks in 2027") << 2027 << 52; QTest::newRow("Weeks in 2028") << 2028 << 52; QTest::newRow("Weeks in 2029") << 2029 << 52; QTest::newRow("Weeks in 2030") << 2030 << 52; } void DatesTests::testNumberOfWeeksInYear() { QFETCH(int, year); QFETCH(int, numWeeks); QCOMPARE(Charm::numberOfWeeksInYear(year), numWeeks); } void DatesTests::testWeekDifference_data() { QTest::addColumn("from"); QTest::addColumn("to"); QTest::addColumn("weekDiff"); QTest::newRow("2013/12/1 - 2013/12/30") << QDate(2013, 12, 1) << QDate(2013, 12, 30) << 5; QTest::newRow("2013/12/1 - 2013/12/31") << QDate(2013, 12, 1) << QDate(2013, 12, 31) << 5; QTest::newRow("2013/12/1 - 2014/1/3") << QDate(2013, 12, 1) << QDate(2014, 1, 3) << 5; QTest::newRow("2013/12/1 - 2013/1/3") << QDate(2013, 12, 1) << QDate(2013, 1, 3) << -47; QTest::newRow("2013/12/20 - 2013/1/5") << QDate(2013, 12, 20) << QDate(2014, 1, 5) << 2; QTest::newRow("2013/1/1 - 2014/1/1") << QDate(2013, 1, 1) << QDate(2014, 1, 1) << 52; QTest::newRow("2013/12/1 - 2050/4/3") << QDate(2013, 12, 1) << QDate(2050, 4, 3) << 1896; QTest::newRow("1994/12/1 - 2050/2/2") << QDate(1994, 12, 1) << QDate(2050, 2, 2) << 2879; QTest::newRow("2010/2/1 - 2010/3/1") << QDate(2010, 2, 1) << QDate(2010, 3, 1) << 4; } void DatesTests::testWeekDifference() { QFETCH(QDate, from); QFETCH(QDate, to); QFETCH(int, weekDiff); QCOMPARE(Charm::weekDifference(from, to), weekDiff); } QTEST_MAIN(DatesTests) Charm-1.12.0/Tests/DatesTests.h000066400000000000000000000024041331066577000161730ustar00rootroot00000000000000/* DatesTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld Author: Olivier JG This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef DATESTESTS_H #define DATESTESTS_H #include class DatesTests : public QObject { Q_OBJECT private Q_SLOTS: void testDateByWeekNumberAndWorkDay(); void testWeekDayInWeekOf(); void testNumberOfWeeksInYear_data(); void testNumberOfWeeksInYear(); void testWeekDifference_data(); void testWeekDifference(); }; #endif Charm-1.12.0/Tests/EventModelFilterTests.cpp000066400000000000000000000253511331066577000207040ustar00rootroot00000000000000/* EventModelFilterTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Michel Boyer de la Giroday This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "EventModelFilterTests.h" #include "Charm/EventModelFilter.h" #include "Core/CharmDataModel.h" #include "Core/Event.h" #include "Core/Task.h" #include #include EventModelFilterTests::EventModelFilterTests() : QObject() { } void EventModelFilterTests::initTestCase() { // set up a model m_referenceModel = new CharmDataModel; // add tasks Task task1(1000, QStringLiteral("Task 1")); TaskList task; task << task1; m_referenceModel->setAllTasks(task); m_eventModelFilter = new EventModelFilter(m_referenceModel, this); TimeSpans spans; m_thisYearSpan = spans.thisYear(); m_theMonthBeforeLastSpan = spans.theMonthBeforeLast(); m_lastMonthSpan = spans.lastMonth(); m_thisMonthSpan = spans.thisMonth(); m_theWeekBeforeLastSpan = spans.theWeekBeforeLast(); m_lastWeekSpan = spans.lastWeek(); m_thisWeekSpan = spans.thisWeek(); m_dayBeforeYesterdaySpan = spans.dayBeforeYesterday(); m_yesterdaySpan = spans.yesterday(); m_todaySpan = spans.today(); NamedTimeSpan allEvents = { tr("Ever"), TimeSpan(QDate::currentDate().addYears(-200), QDate::currentDate().addYears(+200)), Range }; m_everSpan = allEvents; } void EventModelFilterTests::checkYearsFilter() { Event event1, event2; QDateTime time = QDateTime::currentDateTime(); time.setDate(m_thisYearSpan.timespan.first); // Last year event1.setId(1); event1.setComment(QStringLiteral("event1")); event1.setTaskId(1000); event1.setStartDateTime(time.addYears(-1)); event1.setEndDateTime(time.addYears(-1).addSecs(3600)); // This year event2.setId(2); event2.setComment(QStringLiteral("event2")); event2.setTaskId(1000); event2.setStartDateTime(time); event2.setEndDateTime(time.addSecs(3600)); EventList events; events << event1 << event2; m_referenceModel->setAllEvents(events); // Last year m_eventModelFilter->setFilterStartDate(m_thisYearSpan.timespan.first.addYears(-1)); m_eventModelFilter->setFilterEndDate(m_thisYearSpan.timespan.second.addYears(-1)); QVERIFY(m_eventModelFilter->events().count() == 1); // This year m_eventModelFilter->setFilterStartDate(m_thisYearSpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_thisYearSpan.timespan.second); QVERIFY(m_eventModelFilter->events().count() == 1); m_referenceModel->clearEvents(); } void EventModelFilterTests::checkMonthsFilter() { m_referenceModel->clearEvents(); Event event1, event2, event3; QDateTime time = QDateTime::currentDateTime(); time.setDate(m_theMonthBeforeLastSpan.timespan.first); event1.setId(1); event1.setComment(QStringLiteral("event1")); event1.setTaskId(1000); event1.setStartDateTime(time); event1.setEndDateTime(time.addSecs(3600)); time.setDate(m_lastMonthSpan.timespan.first); event2.setId(2); event2.setComment(QStringLiteral("event2")); event2.setTaskId(1000); event2.setStartDateTime(time); event2.setEndDateTime(time.addSecs(3600)); time.setDate(m_thisMonthSpan.timespan.first); event3.setId(3); event3.setComment(QStringLiteral("event3")); event3.setTaskId(1000); event3.setStartDateTime(time); event3.setEndDateTime(time.addSecs(3600)); EventList events; events << event1 << event2 << event3; m_referenceModel->setAllEvents(events); // The month before last month m_eventModelFilter->setFilterStartDate(m_theMonthBeforeLastSpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_theMonthBeforeLastSpan.timespan.second); m_eventModelFilter->events(); QVERIFY(m_eventModelFilter->events().count() == 1); // Last month m_eventModelFilter->setFilterStartDate(m_lastMonthSpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_lastMonthSpan.timespan.second); m_eventModelFilter->events(); QVERIFY(m_eventModelFilter->events().count() == 1); // This month m_eventModelFilter->setFilterStartDate(m_thisMonthSpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_thisMonthSpan.timespan.second); m_eventModelFilter->events(); QVERIFY(m_eventModelFilter->events().count() == 1); m_referenceModel->clearEvents(); } void EventModelFilterTests::checkWeeksFilter() { Event event1, event2, event3; QDateTime time = QDateTime::currentDateTime(); time.setDate(m_theWeekBeforeLastSpan.timespan.first); event1.setId(1); event1.setComment(QStringLiteral("event1")); event1.setTaskId(1000); event1.setStartDateTime(time); event1.setEndDateTime(time.addSecs(3600)); time.setDate(m_lastWeekSpan.timespan.first); event2.setId(2); event2.setComment(QStringLiteral("event2")); event2.setTaskId(1000); event2.setStartDateTime(time); event2.setEndDateTime(time.addSecs(3600)); time.setDate(m_thisWeekSpan.timespan.first); event3.setId(3); event3.setComment(QStringLiteral("event3")); event3.setTaskId(1000); event3.setStartDateTime(time); event3.setEndDateTime(time.addSecs(3600)); EventList events; events << event1 << event2 << event3; m_referenceModel->setAllEvents(events); // The week before last week m_eventModelFilter->setFilterStartDate(m_theWeekBeforeLastSpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_theWeekBeforeLastSpan.timespan.second); m_eventModelFilter->events(); QVERIFY(m_eventModelFilter->events().count() == 1); // Last week m_eventModelFilter->setFilterStartDate(m_lastWeekSpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_lastWeekSpan.timespan.second); m_eventModelFilter->events(); QVERIFY(m_eventModelFilter->events().count() == 1); // This week m_eventModelFilter->setFilterStartDate(m_thisWeekSpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_thisWeekSpan.timespan.second); m_eventModelFilter->events(); QVERIFY(m_eventModelFilter->events().count() == 1); m_referenceModel->clearEvents(); } void EventModelFilterTests::checkDaysFilter() { Event event1, event2, event3; QDateTime time = QDateTime::currentDateTime(); time.setDate(m_dayBeforeYesterdaySpan.timespan.first); event1.setId(1); event1.setComment(QStringLiteral("event1")); event1.setTaskId(1000); event1.setStartDateTime(time); event1.setEndDateTime(time.addSecs(3600)); time.setDate(m_yesterdaySpan.timespan.first); event2.setId(2); event2.setComment(QStringLiteral("event2")); event2.setTaskId(1000); event2.setStartDateTime(time); event2.setEndDateTime(time.addSecs(3600)); time.setDate(m_todaySpan.timespan.first); event3.setId(3); event3.setComment(QStringLiteral("event3")); event3.setTaskId(1000); event3.setStartDateTime(time); event3.setEndDateTime(time.addSecs(3600)); EventList events; events << event1 << event2 << event3; m_referenceModel->setAllEvents(events); // The day before yesterday m_eventModelFilter->setFilterStartDate(m_dayBeforeYesterdaySpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_dayBeforeYesterdaySpan.timespan.second); QVERIFY(m_eventModelFilter->events().count() == 1); // Yesterday m_eventModelFilter->setFilterStartDate(m_yesterdaySpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_yesterdaySpan.timespan.second); QVERIFY(m_eventModelFilter->events().count() == 1); // Today m_eventModelFilter->setFilterStartDate(m_todaySpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_todaySpan.timespan.second); QVERIFY(m_eventModelFilter->events().count() == 1); // Ever (all events) m_eventModelFilter->setFilterStartDate(m_everSpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_everSpan.timespan.second); m_eventModelFilter->events(); QVERIFY(m_eventModelFilter->events().count() == 3); m_referenceModel->clearEvents(); } void EventModelFilterTests::checkEventSpanOver2Weeks() { Event event1; QDateTime time = QDateTime::currentDateTime(); time.setDate(m_theWeekBeforeLastSpan.timespan.first); event1.setId(1); event1.setComment(QStringLiteral("event1")); event1.setTaskId(1000); event1.setStartDateTime(time); event1.setEndDateTime(time.addDays(8)); EventList events; events << event1; m_referenceModel->setAllEvents(events); // Check the week before last week and last week m_eventModelFilter->setFilterStartDate(m_theWeekBeforeLastSpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_theWeekBeforeLastSpan.timespan.second); m_eventModelFilter->events(); QVERIFY(m_eventModelFilter->events().count() == 1); m_eventModelFilter->setFilterStartDate(m_lastWeekSpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_lastWeekSpan.timespan.second); m_eventModelFilter->events(); QVERIFY(m_eventModelFilter->events().count() == 1); m_referenceModel->clearEvents(); } void EventModelFilterTests::checkEventSpanOver2Days() { Event event1; QDateTime time = QDateTime::currentDateTime(); time.setDate(m_dayBeforeYesterdaySpan.timespan.first); event1.setId(1); event1.setComment(QStringLiteral("event1")); event1.setTaskId(1000); event1.setStartDateTime(time); event1.setEndDateTime(time.addDays(1)); EventList events; events << event1; m_referenceModel->setAllEvents(events); // check the day before yesterday and yesterday m_eventModelFilter->setFilterStartDate(m_dayBeforeYesterdaySpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_dayBeforeYesterdaySpan.timespan.second); QVERIFY(m_eventModelFilter->events().count() == 1); m_eventModelFilter->setFilterStartDate(m_yesterdaySpan.timespan.first); m_eventModelFilter->setFilterEndDate(m_yesterdaySpan.timespan.second); QVERIFY(m_eventModelFilter->events().count() == 1); m_referenceModel->clearEvents(); } QTEST_MAIN(EventModelFilterTests) Charm-1.12.0/Tests/EventModelFilterTests.h000066400000000000000000000035761331066577000203560ustar00rootroot00000000000000/* EventModelFilterTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Michel Boyer de la Giroday This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef EVENTMODELFILTERTESTS #define EVENTMODELFILTERTESTS #include #include "Core/TimeSpans.h" class CharmDataModel; class EventModelFilter; class EventModelFilterTests : public QObject { Q_OBJECT public: EventModelFilterTests(); private Q_SLOTS: void initTestCase(); void checkYearsFilter(); void checkMonthsFilter(); void checkWeeksFilter(); void checkDaysFilter(); void checkEventSpanOver2Weeks(); void checkEventSpanOver2Days(); private: CharmDataModel *m_referenceModel = nullptr; EventModelFilter *m_eventModelFilter; NamedTimeSpan m_thisYearSpan; NamedTimeSpan m_theMonthBeforeLastSpan; NamedTimeSpan m_lastMonthSpan; NamedTimeSpan m_thisMonthSpan; NamedTimeSpan m_theWeekBeforeLastSpan; NamedTimeSpan m_lastWeekSpan; NamedTimeSpan m_thisWeekSpan; NamedTimeSpan m_dayBeforeYesterdaySpan; NamedTimeSpan m_yesterdaySpan; NamedTimeSpan m_todaySpan; NamedTimeSpan m_everSpan; }; #endif // EVENTMODELFILTERTESTS Charm-1.12.0/Tests/ImportExportTests.cpp000066400000000000000000000100471331066577000201440ustar00rootroot00000000000000/* ImportExportTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ImportExportTests.h" #include "Core/Controller.h" #include "Core/Task.h" #include "Core/CharmDataModel.h" #include "Charm/Commands/CommandImportFromXml.h" #include #include #include #include #include ImportExportTests::ImportExportTests() : TestApplication(QStringLiteral("./ImportExportTestDatabase.db")) { } void ImportExportTests::initTestCase() { initialize(); } void ImportExportTests::importExportTest() { const QString localFileName(QStringLiteral("ImportExportTests-temp.charmdatabaseexport")); const QString filename = QStringLiteral( ":/importExportTest/Data/test-database-export.charmdatabaseexport"); importDatabase(filename); QSharedPointer databaseStep1(model()->clone()); QDomDocument exportDoc = controller()->exportDatabasetoXml(); QFile outfile(localFileName); QVERIFY(outfile.open(QIODevice::ReadWrite)); QTextStream stream(&outfile); exportDoc.save(stream, 4); // FIXME save does no kind of error reporting? importDatabase(filename); QCOMPARE(*databaseStep1.data(), *model()); // Code to load and anonymize a database: // QBENCHMARK { // Q_FOREACH( Task task, model()->getAllTasks() ) { // task.setName( QString::number( task.id() ) ); // QVERIFY( controller()->modifyTask( task ) ); // } // EventMap eventMap = model()->eventMap(); // for( EventMap::const_iterator it = eventMap.begin(); it != eventMap.end(); ++it ) { // Event event = (*it).second; // event.setComment( QString::number( event.id() )); // QVERIFY( controller()->modifyEvent( event ) ); // } // QDomDocument exportDoc = controller()->exportDatabasetoXml(); // QFile outfile( "test-database-export.charmdatabaseexport" ); // QVERIFY( outfile.open( QIODevice::ReadWrite ) ); // QTextStream stream( &outfile ); // exportDoc.save( stream, 4 ); // qDebug() << outfile.fileName(); // } } void ImportExportTests::importBenchmark() { const QString filename = QStringLiteral( ":/importExportTest/Data/test-database-export.charmdatabaseexport"); QBENCHMARK { importDatabase(filename); } } void ImportExportTests::exportBenchmark() { const QString filename = QStringLiteral( ":/importExportTest/Data/test-database-export.charmdatabaseexport"); const QString localFileName(QStringLiteral("ImportExportTests-temp.charmdatabaseexport")); importDatabase(filename); QBENCHMARK { QDomDocument exportDoc = controller()->exportDatabasetoXml(); QFile outfile(localFileName); QVERIFY(outfile.open(QIODevice::ReadWrite)); QTextStream stream(&outfile); exportDoc.save(stream, 4); // FIXME save does no kind of error reporting? } } void ImportExportTests::cleanupTestCase() { destroy(); } void ImportExportTests::importDatabase(const QString &filename) { QFile file(filename); QVERIFY(file.open(QIODevice::ReadOnly)); QDomDocument dom; QVERIFY(dom.setContent(&file)); QVERIFY(controller()->importDatabaseFromXml(dom).isEmpty()); } QTEST_MAIN(ImportExportTests) Charm-1.12.0/Tests/ImportExportTests.h000066400000000000000000000024051331066577000176100ustar00rootroot00000000000000/* ImportExportTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef IMPORTEXPORTTESTS_H #define IMPORTEXPORTTESTS_H #include "TestApplication.h" class ImportExportTests : public TestApplication { Q_OBJECT public: ImportExportTests(); private Q_SLOTS: void initTestCase(); void importExportTest(); void importBenchmark(); void exportBenchmark(); void cleanupTestCase(); private: void importDatabase(const QString &filename); }; #endif Charm-1.12.0/Tests/SmartNameCacheTests.cpp000066400000000000000000000042001331066577000202750ustar00rootroot00000000000000/* SmartNameCacheTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "SmartNameCacheTests.h" #include "Core/SmartNameCache.h" #include void SmartNameCacheTests::testCache() { SmartNameCache cache; Task projects(1, QStringLiteral("Projects")); Task charm(2, QStringLiteral("Charm")); charm.setParent(projects.id()); Task charmDevelopment(3, QStringLiteral("Development")); charmDevelopment.setParent(charm.id()); Task charmOverhead(4, QStringLiteral("Overhead")); charmOverhead.setParent(charm.id()); Task lotsofcake(5, QStringLiteral("Lotsofcake")); lotsofcake.setParent(projects.id()); Task lotsofcakeDevelopment(6, QStringLiteral("Development")); lotsofcakeDevelopment.setParent(lotsofcake.id()); const TaskList tasks = TaskList() << projects << charm << charmDevelopment << charmOverhead << lotsofcake << lotsofcakeDevelopment; cache.setAllTasks(tasks); QCOMPARE(cache.smartName(charmDevelopment.id()), QLatin1String("Charm/Development")); QCOMPARE(cache.smartName(charmOverhead.id()), QLatin1String("Charm/Overhead")); QCOMPARE(cache.smartName(projects.id()), QLatin1String("Projects")); QCOMPARE(cache.smartName(lotsofcakeDevelopment.id()), QLatin1String("Lotsofcake/Development")); } QTEST_MAIN(SmartNameCacheTests) Charm-1.12.0/Tests/SmartNameCacheTests.h000066400000000000000000000020611331066577000177450ustar00rootroot00000000000000/* SmartNameCacheTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef SMARTNAMECACHETESTS_H #define SMARTNAMECACHETESTS_H #include class SmartNameCacheTests : public QObject { Q_OBJECT private Q_SLOTS: void testCache(); }; #endif Charm-1.12.0/Tests/SqLiteStorageTests.cpp000066400000000000000000000246231331066577000202230ustar00rootroot00000000000000/* SqLiteStorageTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "SqLiteStorageTests.h" #include "Core/User.h" #include "Core/CharmConstants.h" #include "Core/SqLiteStorage.h" #include #include #include #include SqLiteStorageTests::SqLiteStorageTests() : QObject() , m_storage(new SqLiteStorage) , m_localPath(QStringLiteral("./SqLiteStorageTestDatabase.db")) { } SqLiteStorageTests::~SqLiteStorageTests() { delete m_storage; } void SqLiteStorageTests::initTestCase() { QFileInfo file(m_localPath); if (file.exists()) { qDebug() << "test database file exists, deleting"; QDir dir(file.absoluteDir()); QVERIFY(dir.remove(file.fileName())); } m_configuration.installationId = 1; m_configuration.user.setId(1); m_configuration.localStorageType = CHARM_SQLITE_BACKEND_DESCRIPTOR; m_configuration.localStorageDatabase = m_localPath; m_configuration.newDatabase = true; } void SqLiteStorageTests::connectAndCreateDatabaseTest() { bool result = m_storage->connect(m_configuration); QVERIFY(result); } void SqLiteStorageTests::makeModifyDeleteUserTest() { // make two user accounts QString name1 = QStringLiteral("Test-User-1"); User user1 = m_storage->makeUser(name1); QVERIFY(user1.name() == name1); QString name2 = QStringLiteral("Test-User-2"); User user2 = m_storage->makeUser(name2); QVERIFY(user2.name() == name2); // modify the user QString newName = QStringLiteral("User-Test-1"); user1.setName(newName); QVERIFY(m_storage->modifyUser(user1)); // verify user database entry User user1_1 = m_storage->getUser(user1.id()); QVERIFY(user1_1.name() == newName); QVERIFY(user1.id() == user1_1.id()); // delete the user QVERIFY(m_storage->deleteUser(user1_1)); // same id as user1 // verify user2 is unchanged User user2_1 = m_storage->getUser(user2.id()); QVERIFY(user2_1.id() == user2.id() && user2_1.name() == user2.name()); // verify the user account is gone: QVERIFY(!m_storage->getUser(user1.id()).isValid()); // verify user 2 is still there: QVERIFY(m_storage->getUser(user2.id()).isValid()); } void SqLiteStorageTests::makeModifyDeleteTasksTest() { // make two tasks const int Task1Id = 1; const QString Task1Name(QStringLiteral("Task-1-Name")); const int Task2Id = 2; const QString Task2Name(QStringLiteral("Task-2-Name")); Task task1; task1.setId(Task1Id); task1.setName(Task1Name); task1.setValidFrom(QDateTime::currentDateTime()); Task task2; task2.setId(Task2Id); task2.setName(Task2Name); task2.setValidUntil(QDateTime::currentDateTime()); QVERIFY(m_storage->getAllTasks().size() == 0); QVERIFY(m_storage->addTask(task1)); QVERIFY(m_storage->addTask(task2)); QVERIFY(m_storage->getAllTasks().size() == 2); // verify task database entries Task task1_1 = m_storage->getTask(task1.id()); Task task2_1 = m_storage->getTask(task2.id()); if (task1 != task1_1) { task1.dump(); task1_1.dump(); } QVERIFY(task1 == task1_1); QVERIFY(task2 == task2_1); // modify the tasks const QString Task1NewName(QStringLiteral("Name-1-Task")); task1.setName(Task1NewName); task1.setParent(task2.id()); QVERIFY(m_storage->modifyTask(task1)); task1_1 = m_storage->getTask(task1.id()); QVERIFY(task1_1 == task1); QVERIFY(m_storage->getAllTasks().size() == 2); // delete the task QVERIFY(m_storage->deleteTask(task2)); QVERIFY(m_storage->getAllTasks().size() == 1); // verify the task is gone: QVERIFY(!m_storage->getTask(task2.id()).isValid()); // verify the other task is still there: QVERIFY(m_storage->getTask(task1.id()).isValid()); // put the second task back in for later tests: QVERIFY(m_storage->addTask(task2)); } void SqLiteStorageTests::makeModifyDeleteEventsTest() { // make a user User user = m_storage->makeUser(tr("MakeEventTestUser")); // make two events Task task = m_storage->getTask(1); // WARNING: depends on leftover task created in previous test QVERIFY(task.isValid()); Event event1 = m_storage->makeEvent(); QVERIFY(event1.isValid()); event1.setTaskId(task.id()); event1.setUserId(user.id()); event1.setReportId(42); const QString Event1Comment(QStringLiteral("Event-1-Comment")); event1.setComment(Event1Comment); Event event2 = m_storage->makeEvent(); QVERIFY(event2.isValid()); event2.setTaskId(task.id()); event2.setUserId(user.id()); const QString Event2Comment(QStringLiteral("Event-2-Comment")); event2.setComment(Event2Comment); QVERIFY(event1.id() != event2.id()); // modify the events QVERIFY(m_storage->modifyEvent(event1)); // store new name QVERIFY(m_storage->modifyEvent(event2)); // -"- // verify event database entries Event event1_1 = m_storage->getEvent(event1.id()); QCOMPARE(event1_1.comment(), event1.comment()); Event event2_1 = m_storage->getEvent(event2.id()); QCOMPARE(event2_1.comment(), event2.comment()); // delete one event QVERIFY(m_storage->deleteEvent(event1)); // verify the event is gone: QVERIFY(!m_storage->getEvent(event1.id()).isValid()); // verify the other event is still there: QVERIFY(m_storage->getEvent(event2.id()).isValid()); } void SqLiteStorageTests::addDeleteSubscriptionsTest() { // this is a new database, so there should be no subscriptions // Task 1 is expected to be there (from the earlier test) TaskList tasks = m_storage->getAllTasks(); QVERIFY(tasks.size() == 2); // make sure the other test leaves the two tasks in QVERIFY(tasks[0].subscribed() == false); QVERIFY(tasks[1].subscribed() == false); // add a subscription QVERIFY(m_storage->addSubscription(m_configuration.user, tasks[0])); Task task0 = m_storage->getTask(tasks[0].id()); // is the task subscribed now? QVERIFY(task0.subscribed()); Task task1 = m_storage->getTask(tasks[1].id()); // is the other one still not subscribed? QVERIFY(!task1.subscribed()); // delete the subscription QVERIFY(m_storage->deleteSubscription(m_configuration.user, tasks[0])); // is it now unsubscribed? task0 = m_storage->getTask(tasks[0].id()); QVERIFY(!task0.subscribed()); // is the other task still unchanged? task1 = m_storage->getTask(tasks[1].id()); QVERIFY(!task1.subscribed()); } void SqLiteStorageTests::deleteTaskWithEventsTest() { // make a task const int TaskId = 1; const QString Task1Name(QStringLiteral("Task-Name")); Task task; task.setId(TaskId); task.setName(Task1Name); task.setValidFrom(QDateTime::currentDateTime()); QVERIFY(m_storage->deleteAllTasks()); QVERIFY(m_storage->deleteAllEvents()); QVERIFY(m_storage->getAllTasks().size() == 0); QVERIFY(m_storage->addTask(task)); QVERIFY(m_storage->getAllTasks().size() == 1); Task task2; task2.setId(2); task2.setName(QStringLiteral("Task-2-Name")); QVERIFY(m_storage->addTask(task2)); QVERIFY(m_storage->getAllTasks().size() == 2); // create 3 events, 2 for task 1, and one for another one { Event event = m_storage->makeEvent(); QVERIFY(event.isValid()); event.setTaskId(task.id()); event.setUserId(1); event.setReportId(42); const QString EventComment(QStringLiteral("Event-Comment")); event.setComment(EventComment); QVERIFY(m_storage->modifyEvent(event)); } { Event event = m_storage->makeEvent(); QVERIFY(event.isValid()); event.setTaskId(task.id()); event.setUserId(1); event.setReportId(43); const QString EventComment(QStringLiteral("Event-Comment 2")); event.setComment(EventComment); QVERIFY(m_storage->modifyEvent(event)); } // this is the event that is supposed to remain in the DB: Event event = m_storage->makeEvent(); QVERIFY(event.isValid()); event.setTaskId(task2.id()); event.setUserId(1); event.setReportId(43); const QString EventComment(QStringLiteral("Event-Comment 2")); event.setComment(EventComment); QVERIFY(m_storage->modifyEvent(event)); // verify task database entries QVERIFY(m_storage->deleteTask(task)); EventList events = m_storage->getAllEvents(); QVERIFY(events.count() == 1); QVERIFY(events.first() == event); } void SqLiteStorageTests::setGetMetaDataTest() { const QString Key1(QStringLiteral("Key1")); const QString Key2(QStringLiteral("Key2")); const QString Value1(QStringLiteral("Value1")); const QString Value2(QStringLiteral("Value2")); const QString Value1_1(QStringLiteral("Value1_1")); // check that all the keys are not there: QVERIFY(m_storage->getMetaData(Key1).isEmpty()); QVERIFY(m_storage->getMetaData(Key2).isEmpty()); // check that inserted keys are there: QVERIFY(m_storage->setMetaData(Key1, Value1)); QVERIFY(m_storage->getMetaData(Key1) == Value1); // check that only the inserted keys are there: QVERIFY(m_storage->getMetaData(Key2).isEmpty()); QVERIFY(m_storage->setMetaData(Key2, Value2)); QVERIFY(m_storage->getMetaData(Key2) == Value2); // modify value, check results: QVERIFY(m_storage->setMetaData(Key1, Value1_1)); QVERIFY(m_storage->getMetaData(Key1) == Value1_1); QVERIFY(m_storage->getMetaData(Key2) == Value2); } void SqLiteStorageTests::cleanupTestCase() { m_storage->disconnect(); if (QDir::home().exists(m_localPath)) { bool result = QDir::home().remove(m_localPath); QVERIFY(result); } } QTEST_MAIN(SqLiteStorageTests) Charm-1.12.0/Tests/SqLiteStorageTests.h000066400000000000000000000030651331066577000176650ustar00rootroot00000000000000/* SqLiteStorageTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef SQLITESTORAGETEST_H #define SQLITESTORAGETEST_H #include #include "Core/Configuration.h" #include "Core/SqlStorage.h" class SqLiteStorageTests : public QObject { Q_OBJECT public: SqLiteStorageTests(); ~SqLiteStorageTests() override; private: SqlStorage *m_storage; Configuration m_configuration; QString m_localPath; private Q_SLOTS: void initTestCase(); void connectAndCreateDatabaseTest(); void makeModifyDeleteUserTest(); void makeModifyDeleteTasksTest(); void makeModifyDeleteEventsTest(); void addDeleteSubscriptionsTest(); void setGetMetaDataTest(); void deleteTaskWithEventsTest(); void cleanupTestCase(); }; #endif Charm-1.12.0/Tests/SqlTransactionTests.cpp000066400000000000000000000120451331066577000204350ustar00rootroot00000000000000/* SqlTransactionTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "SqlTransactionTests.h" #include "Core/SqlRaiiTransactor.h" #include #include #include #include #include SqlTransactionTests::SqlTransactionTests() : QObject() { } void SqlTransactionTests::testMySqlDriverRequirements() { const auto DriverName = QStringLiteral("QMYSQL"); QVERIFY(QSqlDatabase::isDriverAvailable(DriverName)); QSqlDatabase db = QSqlDatabase::addDatabase(DriverName, QStringLiteral("test-mysql.charm.kdab.com")); } void SqlTransactionTests::testSqLiteDriverRequirements() { const auto DriverName = QStringLiteral("QSQLITE"); QVERIFY(QSqlDatabase::isDriverAvailable(DriverName)); QSqlDatabase db = QSqlDatabase::addDatabase(DriverName, QStringLiteral("test-sqlite.charm.kdab.com")); QSqlDriver *driver = db.driver(); QVERIFY(driver->hasFeature(QSqlDriver::Transactions)); } MySqlStorage SqlTransactionTests::prepareMySqlStorage() { MySqlStorage storage; MySqlStorage::Parameters parameters = MySqlStorage::parseParameterEnvironmentVariable(); storage.configure(parameters); return storage; } #if 0 // old broken tests, commented so far to let us test some other part of the TimeSheetProcessor void SqlTransactionTests::testMySqlTransactionRollback() { MySqlStorage storage = prepareMySqlStorage(); QVERIFY(storage.database().open()); QSqlDriver *driver = storage.database().driver(); QVERIFY(driver->hasFeature(QSqlDriver::Transactions)); QList tasksBefore = storage.getAllTasks(); QVERIFY(!tasksBefore.isEmpty()); Task first = tasksBefore.first(); // test a simple transaction that is completed and committed: { SqlRaiiTransactor transactor(storage.database()); QSqlQuery query(storage.database()); query.prepare("DELETE from Tasks where id=:id"); query.bindValue("id", first.id()); QVERIFY(storage.runQuery(query)); } // this transaction was NOT committed QList tasksAfter = storage.getAllTasks(); QVERIFY(!tasksAfter.isEmpty()); QVERIFY(tasksBefore == tasksAfter); } void SqlTransactionTests::testMySqlTransactionCommit() { MySqlStorage storage = prepareMySqlStorage(); QVERIFY(storage.database().open()); QList tasksBefore = storage.getAllTasks(); QVERIFY(!tasksBefore.isEmpty()); Task first = tasksBefore.takeFirst(); // test a simple transaction that is completed and committed: { SqlRaiiTransactor transactor(storage.database()); QSqlQuery query(storage.database()); query.prepare("DELETE from Tasks where id=:id"); query.bindValue("id", first.id()); QVERIFY(storage.runQuery(query)); transactor.commit(); } // this transaction WAS committed QList tasksAfter = storage.getAllTasks(); QVERIFY(!tasksAfter.isEmpty()); QVERIFY(tasksBefore == tasksAfter); } // this test more or less documents the behaviour of the mysql driver which allows nested transactions, which fail, without // reporting an error void SqlTransactionTests::testMySqlNestedTransactions() { MySqlStorage storage = prepareMySqlStorage(); QVERIFY(storage.database().open()); QList tasksBefore = storage.getAllTasks(); QVERIFY(!tasksBefore.isEmpty()); Task first = tasksBefore.takeFirst(); // test a simple transaction that is completed and committed: { SqlRaiiTransactor transactor(storage.database()); QSqlQuery query(storage.database()); query.prepare("DELETE from Tasks where id=:id"); query.bindValue("id", first.id()); QVERIFY(storage.runQuery(query)); { // now before the first transaction is committed and done, a second one is started // this should throw an exception SqlRaiiTransactor transactor2(storage.database()); QSqlError error = storage.database().lastError(); // QFAIL( "I should not get here." ); transactor2.commit(); } QSqlError error1 = storage.database().lastError(); transactor.commit(); QSqlError error2 = storage.database().lastError(); QFAIL("I should not get here."); } } #endif QTEST_MAIN(SqlTransactionTests) Charm-1.12.0/Tests/SqlTransactionTests.h000066400000000000000000000025701331066577000201040ustar00rootroot00000000000000/* SqlTransactionTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef SQLTRANSACTIONTESTS_H #define SQLTRANSACTIONTESTS_H #include #include "Core/MySqlStorage.h" class SqlTransactionTests : public QObject { Q_OBJECT public: SqlTransactionTests(); private Q_SLOTS: void testMySqlDriverRequirements(); void testSqLiteDriverRequirements(); #if 0 void testMySqlTransactionRollback(); void testMySqlTransactionCommit(); void testMySqlNestedTransactions(); #endif private: MySqlStorage prepareMySqlStorage(); }; #endif // SQLTRANSACTIONTESTS_H Charm-1.12.0/Tests/TaskStructureTests.cpp000066400000000000000000000132601331066577000203130ustar00rootroot00000000000000/* TaskStructureTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TaskStructureTests.h" #include "TestHelpers.h" #include "Core/Task.h" #include "Core/TaskListMerger.h" #include "Core/CharmConstants.h" #include #include TaskStructureTests::TaskStructureTests() : QObject() { } void TaskStructureTests::checkForUniqueTaskIdsTest_data() { QTest::addColumn("tasks"); QTest::addColumn("unique"); Q_FOREACH (const QDomElement &testcase, TestHelpers::retrieveTestCases(QLatin1String(":/checkForUniqueTaskIdsTest/Data"), QLatin1String("checkForUniqueTaskIdsTest"))) { QString name = testcase.attribute(QStringLiteral("name")); bool expectedResult = TestHelpers::attribute(QStringLiteral("expectedResult"), testcase); QDomElement element = testcase.firstChildElement(Task::taskListTagName()); QVERIFY(!element.isNull()); TaskList tasks = Task::readTasksElement(element, CHARM_DATABASE_VERSION); QTest::newRow(name.toLocal8Bit().constData()) << tasks << expectedResult; QVERIFY(element.nextSiblingElement(Task::taskListTagName()).isNull()); qDebug() << "Added test case" << name; } } void TaskStructureTests::checkForUniqueTaskIdsTest() { QFETCH(TaskList, tasks); QFETCH(bool, unique); QCOMPARE(Task::checkForUniqueTaskIds(tasks), unique); } void TaskStructureTests::checkForTreenessTest_data() { QTest::addColumn("tasks"); QTest::addColumn("directed"); Q_FOREACH (const QDomElement &testcase, TestHelpers::retrieveTestCases(QLatin1String(":/checkForTreenessTest/Data"), QLatin1String("checkForTreenessTest"))) { QString name = testcase.attribute(QStringLiteral("name")); bool expectedResult = TestHelpers::attribute(QStringLiteral("expectedResult"), testcase); QDomElement element = testcase.firstChildElement(Task::taskListTagName()); QVERIFY(!element.isNull()); TaskList tasks = Task::readTasksElement(element, CHARM_DATABASE_VERSION); QTest::newRow(name.toLocal8Bit().constData()) << tasks << expectedResult; QVERIFY(element.nextSiblingElement(Task::taskListTagName()).isNull()); qDebug() << "Added test case" << name; } } void TaskStructureTests::checkForTreenessTest() { QFETCH(TaskList, tasks); QFETCH(bool, directed); QCOMPARE(Task::checkForTreeness(tasks), directed); } void TaskStructureTests::mergeTaskListsTest_data() { QTest::addColumn("old"); QTest::addColumn("newTasks"); QTest::addColumn("merged"); Q_FOREACH (const QDomElement &testcase, TestHelpers::retrieveTestCases(QLatin1String(":/mergeTaskListsTest/Data"), QLatin1String("mergeTaskListsTest"))) { QString name = testcase.attribute(QStringLiteral("name")); QList elements; elements << testcase.firstChildElement(Task::taskListTagName()); elements << (elements.first()).nextSiblingElement(Task::taskListTagName()); elements << (elements.at(1)).nextSiblingElement(Task::taskListTagName()); bool oldFound = false, newFound = false, mergedFound = false; TaskList old, newTasks, merged; Q_FOREACH (const QDomElement &element, elements) { QString arg = element.attribute(QStringLiteral("arg")); TaskList tasks = Task::readTasksElement(element, CHARM_DATABASE_VERSION); if (arg == QLatin1String("old")) { old = tasks; oldFound = true; } else if (arg == QLatin1String("new")) { newTasks = tasks; newFound = true; } else if (arg == QLatin1String("merged")) { merged = tasks; qSort(merged.begin(), merged.end(), Task::lowerTaskId); mergedFound = true; } else { QFAIL("invalid XML structure in input data"); } } QVERIFY(oldFound); QVERIFY(newFound); QVERIFY(mergedFound); QTest::newRow(name.toLocal8Bit().constData()) << old << newTasks << merged; qDebug() << "Added test case" << name; } } void TaskStructureTests::mergeTaskListsTest() { QFETCH(TaskList, old); QFETCH(TaskList, newTasks); QFETCH(TaskList, merged); TaskListMerger merger; merger.setOldTasks(old); merger.setNewTasks(newTasks); TaskList result = merger.mergedTaskList(); qSort(result.begin(), result.end(), Task::lowerTaskId); if (result != merged) { qDebug() << "Test failed"; qDebug() << "Merge Result:"; dumpTaskList(result); qDebug() << "Expected Merge Result:"; dumpTaskList(merged); } QCOMPARE(result, merged); } QTEST_MAIN(TaskStructureTests) Charm-1.12.0/Tests/TaskStructureTests.h000066400000000000000000000024701331066577000177610ustar00rootroot00000000000000/* TaskStructureTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TASKSTRUCTURETESTS_H #define TASKSTRUCTURETESTS_H #include #include #include class TaskStructureTests : public QObject { Q_OBJECT public: TaskStructureTests(); private Q_SLOTS: void checkForUniqueTaskIdsTest_data(); void checkForUniqueTaskIdsTest(); void checkForTreenessTest_data(); void checkForTreenessTest(); void mergeTaskListsTest_data(); void mergeTaskListsTest(); }; #endif Charm-1.12.0/Tests/TestApplication.cpp000066400000000000000000000066341331066577000175570ustar00rootroot00000000000000/* TestApplication.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TestApplication.h" #include "Core/Configuration.h" #include "Core/CharmConstants.h" #include "Core/Controller.h" #include "Core/CharmDataModel.h" #include #include #include #include #include const int UserId = 1; const int InstallationId = 1; TestApplication::TestApplication(const QString &databasePath, QObject *parent) : QObject(parent) , m_configuration(&Configuration::instance()) , m_localPath(databasePath) { } Controller *TestApplication::controller() const { return m_controller; } CharmDataModel *TestApplication::model() const { return m_model; } Configuration *TestApplication::configuration() const { return m_configuration; } QString TestApplication::databasePath() const { return m_localPath; } void TestApplication::initialize() { QFileInfo file(m_localPath); if (file.exists()) { qDebug() << "test database file exists, deleting"; QDir dir(file.absoluteDir()); QVERIFY(dir.remove(file.fileName())); } // well, here it gets a bit more challenging - this is not for // sissies: // - make a controller // - make it create a local storage backend // - make a data model and connect it to the controller // - stimulate the controller and see if the right content ends up // in the database and the model // ----- // ... make the controller: m_configuration->installationId = InstallationId; m_configuration->user.setId(UserId); m_configuration->localStorageType = CHARM_SQLITE_BACKEND_DESCRIPTOR; m_configuration->localStorageDatabase = m_localPath; m_configuration->newDatabase = true; m_controller = new Controller; // ... initialize the backend: QVERIFY(m_controller->initializeBackEnd(CHARM_SQLITE_BACKEND_DESCRIPTOR)); QVERIFY(m_controller->connectToBackend()); // ... make the data model: m_model = new CharmDataModel; // ... connect model and controller: connectControllerAndModel(m_controller, m_model); QVERIFY(m_controller->storage() != nullptr); } void TestApplication::destroy() { QVERIFY(controller()->disconnectFromBackend()); delete m_model; m_model = nullptr; delete m_controller; m_controller = nullptr; if (QDir::home().exists(databasePath())) { const bool result = QDir::home().remove(databasePath()); QVERIFY(result); } } int TestApplication::testUserId() const { return 1; } int TestApplication::testInstallationId() const { return 1; } Charm-1.12.0/Tests/TestApplication.h000066400000000000000000000031701331066577000172140ustar00rootroot00000000000000/* TestApplication.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TESTAPPLICATION_H #define TESTAPPLICATION_H #include class Controller; class CharmDataModel; class Configuration; class TestApplication : public QObject { Q_OBJECT public: explicit TestApplication(const QString &databasePath, QObject *parent = nullptr); void initialize(); void destroy(); int testUserId() const; int testInstallationId() const; protected: Controller *controller() const; CharmDataModel *model() const; Configuration *configuration() const; QString databasePath() const; private: Controller *m_controller = nullptr; CharmDataModel *m_model = nullptr; Configuration *m_configuration; QString m_localPath; }; #endif // TESTAPPLICATION_H Charm-1.12.0/Tests/TestData.qrc000066400000000000000000000013051331066577000161560ustar00rootroot00000000000000 Data/simple-tasklists.xml Data/test-database-export.charmdatabaseexport Data/test-timesheet-report.charmreport Data/tasklist-merges.xml Data/tasklist-treeness.xml Data/test-tasklistexport.xml Charm-1.12.0/Tests/TestHelpers.h000066400000000000000000000053401331066577000163540ustar00rootroot00000000000000/* TestHelpers.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TESTHELPERS_H #define TESTHELPERS_H #include "Core/CharmExceptions.h" #include #include namespace TestHelpers { QList retrieveTestCases(const QString &path, const QString &type) { const QString tagName(QStringLiteral("testcase")); QStringList filenamePatterns; filenamePatterns << QStringLiteral("*.xml"); QDir dataDir(path); if (!dataDir.exists()) throw CharmException(QStringLiteral("path to test case data does not exist")); QFileInfoList dataSets = dataDir.entryInfoList(filenamePatterns, QDir::Files, QDir::Name); QList result; Q_FOREACH (const QFileInfo &fileinfo, dataSets) { QDomDocument doc(QStringLiteral("charmtests")); QFile file(fileinfo.filePath()); if (!file.open(QIODevice::ReadOnly)) throw CharmException(QStringLiteral("unable to open input file")); if (!doc.setContent(&file)) throw CharmException(QStringLiteral("invalid DOM document, cannot load")); QDomElement root = doc.firstChildElement(); if (root.tagName() != QLatin1String("testcases")) throw CharmException(QStringLiteral("root element (testcases) not found")); qDebug() << "Loading test cases from" << file.fileName(); for (QDomElement child = root.firstChildElement(tagName); !child.isNull(); child = child.nextSiblingElement(tagName)) { if (child.attribute(QStringLiteral("type")) == type) result << child; } } return result; } bool attribute(const QString &, const QDomElement &element) { QString text = element.attribute(QStringLiteral("expectedResult")); if (text != QLatin1String("false") && text != QLatin1String("true")) throw CharmException(QStringLiteral("attribute does not represent a boolean")); return text == QLatin1String("true"); } } #endif Charm-1.12.0/Tests/TimeSheetProcessorTests.cpp000066400000000000000000000070521331066577000212610ustar00rootroot00000000000000/* TimeSheetProcessorTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Franck Arrecot This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TimeSheetProcessorTests.h" #include #include #include #include #include "Tools/TimesheetProcessor/Operations.h" #include "Tools/TimesheetProcessor/CommandLine.h" #include "Core/MySqlStorage.h" #include #include TimeSheetProcessorTests::TimeSheetProcessorTests() : m_idTimeSheet(0) , m_adminId(43) , m_reportPath(QStringLiteral(":/timeSheetProcessorTest/Data/test-timesheet-report.charmreport")) { } void TimeSheetProcessorTests::testAddRemoveTimeSheet() { // Add and Remove in the same function to avoid order dependency // TimesheetProcessor -a filename -u userid -m comment <-- add timesheet from file // GIVEN Q_ASSERT(!m_reportPath.isEmpty()); CommandLine cmdAdd(m_reportPath, m_adminId); // WHEN addTimesheet(cmdAdd); // THEN MySqlStorage storage; MySqlStorage::Parameters parameters = MySqlStorage::parseParameterEnvironmentVariable(); storage.configure(parameters); QVERIFY(storage.database().open()); QSqlQuery query(storage.database()); query.prepare(QStringLiteral( "SELECT id, date_time_uploaded from timesheets where filename=:file AND userid=:user")); query.bindValue(QStringLiteral("file"), m_reportPath); query.bindValue(QStringLiteral("user"), m_adminId); QVERIFY(storage.runQuery(query)); QVERIFY(query.next()); QSqlRecord record = query.record(); m_idTimeSheet = record.value(record.indexOf(QStringLiteral("id"))).toInt(); uint dateTimeUploaded = record.value(record.indexOf(QStringLiteral("date_time_uploaded"))).toInt(); uint nowTimeStamp = (QDateTime::currentMSecsSinceEpoch()/1000);// seconds since 1970-01-01 QVERIFY(m_idTimeSheet > 0); QVERIFY(dateTimeUploaded > (nowTimeStamp - 60*60)); // one hour ago QVERIFY(dateTimeUploaded < (nowTimeStamp + 60*60)); // in one hour // GIVEN Q_ASSERT(m_idTimeSheet != 0); CommandLine cmdRemove(m_adminId, m_idTimeSheet); // WHEN removeTimesheet(cmdRemove); // THEN MySqlStorage storageRemove; storageRemove.configure(parameters); QVERIFY(storageRemove.database().open()); QSqlQuery queryRemove(storageRemove.database()); queryRemove.prepare(QStringLiteral( "SELECT id, date_time_uploaded from timesheets where filename=:file AND userid=:user")); queryRemove.bindValue(QStringLiteral("file"), m_reportPath); queryRemove.bindValue(QStringLiteral("user"), m_adminId); QVERIFY(storageRemove.runQuery(queryRemove)); QVERIFY(!queryRemove.next()); // not retrievable since it was deleted, must return false } QTEST_MAIN(TimeSheetProcessorTests) #include "moc_TimeSheetProcessorTests.cpp" Charm-1.12.0/Tests/TimeSheetProcessorTests.h000066400000000000000000000023131331066577000207210ustar00rootroot00000000000000/* TimeSheetProcessorTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Franck Arrecot This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TIMESHEETPROCESSORTESTS_H #define TIMESHEETPROCESSORTESTS_H #include class TimeSheetProcessorTests : public QObject { Q_OBJECT public: explicit TimeSheetProcessorTests(); private Q_SLOTS: void testAddRemoveTimeSheet(); private: int m_idTimeSheet; int m_adminId; QString m_reportPath; }; #endif Charm-1.12.0/Tests/TimeSpanTests.cpp000066400000000000000000000047251331066577000172160ustar00rootroot00000000000000/* TimeSpanTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TimeSpanTests.h" #include "Core/TimeSpans.h" #include void TimeSpanTests::testTimeSpans() { const QDate dec31 = QDate(2012, 2, 1); const QDate jan1 = QDate(2012, 1, 1); const QDate jan30 = QDate(2012, 1, 30); const QDate jan31 = QDate(2012, 1, 31); const QDate feb1 = QDate(2012, 2, 1); const QDate feb2 = QDate(2012, 2, 2); const QDate feb29 = QDate(2012, 2, 29); const QDate mar1 = QDate(2012, 3, 1); const TimeSpans spans(feb1); QVERIFY(spans.today().contains(feb1)); QVERIFY(!spans.today().contains(jan31)); QVERIFY(!spans.today().contains(feb2)); QVERIFY(spans.yesterday().contains(jan31)); QVERIFY(!spans.yesterday().contains(feb1)); QVERIFY(spans.dayBeforeYesterday().contains(jan30)); QVERIFY(!spans.dayBeforeYesterday().contains(jan31)); QCOMPARE(spans.thisWeek().timespan.first, QDate(2012, 1, 30)); QCOMPARE(spans.thisWeek().timespan.second, QDate(2012, 2, 6)); QCOMPARE(spans.lastWeek().timespan.first, QDate(2012, 1, 23)); QCOMPARE(spans.lastWeek().timespan.second, QDate(2012, 1, 30)); QCOMPARE(spans.theWeekBeforeLast().timespan.first, QDate(2012, 1, 16)); QCOMPARE(spans.theWeekBeforeLast().timespan.second, QDate(2012, 1, 23)); QCOMPARE(spans.thisMonth().timespan.first, feb1); QCOMPARE(spans.thisMonth().timespan.second, mar1); QVERIFY(spans.thisMonth().contains(feb29)); QVERIFY(spans.lastMonth().contains(jan1)); QVERIFY(spans.lastMonth().contains(jan31)); QVERIFY(!spans.lastMonth().contains(dec31)); QVERIFY(!spans.lastMonth().contains(feb1)); } QTEST_MAIN(TimeSpanTests) Charm-1.12.0/Tests/TimeSpanTests.h000066400000000000000000000020351331066577000166530ustar00rootroot00000000000000/* TimeSpanTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TIMESPANTESTS_H #define TIMESPANTESTS_H #include class TimeSpanTests : public QObject { Q_OBJECT private Q_SLOTS: void testTimeSpans(); }; #endif Charm-1.12.0/Tests/UpdateCheckerTests.cpp000066400000000000000000000044211331066577000201760ustar00rootroot00000000000000/* UpdateCheckerTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "UpdateCheckerTests.h" #include "Charm/HttpClient/CheckForUpdatesJob.h" #include void UpdateCheckerTests::testVersionComparison() { QVERIFY(Charm::versionLessThan(QStringLiteral("0.1"), QStringLiteral("0.2"))); QVERIFY(Charm::versionLessThan(QStringLiteral("1.9.0"), QStringLiteral("1.10.0"))); QVERIFY(Charm::versionLessThan(QStringLiteral("0.1"), QStringLiteral("0.1.1"))); QVERIFY(!Charm::versionLessThan(QStringLiteral("1.9.0"), QStringLiteral("1.9.0"))); QVERIFY(!Charm::versionLessThan(QStringLiteral("1.10.0"), QStringLiteral("1.9.0"))); QVERIFY(Charm::versionLessThan(QStringLiteral("1.9.0"), QStringLiteral("1.9.0.1"))); QVERIFY(!Charm::versionLessThan(QStringLiteral("1.9.0"), QStringLiteral("1.9.abc"))); QVERIFY(!Charm::versionLessThan(QStringLiteral("2.0.1"), QStringLiteral("1.20.0"))); QVERIFY(!Charm::versionLessThan(QStringLiteral("1.9.0.1"), QStringLiteral("1.9.0.0.1"))); QVERIFY(Charm::versionLessThan(QString(), QStringLiteral("0.2"))); QVERIFY(!Charm::versionLessThan(QStringLiteral("0.2"), QString())); QVERIFY(!Charm::versionLessThan(QString(), QString())); QVERIFY(!Charm::versionLessThan(QStringLiteral(" "), QStringLiteral("."))); QVERIFY(!Charm::versionLessThan(QStringLiteral(" ."), QStringLiteral("...."))); QVERIFY(!Charm::versionLessThan(QStringLiteral(".1."), QStringLiteral(" "))); } QTEST_MAIN(UpdateCheckerTests) Charm-1.12.0/Tests/UpdateCheckerTests.h000066400000000000000000000020711331066577000176420ustar00rootroot00000000000000/* UpdateCheckerTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef UPDATECHECKERTESTS_H #define UPDATECHECKERTESTS_H #include class UpdateCheckerTests : public QObject { Q_OBJECT private Q_SLOTS: void testVersionComparison(); }; #endif Charm-1.12.0/Tests/XmlSerializationTests.cpp000066400000000000000000000145061331066577000207720ustar00rootroot00000000000000/* XmlSerializationTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Frank Osterfeld Author: Mike McQuaid This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "XmlSerializationTests.h" #include "Core/CharmConstants.h" #include "Core/CharmExceptions.h" #include "Core/Event.h" #include "Core/XmlSerialization.h" #include #include #include XmlSerializationTests::XmlSerializationTests() : QObject() { } TaskList XmlSerializationTests::tasksToTest() const { // set up test candidates: TaskList tasks; Task task; task.setName(QStringLiteral("A task")); task.setId(42); task.setParent(4711); task.setSubscribed(true); task.setValidFrom(QDateTime::currentDateTime()); Task task2; task2.setName(QStringLiteral("Another task")); task2.setId(-1); task2.setParent(1000000000); task2.setSubscribed(false); task2.setValidUntil(QDateTime::currentDateTime()); Task task3; tasks << Task() << task << task2; return tasks; } void XmlSerializationTests::testEventSerialization() { // set up test candidates: EventList eventsToTest; Event testEvent; testEvent.setComment(QStringLiteral("A comment")); testEvent.setStartDateTime(QDateTime::currentDateTime()); testEvent.setEndDateTime(QDateTime::currentDateTime().addDays(1)); // add one default-constructed one, plus the other candidates: eventsToTest << Event() << testEvent; QDomDocument document(QStringLiteral("testdocument")); Q_FOREACH (const Event &event, eventsToTest) { QDomElement element = event.toXml(document); try { Event readEvent = Event::fromXml(element); // the extra tests are mostly to immidiately see what is wrong: QVERIFY(event.comment() == readEvent.comment()); QVERIFY(event.startDateTime() == readEvent.startDateTime()); QVERIFY(event.endDateTime() == readEvent.endDateTime()); QVERIFY(event == readEvent); } catch (const CharmException &e) { qDebug() << "XmlSerializationTests::testEventSerialization: exception caught (" << e.what() << ")"; QFAIL("Event Serialization throws"); } } } void XmlSerializationTests::testTaskSerialization() { QDomDocument document(QStringLiteral("testdocument")); Q_FOREACH (Task task, tasksToTest()) { QDomElement element = task.toXml(document); try { Task readTask = Task::fromXml(element, CHARM_DATABASE_VERSION); if (task != readTask) task.dump(); QVERIFY(task == readTask); } catch (const CharmException &e) { qDebug() << "XmlSerializationTests::testTaskSerialization: exception caught (" << e.what() << ")"; QFAIL("Task Serialization throws"); } } } void XmlSerializationTests::testQDateTimeToFromString() { // test regular QDate::toString: QTime time1(QTime::currentTime()); time1.setHMS(time1.hour(), time1.minute(), time1.second()); // strip milliseconds QString time1string(time1.toString()); QTime time2 = QTime::fromString(time1string); QVERIFY(time1 == time2); // test toString with ISODate: QTime time3(QTime::currentTime()); time3.setHMS(time3.hour(), time3.minute(), time3.second()); // strip milliseconds QString time3string(time3.toString(Qt::ISODate)); QTime time4 = QTime::fromString(time3string, Qt::ISODate); QVERIFY(time3 == time4); // test regular QDateTime::toString: QDateTime date1(QDateTime::currentDateTime()); date1.setTime(time1); QString date1string = date1.toString(); QDateTime date2 = QDateTime::fromString(date1string); QVERIFY(date1 == date2); // test regular QDateTime::toString: QDateTime date3(QDateTime::currentDateTime()); date3.setTime(time1); QString date3string = date3.toString(Qt::ISODate); QDateTime date4 = QDateTime::fromString(date3string, Qt::ISODate); QVERIFY(date3 == date4); } void XmlSerializationTests::testTaskListSerialization() { TaskList tasks = tasksToTest(); QVERIFY(!Task::checkForTreeness(tasks)); tasks.pop_front(); // FIXME this needs to go into an extra test module named // TaskStructureTests // // FIXME the data to test needs to be retrieved from resources // // just making sure: Q_FOREACH (const Task &task, tasks) QVERIFY(task.isValid()); // the next test fails because tasks contains orphan elements (the // parent they have assigned does not exist) QVERIFY(!Task::checkForTreeness(tasks)); QDomDocument document(QStringLiteral("testdocument")); QDomElement element = Task::makeTasksElement(document, tasks); try { TaskList result = Task::readTasksElement(element, CHARM_DATABASE_VERSION); QVERIFY(tasks.count() == result.count()); for (int i = 0; i < tasks.count(); ++i) { if (tasks[i] != result[i]) { tasks[i].dump(); result[i].dump(); } } QVERIFY(tasks == result); } catch (const XmlSerializationException &e) { qCritical() << "Failure reading tasks:" << e.what(); QFAIL("Read tasks are not equal to the written ones"); } } void XmlSerializationTests::testTaskExportImport() { TaskExport importer; importer.readFrom(QStringLiteral(":/testTaskExportImport/Data/test-tasklistexport.xml")); QVERIFY(!importer.tasks().isEmpty()); QVERIFY(importer.exportTime().isValid()); } QTEST_MAIN(XmlSerializationTests) Charm-1.12.0/Tests/XmlSerializationTests.h000066400000000000000000000024671331066577000204420ustar00rootroot00000000000000/* XmlSerializationTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef XMLSERIALIZATIONTESTS_H #define XMLSERIALIZATIONTESTS_H #include #include "Core/Task.h" class XmlSerializationTests : public QObject { Q_OBJECT public: XmlSerializationTests(); private Q_SLOTS: void testEventSerialization(); void testTaskSerialization(); void testTaskListSerialization(); void testQDateTimeToFromString(); void testTaskExportImport(); private: TaskList tasksToTest() const; }; #endif Charm-1.12.0/Tools/000077500000000000000000000000001331066577000137355ustar00rootroot00000000000000Charm-1.12.0/Tools/Anonymizer/000077500000000000000000000000001331066577000160705ustar00rootroot00000000000000Charm-1.12.0/Tools/Anonymizer/Anonymizer.cpp000066400000000000000000000023551331066577000207340ustar00rootroot00000000000000/* Anonymizer.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include "Core/XmlSerialization.h" class AnonymizerException { public: explicit AnonymizerException(QString what) : m_what(what) { } private: QString m_what; }; int main(int argc, char **argv) { QCoreApplication app(argc, argv); try { if (argc != 2) QFileInfo fileInfo(argv[1]); } Charm-1.12.0/Tools/TimesheetGenerator/000077500000000000000000000000001331066577000175335ustar00rootroot00000000000000Charm-1.12.0/Tools/TimesheetGenerator/CMakeLists.txt000066400000000000000000000005161331066577000222750ustar00rootroot00000000000000INCLUDE_DIRECTORIES( ${Charm_SOURCE_DIR} ${Charm_BINARY_DIR} ) SET( TimesheetGenerator_SRCS main.cpp Options.cpp ) ADD_EXECUTABLE( TimesheetGenerator ${TimesheetGenerator_SRCS} ) TARGET_LINK_LIBRARIES( TimesheetGenerator CharmCore ${QT_LIBRARIES} ) INSTALL( TARGETS TimesheetGenerator DESTINATION ${BIN_INSTALL_DIR} ) Charm-1.12.0/Tools/TimesheetGenerator/Exceptions.h000066400000000000000000000026361331066577000220340ustar00rootroot00000000000000/* Exceptions.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2009-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef EXCEPTIONS_H #define EXCEPTIONS_H #include #include namespace TimesheetGenerator { class Exception : public std::exception { public: explicit Exception(const QString &text = QString()) : mWhat(text) { } ~Exception() throw() { } const char *what() const throw() { return qPrintable(mWhat); } private: QString mWhat; }; class UsageException : public Exception { public: explicit UsageException(const QString &text = QString()) : Exception(text) { } }; } #endif Charm-1.12.0/Tools/TimesheetGenerator/Options.cpp000066400000000000000000000056721331066577000217040ustar00rootroot00000000000000/* Options.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2009-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Options.h" #include "Exceptions.h" #include "CharmCMake.h" #include #include extern "C" { #include } #include #include using namespace TimesheetGenerator; Options::Options(int argc, char **argv) { opterr = 0; int ch; while ((ch = getopt(argc, argv, "vhf:d:")) != -1) { if (ch == '?') { // unparsable argument int option = optopt; if (option == 'f') { throw UsageException(QObject::tr("Option -f requires a filename argument")); } else if (option == 'd') { throw UsageException(QObject::tr( "Option -d requires a date argument (e.g. 2009-01-01)")); } else { int code = static_cast(option); throw UsageException(QObject::tr("Unknown character %1").arg(code)); } } switch (ch) { case 'f': mFile = QString::fromLocal8Bit(optarg); break; case 'd': { const QString text = QString::fromLocal8Bit(optarg); QDate date = QDate::fromString(text, QStringLiteral("yyyy-MM-dd")); if (date.isValid()) { mDate = date; } else { throw UsageException(QObject::tr("Cannot parse date \"%1\"").arg(text)); } break; } case 'h': throw UsageException(); case 'v': std::cout << CHARM_VERSION << std::endl; exit(0); break; default: break; } } if (mFile.isEmpty()) throw UsageException(QObject::tr("No filename specified (-f), aborting.")); if (!mDate.isValid()) throw UsageException(QObject::tr("No date specified (-d), aborting.")); using namespace std; QFile file(mFile); if (!file.exists()) throw UsageException(QObject::tr("Specified file not found, aborting.")); } QString Options::file() const { return mFile; } QDate Options::date() const { return mDate; } Charm-1.12.0/Tools/TimesheetGenerator/Options.h000066400000000000000000000021731331066577000213420ustar00rootroot00000000000000/* Options.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2009-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef OPTIONS_H #define OPTIONS_H #include #include namespace TimesheetGenerator { class Options { public: explicit Options(int argc, char **argv); QString file() const; QDate date() const; private: QString mFile; QDate mDate; }; } #endif Charm-1.12.0/Tools/TimesheetGenerator/example-weekly-template.xml000066400000000000000000000007331331066577000250220ustar00rootroot00000000000000 auto-generated entry 1 auto-generated entry 2 Charm-1.12.0/Tools/TimesheetGenerator/main.cpp000066400000000000000000000150751331066577000211730ustar00rootroot00000000000000/* main.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2009-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include "Core/Event.h" #include "Core/CharmExceptions.h" #include #include "Exceptions.h" #include "Options.h" int main(int argc, char **argv) { using namespace std; try { using namespace TimesheetGenerator; cout << "Timesheet Generator, (C) 2009 Mirko Boehm, KDAB" << endl; Options options(argc, argv); // create a QDomDocument from the file content: QDomDocument doc(QString::fromLatin1("charm_template")); QFile file(options.file()); if (!file.open(QIODevice::ReadOnly)) throw Exception(QObject::tr("Cannot open specified file for reading.")); QString errorMessage; int errorLine, errorColumn; if (!doc.setContent(&file, false, &errorMessage, &errorLine, &errorColumn)) { throw Exception(QObject::tr("Cannot read specified file: ") + errorMessage + QObject::tr(" (%1:%2)").arg(errorLine).arg(errorColumn)); } // find the time sheet elements, loop to create the time sheets: QDomElement docElem = doc.documentElement(); if (docElem.tagName() != QString::fromLatin1("charm_template")) throw Exception(QObject::tr("Wrong document element found in the specified file.")); const QString timesheetTagName = QString::fromLatin1("timesheet"); for (QDomElement child = docElem.firstChildElement(timesheetTagName); !child.isNull(); child = child.nextSiblingElement(timesheetTagName)) { const QString userIdString = child.attributes().namedItem(QStringLiteral("userid")).nodeValue(); bool ok; const int userId = userIdString.toInt(&ok); if (!ok) throw Exception(QObject::tr("Mis-spelled or missing user id in timesheet entry.")); // read event entries: EventList events; // FIXME readEfforts? const QString eventTagName = QString::fromLatin1("event"); for (QDomElement eventElem = child.firstChildElement(eventTagName); !eventElem.isNull(); eventElem = eventElem.nextSiblingElement(eventTagName)) { try { Event event = Event::fromXml(eventElem); // event.dump(); events << event; } catch (XmlSerializationException &e) { throw Exception(QObject::tr("Error reading event: ") + e.what()); } } // now create the time sheet: QDomDocument document = XmlSerialization::createXmlTemplate(QStringLiteral("weekly-timesheet")); // find metadata and report element: QDomElement root = document.documentElement(); QDomElement metadata = XmlSerialization::metadataElement(document); QDomElement report = XmlSerialization::reportElement(document); Q_ASSERT(!root.isNull() && !metadata.isNull() && !report.isNull()); // extend metadata tag: add year, and serial (week) number: // temp: // the start date of the week, specified on the command line: const QDateTime start(options.date(), QTime(), Qt::UTC); int year; const int week = start.date().weekNumber(&year); { QDomElement yearElement = document.createElement(QStringLiteral("year")); metadata.appendChild(yearElement); QDomText text = document.createTextNode(QString::number(year)); yearElement.appendChild(text); QDomElement weekElement = document.createElement(QStringLiteral("serial-number")); weekElement.setAttribute(QStringLiteral("semantics"), QStringLiteral("week-number")); metadata.appendChild(weekElement); QDomText weektext = document.createTextNode(QString::number(week)); weekElement.appendChild(weektext); } { // effort // make effort element: QDomElement effort = document.createElement(QStringLiteral("effort")); report.appendChild(effort); // create elements: Q_FOREACH (Event event, events) { const QDateTime end = start.addSecs(event.duration()); event.setStartDateTime(start); event.setEndDateTime(end); effort.appendChild(event.toXml(document)); } } // save the file: // temp: const QString filename = QString::fromLatin1( "WeeklyTimesheet-generated-%1-%2-%3.charmreport") .arg(userId).arg(year).arg(week); QFile file(filename); if (file.open(QIODevice::WriteOnly)) { QTextStream stream(&file); document.save(stream, 4); cout << "Generated Time Sheet: " << qPrintable(filename) << endl; } else { throw Exception(QObject::tr("Error writing file ") + filename); } } return 0; } catch (TimesheetGenerator::UsageException &e) { cerr << e.what() << endl; cout << "Usage: " << endl << " * TimesheetGenerator -h <-- get help" << endl << " * TimesheetGenerator -f template-filename -d date <-- generate timesheets from template for that date" << endl; return 1; } catch (TimesheetGenerator::Exception &e) { cerr << e.what() << endl; return 1; } } Charm-1.12.0/Tools/TimesheetProcessor/000077500000000000000000000000001331066577000175645ustar00rootroot00000000000000Charm-1.12.0/Tools/TimesheetProcessor/CMakeLists.txt000066400000000000000000000005641331066577000223310ustar00rootroot00000000000000INCLUDE_DIRECTORIES( ${Charm_SOURCE_DIR} ${Charm_BINARY_DIR} ) SET( TimesheetProcessor_SRCS main.cpp CommandLine.cpp Operations.cpp Database.cpp ) ADD_EXECUTABLE( TimesheetProcessor ${TimesheetProcessor_SRCS} ) TARGET_LINK_LIBRARIES( TimesheetProcessor CharmCore ${QT_LIBRARIES} ) INSTALL( TARGETS TimesheetProcessor DESTINATION ${BIN_INSTALL_DIR} ) Charm-1.12.0/Tools/TimesheetProcessor/CommandLine.cpp000066400000000000000000000206771331066577000224720ustar00rootroot00000000000000/* CommandLine.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "CommandLine.h" #include "Exceptions.h" #include #include extern "C" { #include } #include CommandLine::CommandLine(int argc, char **argv) : m_mode(Mode_None) , m_index() , m_userid() { opterr = 0; int ch; while ((ch = getopt(argc, argv, "vhza:x:c:ri:u:m:")) != -1) { if (ch == '?') { // unparsable argument int option = optopt; if (option == 'a') { throw UsageException(QObject::tr( "Option -a requires a filename argument")); } if (option == 'i') { throw UsageException(QObject::tr( "Option -i requires an index argument")); } if (option == 'x') { throw UsageException(QObject::tr( "Option -x requires a filename argument")); } if (option == 'm') throw UsageException(QObject::tr("Option -m requires a user comment argument")); if (isprint(option)) { throw UsageException( QObject::tr("Unknown option %1").arg(option)); } else { int code = static_cast(option); throw UsageException(QObject::tr("Unknown character %1").arg( code)); } } switch (ch) { case 'a': if (m_mode != Mode_None) { QString msg = QObject::tr( "Multiple mode selections, please use only one"); throw UsageException(msg); } // mode m_filename = QString::fromLocal8Bit(optarg); m_mode = Mode_AddTimesheet; break; case 'x': if (m_mode != Mode_None) { QString msg = QObject::tr( "Multiple mode selections, please use only one"); throw UsageException(msg); } // mode m_exportFilename = QString::fromLocal8Bit(optarg); m_mode = Mode_ExportProjectcodes; break; case 'c': if (m_mode != Mode_None) { QString msg = QObject::tr( "Multiple mode selections, please use only one"); throw UsageException(msg); } m_userName = QString::fromLocal8Bit(optarg); m_mode = Mode_CheckOrCreateUser; break; case 'r': // remove if (m_mode != Mode_None) { QString msg = QObject::tr( "Multiple mode selections, please use only one"); throw UsageException(msg); } m_mode = Mode_RemoveTimesheet; break; case 'u': // user id { if (m_userid != 0) { QString msg = QObject::tr( "Multiple user id selections, please use only one"); throw UsageException(msg); } QString arg = QString::fromLocal8Bit(optarg); bool ok; m_userid = arg.toInt(&ok); if (!ok || m_userid < 1) { throw UsageException( QObject::tr( "Argument to option -u must be an integer user id larger than zero")); } break; } case 'i': { // index if (m_index != 0) { QString msg = QObject::tr( "Multiple index selections, please use only one"); throw UsageException(msg); } QString arg = QString::fromLocal8Bit(optarg); bool ok; m_index = arg.toInt(&ok); if (!ok || m_index < 1) { throw UsageException( QObject::tr( "Argument to option -i must be an integer index larger than zero")); } break; } case 'm': { if (!m_userComment.isEmpty()) { QString msg = QObject::tr("Multiple user comments specified, please use only one"); throw UsageException(msg); } QString arg = QString::fromLocal8Bit(optarg); m_userComment = arg; break; } case 'z': // initialize the database m_mode = Mode_InitializeDatabase; break; case 'v': m_mode = Mode_PrintVersion; break; case 'h': default: // help/usage m_mode = Mode_DescribeUsage; //return; } } // check for other command line arguments and yell at the user: if (optind < argc) { QString msg; for (int index = optind; index < argc; index++) { msg += QObject::tr("Unknown extra argument \"%1\"\n").arg( QString::fromLatin1(argv[index])); } throw UsageException(msg); } // final checks: QString msg; if (m_mode == Mode_None) { msg += QObject::tr("No mode selected. Use one of -a filename, -r."); } else if (m_mode == Mode_RemoveTimesheet) { if (m_index < 1) { msg += QObject::tr("No index specified. -a filename, " " -r require an index specified with -i."); } if (m_userid < 1) { msg += QObject::tr("No userid specified. -a filename, " " -r require a user id specified with -u."); } } else if (m_mode == Mode_AddTimesheet) { if (m_index > 0) msg += QObject::tr( "Specifying an index when adding a time sheet is not supported anymore."); } if (!msg.isEmpty()) throw UsageException(msg); } CommandLine::CommandLine(const QString file, const int userId) { m_filename = file; m_userid = userId; } CommandLine::CommandLine(const int userId, const int index) { m_userid = userId; m_index = index; } CommandLine::Mode CommandLine::mode() const { return m_mode; } QString CommandLine::userName() const { return m_userName; } QString CommandLine::userComment() const { return m_userComment; } QString CommandLine::filename() const { return m_filename; } QString CommandLine::exportFilename() const { return m_exportFilename; } int CommandLine::userid() const { return m_userid; } int CommandLine::index() const { return m_index; } void CommandLine::usage() { using namespace std; cout << "Timesheet Processor, (C) 2008 Mirko Boehm, KDAB" << endl << "Usage: " << endl << " * TimesheetProzessor -h <-- get help" << endl << " * TimesheetProzessor -v <-- print version" << endl << " * TimesheetProzessor -a filename -u userid -m comment <-- add timesheet from file" << endl << " * TimesheetProzessor -r -i index -u userid <-- remove timesheet at index" << endl << " * TimesheetProzessor -c username <-- create user if user does not exist" << endl << " * TimesheetProzessor -x filename <-- export project codes to XML file" << endl << " * TimesheetProzessor -z <-- initialize database (careful!)" << endl; } Charm-1.12.0/Tools/TimesheetProcessor/CommandLine.h000066400000000000000000000035061331066577000221270ustar00rootroot00000000000000/* CommandLine.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMMANDLINE_H #define COMMANDLINE_H #include class CommandLine { public: CommandLine(int argc, char **argv); CommandLine(const QString file, const int userId); CommandLine(const int userId, const int index); enum Mode { Mode_None, Mode_InitializeDatabase, Mode_CheckOrCreateUser, Mode_DescribeUsage, Mode_PrintVersion, Mode_AddTimesheet, Mode_RemoveTimesheet, Mode_ExportProjectcodes, Mode_NumberOfModes }; Mode mode() const; QString filename() const; QString userComment() const; QString exportFilename() const; QString userName() const; int userid() const; int index() const; /** Dump command line option reference. */ static void usage(); private: QString m_filename; QString m_userComment; QString m_userName; QString m_exportFilename; Mode m_mode; int m_index; int m_userid; }; #endif /*COMMANDLINE_H*/ Charm-1.12.0/Tools/TimesheetProcessor/Database.cpp000066400000000000000000000127111331066577000217760ustar00rootroot00000000000000/* Database.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Database.h" #include "Exceptions.h" #include "Core/CharmExceptions.h" #include #include #include #include #include #include #include #include #include Database::Database() { } Database::~Database() { } void Database::checkUserid(int id) throw (TimesheetProcessorException) { User user = m_storage.getUser(id); if (!user.isValid()) throw TimesheetProcessorException(QStringLiteral("No such user")); } User Database::getOrCreateUserByName(QString name) throw (TimesheetProcessorException) { User user; QSqlQuery query(database()); query.prepare(QStringLiteral("SELECT user_id from Users WHERE name = :user_name;")); query.bindValue(QStringLiteral(":user_name"), name); bool result = query.exec(); if (result) { if (query.next()) { int userIdPosition = query.record().indexOf(QStringLiteral("user_id")); Q_ASSERT(userIdPosition != -1); int userId = query.value(userIdPosition).toInt(); user = m_storage.getUser(userId); } else { // user with this name does not exist: user = m_storage.makeUser(name); // that should work if (!user.isValid()) throw TimesheetProcessorException(QStringLiteral("Cannot create the new user")); } } else { throw TimesheetProcessorException(QStringLiteral("Cannot execute query for user name")); } return user; } Task Database::getTask(int taskid) throw (TimesheetProcessorException) { Task task = m_storage.getTask(taskid); if (!task.isValid()) throw TimesheetProcessorException(QObject::tr("Invalid task %1 in report").arg(taskid)); return task; } TaskList Database::getAllTasks() throw(TimesheetProcessorException) { return m_storage.getAllTasks(); } QSqlDatabase &Database::database() { return m_storage.database(); } void Database::login() throw (TimesheetProcessorException) { MySqlStorage::Parameters parameters; try { parameters = MySqlStorage::parseParameterEnvironmentVariable(); } catch (ParseError &e) { throw TimesheetProcessorException(e.what()); } m_storage.configure(parameters); bool ok = m_storage.database().open(); if (!ok) { QSqlError error = m_storage.database().lastError(); QString msg = QObject::tr("Cannot connect to database %1 on host %2, database said " "\"%3\", driver said \"%4\"") .arg(parameters.database).arg(parameters.host) .arg(error.driverText()) .arg(error.databaseText()); throw TimesheetProcessorException(msg); } // check if the driver has transaction support if (!m_storage.database().driver()->hasFeature(QSqlDriver::Transactions)) { QString msg = QObject::tr( "The database driver in use does not support transactions. Transactions are required."); throw TimesheetProcessorException(msg); } } void Database::initializeDatabase() throw (TimesheetProcessorException) { try { QStringList tables = m_storage.database().tables(); if (!tables.empty()) { throw TimesheetProcessorException(QStringLiteral("The database is not empty. Only " "empty databases can be automatically initialized.")); } if (!m_storage.createDatabaseTables()) throw TimesheetProcessorException(QStringLiteral( "Cannot create database contents, please double-check permissions.")); } catch (UnsupportedDatabaseVersionException &e) { throw TimesheetProcessorException(e.what()); } } void Database::addEvent(const Event &event, const SqlRaiiTransactor &t) { Event newEvent = m_storage.makeEvent(t); int id = newEvent.id(); newEvent = event; newEvent.setId(id); if (!m_storage.modifyEvent(newEvent, t)) throw TimesheetProcessorException(QStringLiteral("Cannot add event")); } void Database::deleteEventsForReport(int userid, int index) { // delete the time sheet: pretty straightforward QString statement = QString::fromLocal8Bit( "DELETE FROM Events WHERE report_id = :index and user_id = :userid"); QSqlQuery query(m_storage.database()); query.prepare(statement); query.bindValue(QStringLiteral(":index"), index); query.bindValue(QStringLiteral(":userid"), userid); bool result = query.exec(); if (!result) throw TimesheetProcessorException(QStringLiteral("Failed to delete report")); } Charm-1.12.0/Tools/TimesheetProcessor/Database.h000066400000000000000000000033061331066577000214430ustar00rootroot00000000000000/* Database.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef DATABASE_H #define DATABASE_H #include "Exceptions.h" #include "Core/User.h" #include "Core/Task.h" #include "Core/MySqlStorage.h" #include class SqlRaiiTransactor; class Database { public: Database(); virtual ~Database(); void login() throw (TimesheetProcessorException); void initializeDatabase() throw (TimesheetProcessorException); void addEvent(const Event &event, const SqlRaiiTransactor &); void deleteEventsForReport(int userid, int index); void checkUserid(int id) throw (TimesheetProcessorException); User getOrCreateUserByName(QString name) throw (TimesheetProcessorException); Task getTask(int taskid) throw (TimesheetProcessorException); TaskList getAllTasks() throw (TimesheetProcessorException); QSqlDatabase &database(); private: MySqlStorage m_storage; }; #endif /*DATABASE_H*/ Charm-1.12.0/Tools/TimesheetProcessor/Exceptions.h000066400000000000000000000027371331066577000220670ustar00rootroot00000000000000/* Exceptions.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef EXCEPTIONS_H #define EXCEPTIONS_H #include #include class TimesheetProcessorException : public std::exception { public: explicit TimesheetProcessorException(const QString &text = QString::null) : mWhat(text) { } ~TimesheetProcessorException() throw() { } const char *what() const throw() { return qPrintable(mWhat); } private: QString mWhat; }; class UsageException : public TimesheetProcessorException { public: explicit UsageException(const QString &text = QString::null) : TimesheetProcessorException(text) { } }; #endif Charm-1.12.0/Tools/TimesheetProcessor/Operations.cpp000066400000000000000000000245531331066577000224240ustar00rootroot00000000000000/* Operations.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm Author: Sebastian Sauer This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Operations.h" #include "CommandLine.h" #include "Exceptions.h" #include "Database.h" #include "Core/User.h" #include "Core/Event.h" #include "Core/SqlRaiiTransactor.h" #include "Core/XmlSerialization.h" #include #include #include #include #include #include #include #include #include void initializeDatabase(const CommandLine &cmd) { using namespace std; cout << "Initializing database." << endl; Database database; database.login(); cout << "Logged in." << endl; database.initializeDatabase(); cout << "Database initialized." << endl; } void checkOrCreateUser(const CommandLine &cmd) { using namespace std; Database database; database.login(); User user = database.getOrCreateUserByName(cmd.userName()); cout << user.id() << endl; } void addTimesheet(const CommandLine &cmd) { using namespace std; // load the time sheet: QFile file(cmd.filename()); if (!file.exists()) throw TimesheetProcessorException(QObject::tr("File %1 does not exist.").arg(cmd.filename())); // load the XML into a DOM tree: if (!file.open(QIODevice::ReadOnly)) { QString msg = QObject::tr("Cannot open file %1 for reading.").arg(cmd.filename()); throw TimesheetProcessorException(msg); } QDomDocument doc(QStringLiteral("timesheet")); if (!doc.setContent(&file)) { QString msg = QObject::tr("Cannot read file %1.").arg(cmd.filename()); throw TimesheetProcessorException(msg); } // add it to the database: // 1) make a list of all the events: EventList events; QDomElement charmReportElement = doc.firstChildElement(QStringLiteral("charmreport")); QDomElement metadataElement = charmReportElement.firstChildElement(QStringLiteral("metadata")); QDomElement yearElement = metadataElement.firstChildElement(QStringLiteral("year")); QString year = yearElement.text().simplified(); QDomElement weekElement = metadataElement.firstChildElement(QStringLiteral("serial-number")); QString week = weekElement.text().simplified(); QDomElement reportElement = charmReportElement.firstChildElement(QStringLiteral("report")); QDomElement effortElement = reportElement.firstChildElement(QStringLiteral("effort")); if (effortElement.isNull()) { QString msg = QObject::tr("Invalid structure in file %1.").arg(cmd.filename()); throw TimesheetProcessorException(msg); } int totalSeconds = 0; QDomElement element = effortElement.firstChildElement(Event::tagName()); for (; !element.isNull(); element = element.nextSiblingElement(Event::tagName())) { try { Event e = Event::fromXml(element); events << e; totalSeconds += e.duration(); // e.dump(); } catch (const XmlSerializationException &e) { const QString msg = QObject::tr("Syntax error in file %1: %2.").arg( cmd.filename(), e.what()); throw TimesheetProcessorException(msg); } } uint dateTimeUploaded = QDateTime::currentMSecsSinceEpoch()/1000;// seconds since 1970-01-01 // 2) log into database Database database; database.login(); int index = -1; // check for the user id database.checkUserid(cmd.userid()); try { SqlRaiiTransactor transaction(database.database()); // add time sheet to time sheets list { QSqlQuery query(database.database()); query.prepare(QStringLiteral( "INSERT into timesheets VALUES( 0, :filename, :original_filename, :year, :week, :total, :userid, 0, :date_time_uploaded)")); query.bindValue(QString::fromLatin1(":filename"), cmd.filename()); query.bindValue(QString::fromLatin1(":original_filename"), cmd.userComment()); query.bindValue(QString::fromLatin1(":year"), year); query.bindValue(QString::fromLatin1(":week"), week); query.bindValue(QString::fromLatin1(":total"), totalSeconds); query.bindValue(QString::fromLatin1(":userid"), cmd.userid()); query.bindValue(QString::fromLatin1(":date_time_uploaded"), dateTimeUploaded); if (!query.exec()) { QString msg = QObject::tr("Error adding time sheet %1.").arg(cmd.filename()); throw TimesheetProcessorException(msg); } } // retrieve index { QSqlQuery query(database.database()); if (!query.exec(QStringLiteral("SELECT id from timesheets WHERE id = last_insert_id()"))) { QString msg = QObject::tr("SQL error retrieving index for time sheet %1.").arg( cmd.filename()); throw TimesheetProcessorException(msg); } if (query.next()) { const int idField = query.record().indexOf(QStringLiteral("id")); index = query.value(idField).toInt(); } else { QString msg = QObject::tr("Error retrieving index for time sheet %1.").arg( cmd.filename()); throw TimesheetProcessorException(msg); } } Q_ASSERT(index > 0); cout << "Adding report " << index << " for user " << cmd.userid() << endl; // add the events to the database Q_FOREACH (Event e, events) { // check for the project code, if this does not throw an exception, the task id exists Task task = database.getTask(e.taskId()); // FIXME check for reporting period for the task, not implemented in the DB e.setUserId(cmd.userid()); e.setReportId(index); database.addEvent(e, transaction); } transaction.commit(); cout << "Report added" << endl << "total:" << totalSeconds << endl << "year:" << year.toLocal8Bit().constData() << endl << "week:" << week.toLocal8Bit().constData() << endl << "uploadedTime:" << dateTimeUploaded << endl << "index:" << index << endl; } catch (const TimesheetProcessorException &e) { if (index >= 0) { // A valid index means that something with the events/tasks went wrong and triggered the // exception. In that case we already created an entry in the Charm/timesheets table that // we would need to remove again cause adding the timesheet just failed. Normally this // should have been done by the transaction we created above. Cause we did not commit() // the transction should be rollback() and there should be no item with id=index in the // Charm/timesheets table any longer... QSqlQuery query(database.database()); if (query.exec(QStringLiteral("SELECT id from timesheets WHERE id = %1").arg(index))) { if (query.next()) { //WTF, if that happens that something went wrong with the transaction QSqlQuery deletequery(database.database()); deletequery.exec(QStringLiteral("DELETE FROM timesheets WHERE id = %1").arg(index)); qDebug() << "CRITICAL ERROR: A database transaction did not roll back as expected. " "Please report this to the administrators. The time sheet with index " << index << " needs to be cleand up."; } } } throw e; } } void removeTimesheet(const CommandLine &cmd) { using namespace std; cout << "Removing report " << cmd.index() << endl; Database database; database.login(); SqlRaiiTransactor transaction(database.database()); database.deleteEventsForReport(cmd.userid(), cmd.index()); { QSqlQuery query(database.database()); if (!query.prepare(QStringLiteral("DELETE from timesheets WHERE id = :index"))) { QString msg = QObject::tr("Error prepare to remove timesheet %1.").arg(cmd.index()); throw TimesheetProcessorException(msg); } query.bindValue(QString::fromLatin1(":index"), cmd.index()); if (!query.exec()) { QString msg = QObject::tr("Error removing timesheet %1.").arg(cmd.index()); throw TimesheetProcessorException(msg); } if (query.numRowsAffected() < 1) { QString msg = QObject::tr("No such timesheet %1.").arg(cmd.index()); throw TimesheetProcessorException(msg); } } if (!transaction.commit()) { QString msg = QObject::tr("Error commit remove timesheet %1.").arg(cmd.index()); throw TimesheetProcessorException(msg); } cout << "Report " << cmd.index() << " removed" << endl; } void exportProjectcodes(const CommandLine &cmd) { using namespace std; cout << "Exporting project codes to " << qPrintable(cmd.exportFilename()) << endl; Database database; database.login(); TaskList tasks = database.getAllTasks(); try { TaskExport::writeTo(cmd.exportFilename(), tasks); } catch (const XmlSerializationException &e) { throw TimesheetProcessorException(QObject::tr("Cannot write to file %1: %2").arg(cmd. exportFilename(), e.what())); } cout << "Done, " << tasks.count() << " tasks definitions exported." << endl; } Charm-1.12.0/Tools/TimesheetProcessor/Operations.h000066400000000000000000000023731331066577000220650ustar00rootroot00000000000000/* Operations.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef OPERATIONS_H #define OPERATIONS_H /* Define functions that implement the operations of the time sheet processor. */ class CommandLine; void initializeDatabase(const CommandLine &cmd); void addTimesheet(const CommandLine &cmd); void removeTimesheet(const CommandLine &cmd); void checkOrCreateUser(const CommandLine &cmd); void exportProjectcodes(const CommandLine &cmd); #endif /*OPERATIONS_H*/ Charm-1.12.0/Tools/TimesheetProcessor/main.cpp000066400000000000000000000043261331066577000212210ustar00rootroot00000000000000/* main.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Mirko Boehm This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /* This program adds or removes timesheet XML files to and from an SQL * database. */ #include #include #include "CommandLine.h" #include "Exceptions.h" #include "Operations.h" #include "CharmCMake.h" int main(int argc, char **argv) { using namespace std; try { CommandLine cmd(argc, argv); switch (cmd.mode()) { case CommandLine::Mode_InitializeDatabase: initializeDatabase(cmd); break; case CommandLine::Mode_CheckOrCreateUser: checkOrCreateUser(cmd); break; case CommandLine::Mode_AddTimesheet: addTimesheet(cmd); break; case CommandLine::Mode_RemoveTimesheet: removeTimesheet(cmd); break; case CommandLine::Mode_ExportProjectcodes: exportProjectcodes(cmd); break; case CommandLine::Mode_PrintVersion: cout << CHARM_VERSION << endl; break; case CommandLine::Mode_DescribeUsage: default: CommandLine::usage(); return 0; } } catch (const UsageException &e) { cerr << e.what() << endl; CommandLine::usage(); return 1; } catch (const TimesheetProcessorException &e) { cerr << e.what() << endl; CommandLine::usage(); return 1; } return 0; } Charm-1.12.0/android/000077500000000000000000000000001331066577000142555ustar00rootroot00000000000000Charm-1.12.0/android/AndroidManifest.xml000066400000000000000000000072441331066577000200550ustar00rootroot00000000000000 Charm-1.12.0/appveyor.ini000066400000000000000000000033751331066577000152130ustar00rootroot00000000000000[General] Branch = master ShallowClone = True Command = craft # Variables defined here override the default value # The variable names are casesensitive [Variables] #Root = D:\qt-sdk #Values need to be overwritten to create a cache UseCache = True CreateCache = True Msys = C:\msys64\ APPVEYOR_BUILD_FOLDER = ${Variables:Root} # Settings applicable for all Crafts matrices # Settings are Category/key=value # Category is case sensitive [GeneralSettings] General/EMERGE_PKGDSTDIR=${Variables:APPVEYOR_BUILD_FOLDER}/binaries Paths/python = C:\Python36 Paths/python27 = C:\Python27 Paths/Msys = ${Variables:Msys} Paths/DownloadDir = ${Variables:Root}/downloads ShortPath/Enabled = False ShortPath/EnableJunctions = True ShortPath/JunctionDir = ${Variables:Root}/csp Packager/UseCache = ${Variables:UseCache} Packager/CreateCache = ${Variables:CreateCache} Packager/CacheDir = ${Variables:APPVEYOR_BUILD_FOLDER}/cache Packager/RepositoryUrl = http://downloads.kdab.com/ci/cache/gammaray/cache ContinuousIntegration/RepositoryUrl = http://downloads.kdab.com/ci/cache/gammaray/binary ContinuousIntegration/UpdateRepository = True Compile/BuildType = Release ContinuousIntegration/Enabled=True QtSDK/Version=5.10.1 QtSDK/Path=C:\Qt QtSDK/Enabled=True Blueprints/BlueprintRoot = ${Variables:Root}/blueprints [BlueprintSettings] /.buildTests = False qt-apps/gammaray.version = master qt-apps/charm.version = master libs/openssl.version = 1.0.2o libs/icu.ignored = True binary/mysql.ignored = True libs/dbus.ignored = True [windows-msvc2015_32-cl] QtSDK/Compiler = msvc2015 General/ABI = windows-msvc2015_32-cl [windows-mingw_32-gcc] QtSDK/Compiler = mingw53_32 General/ABI = windows-mingw_32-gcc [windows-msvc2017_64-cl] QtSDK/Compiler = msvc2017_64 General/ABI = windows-msvc2017_64-cl Charm-1.12.0/appveyor.yml000066400000000000000000000023541331066577000152310ustar00rootroot00000000000000version: '{build}-{branch}' branches: # whitelist only: - master # don't enable for charm #clone_depth: 50 init: - ps: | function craft() { & "C:\python36\python.exe" "C:\CraftMaster\CraftMaster\CraftMaster.py" --config "$env:APPVEYOR_BUILD_FOLDER\appveyor.ini" --variables "APPVEYOR_BUILD_FOLDER=$env:APPVEYOR_BUILD_FOLDER" --target $env:TARGET -c $args if($LASTEXITCODE -ne 0) {exit $LASTEXITCODE} } install: - ps: | #use cmd to silence powershell behaviour for stderr & cmd /C "git clone -q --depth=1 git://anongit.kde.org/craftmaster.git C:\CraftMaster\CraftMaster 2>&1" craft craft craft nsis craft --install-deps charm build_script: - ps: | craft --no-cache --src-dir $env:APPVEYOR_BUILD_FOLDER charm after_build: - ps: | craft --no-cache --src-dir $env:APPVEYOR_BUILD_FOLDER --package charm test_script: - ps: | craft --no-cache --src-dir $env:APPVEYOR_BUILD_FOLDER --test charm environment: matrix: - TARGET: windows-msvc2015_32-cl - TARGET: windows-mingw_32-gcc - TARGET: windows-msvc2017_64-cl APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 artifacts: - path: cache\**\.* - path: binaries\* deploy: - provider: Environment name: GammaRay Charm-1.12.0/charmtimetracker.dsc000066400000000000000000000004741331066577000166620ustar00rootroot00000000000000Format: 1.0 Source: charmtimetracker Version: 1.12.0 Binary: charmtimetracker Maintainer: Frank Osterfeld Architecture: any Build-Depends: debhelper (>=9), cdbs, cmake, libqt4-dev, libxss-dev, libqt4-sql-sqlite Files: 00000000000000000000000000000000 00000 charmtimetracker-1.11.4.tar.gz Charm-1.12.0/charmtimetracker.spec000066400000000000000000000056071331066577000170460ustar00rootroot00000000000000Name: charmtimetracker Version: 1.12.0 Release: 0 Summary: Time Tracking Application Source: %{name}-%{version}.tar.gz Url: https://github.com/KDAB/Charm Group: Productivity/Other License: GPL-2.0+ BuildRoot: %{_tmppath}/%{name}-%{version}-build Vendor: Klaralvdalens Datakonsult AB (KDAB) Packager: Klaralvdalens Datakonsult AB (KDAB) %if %{defined suse_version} BuildRequires: libqt4-devel cmake update-desktop-files Requires: libqt4-sql-sqlite %endif %if %{defined fedora} BuildRequires: gcc-c++ qt-devel cmake desktop-file-utils Requires: qt4-sqlite %endif %if %{defined rhel} BuildRequires: gcc-c++ qt-devel cmake desktop-file-utils Requires: qt4-sqlite %endif %description Charm is a program for OS X, Linux and Windows that helps to keep track of time. It is built around two major ideas - tasks, and events. Tasks are the things time is spend on, repeatedly. For example, ironing laundry is a task. The laundry done for two hours on last Tuesday is an event in that task. When doing laundry multiple times, the events will be accumulated, and can later be printed in activity reports or weekly time sheets. So in case laundry would be done for three hours on Wednesday again, the activity report for the "Ironing Laundry" task would list the event on tuesday, the event on wednesday and a total of five hours. Authors: -------- Mirko Boehm %define debug_package %{nil} %global __debug_install_post %{nil} %prep %setup -T -c %{__tar} -zxf %{SOURCE0} --strip-components=1 %build cmake . -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DCharm_VERSION=%{version} %__make %{?_smp_mflags} %install %make_install %if %{defined suse_version} %suse_update_desktop_file charmtimetracker Utility TimeUtility %endif %clean %__rm -rf "%{buildroot}" %files %defattr(-,root,root) %{_prefix}/share/doc/charmtimetracker %{_prefix}/share/icons/hicolor %{_prefix}/share/applications/charmtimetracker.desktop %{_prefix}/bin/charmtimetracker %changelog * Tue Nov 01 2016 Steffen Hansen 1.11.4 - 1.11.4 release * Tue Nov 01 2016 Steffen Hansen 1.11.3 - 1.11.3 release * Sat Apr 30 2016 Allen Winter 1.11.1 - 1.11.1 release * Mon Apr 25 2016 Allen Winter 1.11.0 - 1.11.0 release * Mon Oct 05 2015 Allen Winter 1.10.0 - 1.10.0 release * Thu Mar 26 2015 Allen Winter 1.9.0 - 1.9.0 release * Tue Jul 02 2013 Kevin Ottens 1.8.0 - 1.8.0 release * Fri Jul 27 2012 Frank Osterfeld 1.7.0 - 1.7.0 release * Thu Feb 23 2012 Mike McQuaid 1.6.0 - 1.6.0 release * Wed Apr 20 2011 Mike McQuaid 1.5.2 - Initial setup of 1.5.2 version (based on Kevin Ottens work). Charm-1.12.0/cmake/000077500000000000000000000000001331066577000137155ustar00rootroot00000000000000Charm-1.12.0/cmake/ECM/000077500000000000000000000000001331066577000143215ustar00rootroot00000000000000Charm-1.12.0/cmake/ECM/kde-modules/000077500000000000000000000000001331066577000165325ustar00rootroot00000000000000Charm-1.12.0/cmake/ECM/kde-modules/KDECMakeSettings.cmake000066400000000000000000000326451331066577000225730ustar00rootroot00000000000000#.rst: # KDECMakeSettings # ---------------- # # Changes various CMake settings to what the KDE community views as more # sensible defaults. # # It is recommended to include this module with the NO_POLICY_SCOPE flag, # otherwise you may get spurious warnings with some versions of CMake. # # It is split into three parts, which can be independently disabled if desired. # # Runtime Paths # ~~~~~~~~~~~~~ # # The default runtime path (used on Unix systems to search for # dynamically-linked libraries) is set to include the location that libraries # will be installed to (as set in LIB_INSTALL_DIR or, if the former is not set, # KDE_INSTALL_LIBDIR), and also the linker search path. # # Note that ``LIB_INSTALL_DIR`` or alternatively ``KDE_INSTALL_LIBDIR`` needs # to be set before including this module. # Typically, this is done by including the :kde-module:`KDEInstallDirs` module. # # This section can be disabled by setting ``KDE_SKIP_RPATH_SETTINGS`` to TRUE # before including this module. # # # Testing # ~~~~~~~ # # Testing is enabled by default, and an option (BUILD_TESTING) is provided for # users to control this. See the CTest module documentation in the CMake manual # for more details. # # This section can be disabled by setting ``KDE_SKIP_TEST_SETTINGS`` to TRUE # before including this module. # # # Build Settings # ~~~~~~~~~~~~~~ # # Various CMake build defaults are altered, such as searching source and build # directories for includes first and enabling automoc by default. # # This section can be disabled by setting ``KDE_SKIP_BUILD_SETTINGS`` to TRUE # before including this module. # # This section also provides an "uninstall" target that can be individually # disabled by setting ``KDE_SKIP_UNINSTALL_TARGET`` to TRUE before including # this module. # # By default on OS X, X11 and XCB related detections are disabled. However if # the need would arise to use these technologies, the detection can be enabled # by setting ``APPLE_FORCE_X11`` to ``ON``. # # A warning is printed for the developer to know that the detection is disabled on OS X. # This message can be turned off by setting ``APPLE_SUPPRESS_X11_WARNING`` to ``ON``. # # Since pre-1.0.0. # # ``ENABLE_CLAZY`` option is added (OFF by default) when clang is being used. # Turning this option on will force clang to load the clazy plugins for richer # warnings on Qt-related code. # # If clang is not being used, this won't have an effect. # See https://commits.kde.org/clazy?path=README.md # # Since 5.17.0 # # - Uninstall target functionality since 1.7.0. # - ``APPLE_FORCE_X11`` option since 5.14.0 (detecting X11 was previously the default behavior) # - ``APPLE_SUPPRESS_X11_WARNING`` option since 5.14.0 # # Translations # ~~~~~~~~~~~~ # A fetch-translations target will be set up that will download translations # for projects using l10n.kde.org. # # ``KDE_L10N_BRANCH`` will be responsible for choosing which l10n branch to use # for the translations. # # ``KDE_L10N_AUTO_TRANSLATIONS`` (OFF by default) will indicate whether translations # should be downloaded when building the project. # # Since 5.34.0 #============================================================================= # Copyright 2014 Alex Merry # Copyright 2013 Aleix Pol # Copyright 2012-2013 Stephen Kelly # Copyright 2007 Matthias Kretz # Copyright 2006-2007 Laurent Montel # Copyright 2006-2013 Alex Neundorf # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ################# RPATH handling ################################## if(NOT KDE_SKIP_RPATH_SETTINGS) # Set the default RPATH to point to useful locations, namely where the # libraries will be installed and the linker search path # First look for the old LIB_INSTALL_DIR, then fallback to newer KDE_INSTALL_LIBDIR if(NOT LIB_INSTALL_DIR) if(NOT KDE_INSTALL_LIBDIR) message(FATAL_ERROR "Neither KDE_INSTALL_LIBDIR nor LIB_INSTALL_DIR is set. Setting one is necessary for using the RPATH settings.") else() set(_abs_LIB_INSTALL_DIR "${KDE_INSTALL_LIBDIR}") endif() else() set(_abs_LIB_INSTALL_DIR "${LIB_INSTALL_DIR}") endif() if (NOT IS_ABSOLUTE "${_abs_LIB_INSTALL_DIR}") set(_abs_LIB_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR}") endif() if (UNIX) # for mac os: add install name dir in addition # check: is the rpath stuff below really required on mac os? at least it seems so to use a stock qt from qt.io if (APPLE) set(CMAKE_INSTALL_NAME_DIR ${_abs_LIB_INSTALL_DIR}) endif () # add our LIB_INSTALL_DIR to the RPATH (but only when it is not one of # the standard system link directories - such as /usr/lib on UNIX) list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${_abs_LIB_INSTALL_DIR}" _isSystemLibDir) list(FIND CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES "${_abs_LIB_INSTALL_DIR}" _isSystemCxxLibDir) list(FIND CMAKE_C_IMPLICIT_LINK_DIRECTORIES "${_abs_LIB_INSTALL_DIR}" _isSystemCLibDir) if("${_isSystemLibDir}" STREQUAL "-1" AND "${_isSystemCxxLibDir}" STREQUAL "-1" AND "${_isSystemCLibDir}" STREQUAL "-1") set(CMAKE_INSTALL_RPATH "${_abs_LIB_INSTALL_DIR}") endif() # Append directories in the linker search path (but outside the project) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) endif (UNIX) endif() ################ Testing setup #################################### find_program(APPSTREAMCLI appstreamcli) function(appstreamtest) if(APPSTREAMCLI AND NOT appstreamtest_added) set(appstreamtest_added TRUE PARENT_SCOPE) add_test(NAME appstreamtest COMMAND ${CMAKE_COMMAND} -DAPPSTREAMCLI=${APPSTREAMCLI} -DINSTALL_FILES=${CMAKE_BINARY_DIR}/install_manifest.txt -P ${CMAKE_CURRENT_LIST_DIR}/appstreamtest.cmake) else() message(STATUS "Could not set up the appstream test. appstreamcli is missing.") endif() endfunction() if(NOT KDE_SKIP_TEST_SETTINGS) # If there is a CTestConfig.cmake, include CTest. # Otherwise, there will not be any useful settings, so just # fake the functionality we care about from CTest. if (EXISTS ${CMAKE_SOURCE_DIR}/CTestConfig.cmake) include(CTest) else() option(BUILD_TESTING "Build the testing tree." ON) if(BUILD_TESTING) enable_testing() appstreamtest() endif () endif () endif() ################ Build-related settings ########################### if(NOT KDE_SKIP_BUILD_SETTINGS) # Always include srcdir and builddir in include path # This saves typing ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} in about every subdir # since cmake 2.4.0 set(CMAKE_INCLUDE_CURRENT_DIR ON) # put the include dirs which are in the source or build tree # before all other include dirs, so the headers in the sources # are prefered over the already installed ones # since cmake 2.4.1 set(CMAKE_INCLUDE_DIRECTORIES_PROJECT_BEFORE ON) # Add the src and build dir to the BUILD_INTERFACE include directories # of all targets. Similar to CMAKE_INCLUDE_CURRENT_DIR, but transitive. # Since CMake 2.8.11 set(CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE ON) # When a shared library changes, but its includes do not, don't relink # all dependencies. It is not needed. # Since CMake 2.8.11 set(CMAKE_LINK_DEPENDS_NO_SHARED ON) # Default to shared libs for KDE, if no type is explicitely given to add_library(): set(BUILD_SHARED_LIBS TRUE CACHE BOOL "If enabled, shared libs will be built by default, otherwise static libs") # Enable automoc in cmake # Since CMake 2.8.6 set(CMAKE_AUTOMOC ON) # By default, create 'GUI' executables. This can be reverted on a per-target basis # using ECMMarkNonGuiExecutable # Since CMake 2.8.8 set(CMAKE_WIN32_EXECUTABLE ON) set(CMAKE_MACOSX_BUNDLE ON) # By default, don't put a prefix on MODULE targets. add_library(MODULE) is basically for plugin targets, # and in KDE plugins don't have a prefix. set(CMAKE_SHARED_MODULE_PREFIX "") unset(EXECUTABLE_OUTPUT_PATH) unset(LIBRARY_OUTPUT_PATH) unset(CMAKE_ARCHIVE_OUTPUT_DIRECTORY) unset(CMAKE_LIBRARY_OUTPUT_DIRECTORY) unset(CMAKE_RUNTIME_OUTPUT_DIRECTORY) # under Windows, output all executables and dlls into # one common directory, and all static|import libraries and plugins # into another one. This way test executables can find their dlls # even without installation. if(WIN32) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") endif() if (APPLE) # Disable detection of X11 and related package on OS X because when using # brew or macports, X11 can be installed and thus is detected. option(APPLE_FORCE_X11 "Force enable X11 related detection on OS X" OFF) option(APPLE_SUPPRESS_X11_WARNING "Suppress X11 and related technologies search disabling warning on OS X" OFF) if(NOT APPLE_FORCE_X11) if (NOT APPLE_SUPPRESS_X11_WARNING) message(WARNING "Searching for X11 and related technologies is disabled on Apple systems. Set APPLE_FORCE_X11 to ON to change this behaviour. Set APPLE_SUPPRESS_X11_WARNING to ON to hide this warning.") endif() set(CMAKE_DISABLE_FIND_PACKAGE_X11 true) set(CMAKE_DISABLE_FIND_PACKAGE_XCB true) set(CMAKE_DISABLE_FIND_PACKAGE_Qt5X11Extras true) endif() endif() option(KDE_SKIP_UNINSTALL_TARGET "Prevent an \"uninstall\" target from being generated." OFF) if(NOT KDE_SKIP_UNINSTALL_TARGET) include("${ECM_MODULE_DIR}/ECMUninstallTarget.cmake") endif() endif() if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") option(ENABLE_CLAZY "Enable Clazy warnings" OFF) if(ENABLE_CLAZY) set(CMAKE_CXX_COMPILE_OBJECT "${CMAKE_CXX_COMPILE_OBJECT} -Xclang -load -Xclang ClangLazy${CMAKE_SHARED_LIBRARY_SUFFIX} -Xclang -add-plugin -Xclang clang-lazy") endif() endif() ################################################################### # Download translations function(_repository_name reponame dir) execute_process(COMMAND git config --get remote.origin.url OUTPUT_VARIABLE giturl RESULT_VARIABLE exitCode WORKING_DIRECTORY "${dir}") if(exitCode EQUAL 0) string(REGEX MATCHALL ".+[:\\/]([-A-Za-z\\d]+)(.git)?\\s*" "" ${giturl}) set(${reponame} ${CMAKE_MATCH_1}) endif() if(NOT ${reponame}) set(${reponame} ${CMAKE_PROJECT_NAME}) endif() set(${reponame} ${${reponame}} PARENT_SCOPE) endfunction() if(NOT EXISTS ${CMAKE_SOURCE_DIR}/po AND NOT TARGET fetch-translations) option(KDE_L10N_AUTO_TRANSLATIONS "Automatically 'make fetch-translations`" OFF) set(KDE_L10N_BRANCH "trunk" CACHE STRING "Branch from l10n.kde.org to fetch from: trunk | stable | lts | trunk_kde4 | stable_kde4") if(KDE_L10N_AUTO_TRANSLATIONS) set(_EXTRA_ARGS "ALL") else() set(_EXTRA_ARGS) endif() set(_reponame "") _repository_name(_reponame "${CMAKE_SOURCE_DIR}") add_custom_command( OUTPUT "${CMAKE_BINARY_DIR}/releaseme" COMMAND git clone --depth 1 "https://anongit.kde.org/releaseme.git" COMMENT "Fetching releaseme scripts to download translations..." ) set(_l10n_po_dir "${CMAKE_BINARY_DIR}/po") set(_l10n_poqm_dir "${CMAKE_BINARY_DIR}/poqm") if(CMAKE_VERSION VERSION_GREATER 3.2) set(extra BYPRODUCTS ${_l10n_po_dir} ${_l10n_poqm_dir}) endif() add_custom_target(fetch-translations ${_EXTRA_ARGS} COMMENT "Downloading translations for ${_reponame} branch ${KDE_L10N_BRANCH}..." COMMAND git -C "${CMAKE_BINARY_DIR}/releaseme" pull COMMAND cmake -E remove_directory ${_l10n_po_dir} COMMAND cmake -E remove_directory ${_l10n_poqm_dir} COMMAND ruby "${CMAKE_BINARY_DIR}/releaseme/fetchpo.rb" --origin ${KDE_L10N_BRANCH} --project "${_reponame}" --output-dir "${_l10n_po_dir}" --output-poqm-dir "${_l10n_poqm_dir}" "${CMAKE_CURRENT_SOURCE_DIR}" ${extra} DEPENDS "${CMAKE_BINARY_DIR}/releaseme" ) endif() Charm-1.12.0/cmake/ECM/kde-modules/KDECompilerSettings.cmake000066400000000000000000000550171331066577000233630ustar00rootroot00000000000000#.rst: # KDECompilerSettings # ------------------- # # Set useful compile and link flags for C++ (and C) code. # # Enables many more warnings than the default, and sets stricter modes # for some compiler features. By default, exceptions are disabled; # kde_target_enable_exceptions() can be used to re-enable them for a # specific target. # # NB: it is recommended to include this module with the NO_POLICY_SCOPE # flag, otherwise you may get spurious warnings with some versions of CMake. # # This module provides the following functions:: # # kde_source_files_enable_exceptions([file1 [file2 [...]]]) # # Enables exceptions for specific source files. This should not be # used on source files in a language other than C++. # # :: # # kde_target_enable_exceptions(target ) # # Enables exceptions for a specific target. This should not be used # on a target that has source files in a language other than C++. # # :: # # kde_enable_exceptions() # # Enables exceptions for C++ source files compiled for the # CMakeLists.txt file in the current directory and all subdirectories. # # Since pre-1.0.0. #============================================================================= # Copyright 2014 Alex Merry # Copyright 2013 Stephen Kelly # Copyright 2012-2013 Raphael Kubo da Costa # Copyright 2007 Matthias Kretz # Copyright 2006-2007 Laurent Montel # Copyright 2006-2013 Alex Neundorf # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ############################################################ # Toolchain minimal requirements # # Note that only compilers officially supported by Qt are # supported by this file; workarounds for older compilers # will generally not be included. See # https://qt-project.org/doc/qt-5/supported-platforms.html # and # https://community.kde.org/Frameworks/Policies#Frameworks_compiler_requirements_and_C.2B.2B11 # for more details. ############################################################ macro(_kde_compiler_min_version min_version) if ("${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS "${min_version}") message(WARNING "Version ${CMAKE_CXX_COMPILER_VERSION} of the ${CMAKE_CXX_COMPILER_ID} C++ compiler is not supported. Please use version ${min_version} or later.") endif() endmacro() if (MSVC) # MSVC_VERSION 1600 = VS 10.0 = Windows SDK 7 # See: cmake --help-variable MSVC_VERSION # and https://developer.mozilla.org/en-US/docs/Windows_SDK_versions if (${MSVC_VERSION} LESS 1600) message(WARNING "Your MSVC version (${MSVC_VERSION}) is not supported. Please use the Windows SDK version 7 or later (or Microsoft Visual Studio 2010 or later).") endif() elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if (WIN32) _kde_compiler_min_version("4.7") elseif (APPLE) # FIXME: Apple heavily modifies GCC, so checking the # GCC version on OS/X is not very useful. else() _kde_compiler_min_version("4.5") endif() elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") _kde_compiler_min_version("3.1") else() message(WARNING "${CMAKE_CXX_COMPILER_ID} is not a supported C++ compiler.") endif() ############################################################ # System API features ############################################################ # This macro is for adding definitions that affect the underlying # platform API. It makes sure that configure checks will also have # the same defines, so that the checks match compiled code. macro (_KDE_ADD_PLATFORM_DEFINITIONS) add_definitions(${ARGV}) set(CMAKE_REQUIRED_DEFINITIONS ${CMAKE_REQUIRED_DEFINITIONS} ${ARGV}) endmacro() include(CheckCXXSymbolExists) check_cxx_symbol_exists("__GLIBC__" "stdlib.h" LIBC_IS_GLIBC) if (LIBC_IS_GLIBC) # Enable everything in GNU libc. Any code using non-portable features # needs to perform feature tests, but this ensures that any such features # will be found if they exist. # # NB: we do NOT define _BSD_SOURCE, as with GNU libc that requires linking # against the -lbsd-compat library (it changes the behaviour of some # functions). This, however, means that strlcat and strlcpy are not # provided by glibc. _kde_add_platform_definitions(-D_GNU_SOURCE) endif () if (UNIX) # Enable extra API for using 64-bit file offsets on 32-bit systems. # FIXME: this is included in _GNU_SOURCE in glibc; do other libc # implementation recognize it? _kde_add_platform_definitions(-D_LARGEFILE64_SOURCE) include(CheckCXXSourceCompiles) # By default (in glibc, at least), on 32bit platforms off_t is 32 bits, # which causes a SIGXFSZ when trying to manipulate files larger than 2Gb # using libc calls (note that this issue does not occur when using QFile). check_cxx_source_compiles(" #include /* Check that off_t can represent 2**63 - 1 correctly. We can't simply define LARGE_OFF_T to be 9223372036854775807, since some C++ compilers masquerading as C compilers incorrectly reject 9223372036854775807. */ #define LARGE_OFF_T (((off_t) 1 << 62) - 1 + ((off_t) 1 << 62)) int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 && LARGE_OFF_T % 2147483647 == 1) ? 1 : -1]; int main() { return 0; } " _OFFT_IS_64BIT) if (NOT _OFFT_IS_64BIT) _kde_add_platform_definitions(-D_FILE_OFFSET_BITS=64) endif () endif() if (WIN32) # Speeds up compile times by not including everything with windows.h # See http://msdn.microsoft.com/en-us/library/windows/desktop/aa383745%28v=vs.85%29.aspx _kde_add_platform_definitions(-DWIN32_LEAN_AND_MEAN) # Target Windows Vista # This enables various bits of new API # See http://msdn.microsoft.com/en-us/library/windows/desktop/aa383745%28v=vs.85%29.aspx _kde_add_platform_definitions(-D_WIN32_WINNT=0x0600 -DWINVER=0x0600 -D_WIN32_IE=0x0600) # Use the Unicode versions of Windows API by default # See http://msdn.microsoft.com/en-us/library/windows/desktop/dd317766%28v=vs.85%29.aspx _kde_add_platform_definitions(-DUNICODE -D_UNICODE) # As stated in http://msdn.microsoft.com/en-us/library/4hwaceh6.aspx M_PI only gets defined # if _USE_MATH_DEFINES is defined, with mingw this has a similar effect as -D_GNU_SOURCE on math.h _kde_add_platform_definitions(-D_USE_MATH_DEFINES) endif() ############################################################ # Language and toolchain features ############################################################ # Pick sensible versions of the C and C++ standards. # Note that MSVC does not have equivalent flags; the features are either # supported or they are not. if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID MATCHES "Clang") # We use the C89 standard because that is what is common to all our # compilers (in particular, MSVC 2010 does not support C99) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=iso9899:1990") endif() if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Intel" AND NOT WIN32) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x") endif() # Do not merge uninitialized global variables. # This is mostly a "principle of least surprise" thing, but also # has performance benefits. # See https://www.ibm.com/developerworks/community/blogs/zTPF/entry/benefits_of_the_fnocommon_compile_option_peter_lemieszewski?lang=en # Note that this only applies to C code; C++ already behaves like this. if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID MATCHES "Clang" OR (CMAKE_C_COMPILER_ID STREQUAL "Intel" AND NOT WIN32)) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-common") endif() # Do not treat the operator name keywords and, bitand, bitor, compl, not, or and xor as synonyms as keywords. # They're not supported under Visual Studio out of the box thus using them limits the portability of code if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID MATCHES "Clang" OR (CMAKE_C_COMPILER_ID STREQUAL "Intel" AND NOT WIN32)) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-operator-names") endif() # Default to hidden visibility for symbols set(CMAKE_C_VISIBILITY_PRESET hidden) set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_VISIBILITY_INLINES_HIDDEN 1) if (POLICY CMP0063) # No sane project should be affected by CMP0063, so suppress the warnings # generated by the above visibility settings in CMake >= 3.3 cmake_policy(SET CMP0063 NEW) endif() if (UNIX AND NOT APPLE AND NOT CYGWIN) # Enable adding DT_RUNPATH, which means that LD_LIBRARY_PATH takes precedence # over the built-in rPath set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--enable-new-dtags ${CMAKE_SHARED_LINKER_FLAGS}") set(CMAKE_MODULE_LINKER_FLAGS "-Wl,--enable-new-dtags ${CMAKE_MODULE_LINKER_FLAGS}") set(CMAKE_EXE_LINKER_FLAGS "-Wl,--enable-new-dtags ${CMAKE_EXE_LINKER_FLAGS}") endif() if (CMAKE_SYSTEM_NAME STREQUAL GNU) # Enable multithreading with the pthread library # FIXME: Is this actually necessary to have here? # Can CMakeLists.txt files that require it use FindThreads.cmake # instead? set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pthread") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -pthread") endif() ############################################################ # Turn off exceptions by default # # This involves enough code to be separate from the # previous section. ############################################################ # TODO: Deal with QT_NO_EXCEPTIONS for non-gnu compilers? # This should be defined if and only if exceptions are disabled. # qglobal.h has some magic to set it when exceptions are disabled # with gcc, but other compilers are unaccounted for. # Turn off exceptions by default if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions") elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Intel" AND NOT WIN32) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions") #elseif (MSVC OR (WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "Intel")) # Exceptions appear to be disabled by default for MSVC # http://msdn.microsoft.com/en-us/library/1deeycx5.aspx # FIXME: are exceptions disabled by default for Intel? endif() macro(_kdecompilersettings_append_exception_flag VAR) if (MSVC) set(${VAR} "${${VAR}} -EHsc") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Intel") if (WIN32) set(${VAR} "${${VAR}} -EHsc") else() set(${VAR} "${${VAR}} -fexceptions") endif() elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(${VAR} "${${VAR}} -fexceptions") elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(${VAR} "${${VAR}} -fexceptions") endif() string(STRIP "${${VAR}}" ${VAR}) endmacro() function(KDE_SOURCE_FILES_ENABLE_EXCEPTIONS) foreach(source_file ${ARGV}) get_source_file_property(flags ${source_file} COMPILE_FLAGS) if(NOT flags) # If COMPILE_FLAGS is not set, get_source_file_property() sets it to # NOTFOUND, which breaks build if we concatenate anything to # the "NOTFOUND" string. # Note that NOTFOUND evaluates to False, so we do enter the if. set(flags "") endif() _kdecompilersettings_append_exception_flag(flags) set_source_files_properties(${source_file} COMPILE_FLAGS "${flags}") endforeach() endfunction() function(KDE_TARGET_ENABLE_EXCEPTIONS target mode) target_compile_options(${target} ${mode} "$<$:-EHsc>") if (WIN32) target_compile_options(${target} ${mode} "$<$:-EHsc>") else() target_compile_options(${target} ${mode} "$<$:-fexceptions>") endif() target_compile_options(${target} ${mode} "$<$,$,$>:-fexceptions>") endfunction() function(KDE_ENABLE_EXCEPTIONS) # We set CMAKE_CXX_FLAGS, rather than add_compile_options(), because # we only want to affect the compilation of C++ source files. # strip any occurrences of -DQT_NO_EXCEPTIONS; this should only be defined # if exceptions are disabled # the extra spaces mean we will not accentially mangle any other options string(REPLACE " -DQT_NO_EXCEPTIONS " " " CMAKE_CXX_FLAGS " ${CMAKE_CXX_FLAGS} ") # this option is common to several compilers, so just always remove it string(REPLACE " -fno-exceptions " " " CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") # strip undoes the extra spaces we put in above string(STRIP "${CMAKE_CXX_FLAGS}" CMAKE_CXX_FLAGS) _kdecompilersettings_append_exception_flag(CMAKE_CXX_FLAGS) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}" PARENT_SCOPE) endfunction() ############################################################ # Better diagnostics (warnings, errors) ############################################################ if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT APPLE) OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND NOT APPLE) OR (CMAKE_CXX_COMPILER_ID STREQUAL "Intel" AND NOT WIN32)) # Linker warnings should be treated as errors set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--fatal-warnings ${CMAKE_SHARED_LINKER_FLAGS}") set(CMAKE_MODULE_LINKER_FLAGS "-Wl,--fatal-warnings ${CMAKE_MODULE_LINKER_FLAGS}") # Do not allow undefined symbols, even in non-symbolic shared libraries set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--no-undefined ${CMAKE_SHARED_LINKER_FLAGS}") set(CMAKE_MODULE_LINKER_FLAGS "-Wl,--no-undefined ${CMAKE_MODULE_LINKER_FLAGS}") endif() set(_KDE_GCC_COMMON_WARNING_FLAGS "-Wall -Wextra -Wcast-align -Wchar-subscripts -Wformat-security -Wno-long-long -Wpointer-arith -Wundef") if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # -Wgnu-zero-variadic-macro-arguments (part of -pedantic) is triggered by every qCDebug() call and therefore results # in a lot of noise. This warning is only notifying us that clang is emulating the GCC behaviour # instead of the exact standard wording so we can safely ignore it set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-gnu-zero-variadic-macro-arguments") endif() if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID MATCHES "Clang") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${_KDE_GCC_COMMON_WARNING_FLAGS} -Wmissing-format-attribute -Wwrite-strings") # Make some warnings errors set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=implicit-function-declaration") endif() if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_KDE_GCC_COMMON_WARNING_FLAGS} -Wnon-virtual-dtor -Woverloaded-virtual") # Make some warnings errors set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=return-type") endif() if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 3.5)) # -Wvla: use of variable-length arrays (an extension to C++) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wvla") endif() if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5.0) OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 3.5)) include(CheckCXXCompilerFlag) check_cxx_compiler_flag(-Wdate-time HAVE_DATE_TIME) if (HAVE_DATE_TIME) # -Wdate-time: warn if we use __DATE__ or __TIME__ (we want to be able to reproduce the exact same binary) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wdate-time") endif() endif() # -w1 turns on warnings and errors # FIXME: someone needs to have a closer look at the Intel compiler options if (CMAKE_C_COMPILER_ID STREQUAL "Intel" AND NOT WIN32) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -w1 -Wpointer-arith") endif() if (CMAKE_CXX_COMPILER_ID STREQUAL "Intel" AND NOT WIN32) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -w1 -Wpointer-arith") endif() if (MSVC) # FIXME: do we not want to set the warning level up to level 3? (/W3) # Disable warnings: # C4250: 'class1' : inherits 'class2::member' via dominance set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4250") # C4251: 'identifier' : class 'type' needs to have dll-interface to be # used by clients of class 'type2' set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4251") # C4396: 'identifier' : 'function' the inline specifier cannot be used # when a friend declaration refers to a specialization of a # function template set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4396") # C4661: 'identifier' : no suitable definition provided for explicit # template instantiation request set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4661") endif() if (WIN32) # Disable deprecation warnings for some API # FIXME: do we really want this? add_definitions(-D_CRT_SECURE_NO_DEPRECATE -D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE -D_SCL_SECURE_NO_WARNINGS ) endif() if (APPLE) #full Single Unix Standard v3 (SUSv3) conformance (the Unix API) _kde_add_platform_definitions(-D_DARWIN_C_SOURCE) #Cocoa is unconditional since we only support OS X 10.6 and above _kde_add_platform_definitions(-DQT_MAC_USE_COCOA) endif() ############################################################ # Hacks # # Anything in this section should be thoroughly documented, # including what problems it is supposed to fix and in what # circumstances those problems occur. Include links to any # relevant bug reports. ############################################################ if (APPLE) # FIXME: why are these needed? The commit log is unhelpful # (it was introduced in svn path=/trunk/KDE/kdelibs/; revision=503025 - # kdelibs git commit 4e4cb9cb9a2216b63d3eabf88b8fe94ee3c898cf - # with the message "mac os x fixes for the cmake build") set (CMAKE_SHARED_LINKER_FLAGS "-single_module -multiply_defined suppress ${CMAKE_SHARED_LINKER_FLAGS}") set (CMAKE_MODULE_LINKER_FLAGS "-multiply_defined suppress ${CMAKE_MODULE_LINKER_FLAGS}") endif() if (WIN32) if (MSVC OR CMAKE_CXX_COMPILER_ID STREQUAL "Intel") # MSVC has four incompatible C runtime libraries: static (libcmt), # static debug (libcmtd), shared (msvcrt) and shared debug (msvcrtd): # see http://support.microsoft.com/kb/154753 # # By default, when you create static libraries, they are automatically # linked against either libcmt or libcmtd, and when you create shared # libraries, they are automatically linked against either msvcrt or # msvcrtd. Trying to link to both a library that links to libcmt and # library that links to mscvrt, for example, will produce a warning as # described at # http://msdn.microsoft.com/en-us/library/aa267384%28VS.60%29.aspx # and can produce link errors like # "__thiscall type_info::type_info(class type_info const &)" # (??0type_info@@AAE@ABV0@@Z) already defined in LIBCMT.lib(typinfo.obj) # # It is actually the options passed to the compiler, rather than the # linker, which control what will be linked (/MT, /MTd, /MD or /MDd), # but we can override this by telling the linker to ignore any "libcmt" # or "libcmtd" link suggestion embedded in the object files, and instead # link against the shared versions. That way, everything will link # against the same runtime library. set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/NODEFAULTLIB:libcmt /DEFAULTLIB:msvcrt ${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "/NODEFAULTLIB:libcmt /DEFAULTLIB:msvcrt ${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO}") set(CMAKE_EXE_LINKER_FLAGS_MINSIZEREL "/NODEFAULTLIB:libcmt /DEFAULTLIB:msvcrt ${CMAKE_EXE_LINKER_FLAGS_MINSIZEREL}") set(CMAKE_EXE_LINKER_FLAGS_DEBUG "/NODEFAULTLIB:libcmtd /DEFAULTLIB:msvcrtd ${CMAKE_EXE_LINKER_FLAGS_DEBUG}") endif() endif() if (MINGW AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # This was copied from the Phonon build settings, where it had the comment # "otherwise undefined symbol in phononcore.dll errors occurs", with the commit # message "set linker flag --export-all-symbols for all targets, otherwise # some depending targets could not be build" # FIXME: do our export macros not deal with this properly? set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--export-all-symbols") set (CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--export-all-symbols") endif() if (CMAKE_GENERATOR STREQUAL "Ninja" AND ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9) OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 3.5))) # Force colored warnings in Ninja's output, if the compiler has -fdiagnostics-color support. # Rationale in https://github.com/ninja-build/ninja/issues/814 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always") endif() include("${ECM_MODULE_DIR}/ECMEnableSanitizers.cmake") include("${ECM_MODULE_DIR}/ECMCoverageOption.cmake") Charm-1.12.0/cmake/ECM/kde-modules/KDEFrameworkCompilerSettings.cmake000066400000000000000000000060631331066577000252360ustar00rootroot00000000000000#.rst: # KDEFrameworkCompilerSettings # ---------------------------- # # Set stricter compile and link flags for KDE Frameworks modules. # # The KDECompilerSettings module is included and, in addition, various # defines that affect the Qt libraries are set to enforce certain # conventions. # # For example, constructions like QString("foo") are prohibited, instead # forcing the use of QLatin1String or QStringLiteral, and some # Qt-defined keywords like signals and slots will not be defined. # # NB: it is recommended to include this module with the NO_POLICY_SCOPE # flag, otherwise you may get spurious warnings with some versions of CMake. # # Since pre-1.0.0. #============================================================================= # Copyright 2013 Albert Astals Cid # Copyright 2007 Matthias Kretz # Copyright 2006-2007 Laurent Montel # Copyright 2006-2013 Alex Neundorf # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. include(KDECompilerSettings NO_POLICY_SCOPE) add_definitions(-DQT_NO_CAST_TO_ASCII -DQT_NO_CAST_FROM_ASCII -DQT_NO_URL_CAST_FROM_STRING -DQT_NO_CAST_FROM_BYTEARRAY -DQT_NO_SIGNALS_SLOTS_KEYWORDS -DQT_USE_FAST_OPERATOR_PLUS -DQT_USE_QSTRINGBUILDER ) if (CMAKE_BUILD_TYPE STREQUAL "Debug") add_definitions(-DQT_STRICT_ITERATORS) endif() if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic") endif() if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if (NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS "5.0.0") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsuggest-override" ) endif() endif() Charm-1.12.0/cmake/ECM/kde-modules/KDEInstallDirs.cmake000066400000000000000000000647731331066577000223310ustar00rootroot00000000000000#.rst: # KDEInstallDirs # -------------- # # Define KDE standard installation directories. # # Note that none of the variables defined by this module provide any # information about the location of already-installed KDE software. # # Inclusion of this module defines the following variables: # # ``KDE_INSTALL_`` # destination for files of a given type # ``KDE_INSTALL_FULL_`` # corresponding absolute path # # where ```` is one of (default values in parentheses and alternative, # deprecated variable name in square brackets): # # ``BUNDLEDIR`` # application bundles (``/Applications/KDE``) [``BUNDLE_INSTALL_DIR``] # ``EXECROOTDIR`` # executables and libraries (````) [``EXEC_INSTALL_PREFIX``] # ``BINDIR`` # user executables (``EXECROOTDIR/bin``) [``BIN_INSTALL_DIR``] # ``SBINDIR`` # system admin executables (``EXECROOTDIR/sbin``) [``SBIN_INSTALL_DIR``] # ``LIBDIR`` # object code libraries (``EXECROOTDIR/lib``, ``EXECROOTDIR/lib64`` or # ``EXECROOTDIR/lib/`` variables (or their ``CMAKE_INSTALL_`` or # deprecated counterparts) may be passed to the DESTINATION options of # ``install()`` commands for the corresponding file type. They are set in the # CMake cache, and so the defaults above can be overridden by users. # # Note that the ``KDE_INSTALL_``, ``CMAKE_INSTALL_`` or deprecated # form of the variable can be changed using CMake command line variable # definitions; in either case, all forms of the variable will be affected. The # effect of passing multiple forms of the same variable on the command line # (such as ``KDE_INSTALL_BINDIR`` and ``CMAKE_INSTALL_BINDIR`` is undefined. # # The variable ``KDE_INSTALL_TARGETS_DEFAULT_ARGS`` is also defined (along with # the deprecated form ``INSTALL_TARGETS_DEFAULT_ARGS``). This should be used # when libraries or user-executable applications are installed, in the # following manner: # # .. code-block:: cmake # # install(TARGETS mylib myapp ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) # # It MUST NOT be used for installing plugins, system admin executables or # executables only intended for use internally by other code. Those should use # ``KDE_INSTALL_PLUGINDIR``, ``KDE_INSTALL_SBINDIR`` or # ``KDE_INSTALL_LIBEXECDIR`` respectively. # # Additionally, ``CMAKE_INSTALL_DEFAULT_COMPONENT_NAME`` will be set to # ``${PROJECT_NAME}`` to provide a sensible default for this CMake option. # # Note that mixing absolute and relative paths, particularly for ``BINDIR``, # ``LIBDIR`` and ``INCLUDEDIR``, can cause issues with exported targets. Given # that the default values for these are relative paths, relative paths should # be used on the command line when possible (eg: use # ``-DKDE_INSTALL_LIBDIR=lib64`` instead of # ``-DKDE_INSTALL_LIBDIR=/usr/lib/lib64`` to override the library directory). # # Since pre-1.0.0. # # NB: The variables starting ``KDE_INSTALL_`` are available since 1.6.0, # unless otherwise noted with the variable. #============================================================================= # Copyright 2014-2015 Alex Merry # Copyright 2013 Stephen Kelly # Copyright 2012 David Faure # Copyright 2007 Matthias Kretz # Copyright 2006-2007 Laurent Montel # Copyright 2006-2013 Alex Neundorf # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Figure out what the default install directory for libraries should be. # This is based on the logic in GNUInstallDirs, but simplified (the # GNUInstallDirs code deals with re-configuring, but that is dealt with # by the _define_* macros in this module). set(_LIBDIR_DEFAULT "lib") # Override this default 'lib' with 'lib64' iff: # - we are on a Linux, kFreeBSD or Hurd system but NOT cross-compiling # - we are NOT on debian # - we are on a 64 bits system # reason is: amd64 ABI: http://www.x86-64.org/documentation/abi.pdf # For Debian with multiarch, use 'lib/${CMAKE_LIBRARY_ARCHITECTURE}' if # CMAKE_LIBRARY_ARCHITECTURE is set (which contains e.g. "i386-linux-gnu" # See http://wiki.debian.org/Multiarch if((CMAKE_SYSTEM_NAME MATCHES "Linux|kFreeBSD" OR CMAKE_SYSTEM_NAME STREQUAL "GNU") AND NOT CMAKE_CROSSCOMPILING) if (EXISTS "/etc/debian_version") # is this a debian system ? if(CMAKE_LIBRARY_ARCHITECTURE) set(_LIBDIR_DEFAULT "lib/${CMAKE_LIBRARY_ARCHITECTURE}") endif() else() # not debian, rely on CMAKE_SIZEOF_VOID_P: if(NOT DEFINED CMAKE_SIZEOF_VOID_P) message(AUTHOR_WARNING "Unable to determine default LIB_INSTALL_LIBDIR directory because no target architecture is known. " "Please enable at least one language before including KDEInstallDirs.") else() if("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") set(_LIBDIR_DEFAULT "lib64") endif() endif() endif() endif() set(_gnu_install_dirs_vars BINDIR SBINDIR LIBEXECDIR SYSCONFDIR SHAREDSTATEDIR LOCALSTATEDIR LIBDIR INCLUDEDIR OLDINCLUDEDIR DATAROOTDIR DATADIR INFODIR LOCALEDIR MANDIR DOCDIR) # Macro for variables that are relative to another variable. We store an empty # value in the cache (for documentation/GUI cache editor purposes), and store # the default value in a local variable. If the cache variable is ever set to # something non-empty, the local variable will no longer be set. However, if # the cache variable remains (or is set to be) empty, the value will be # relative to that of the parent variable. # # varname: the variable name suffix (eg: BINDIR for KDE_INSTALL_BINDIR) # parent: the variable suffix of the variable this is relative to # (eg: DATAROOTDIR for KDE_INSTALL_DATAROOTDIR) # subdir: the path of the default value of KDE_INSTALL_${varname} # relative to KDE_INSTALL_${parent}: no leading / # docstring: documentation about the variable (not including the default value) # oldstylename (optional): the old-style name of the variable macro(_define_relative varname parent subdir docstring) set(_oldstylename) if(NOT KDE_INSTALL_DIRS_NO_DEPRECATED AND ${ARGC} GREATER 4) set(_oldstylename "${ARGV4}") endif() set(_cmakename) if(NOT KDE_INSTALL_DIRS_NO_CMAKE_VARIABLES) list(FIND _gnu_install_dirs_vars "${varname}" _list_offset) set(_cmakename_is_deprecated FALSE) if(NOT KDE_INSTALL_DIRS_NO_DEPRECATED OR NOT _list_offset EQUAL -1) set(_cmakename CMAKE_INSTALL_${varname}) if(_list_offset EQUAL -1) set(_cmakename_is_deprecated TRUE) endif() endif() endif() # Surprisingly complex logic to deal with joining paths. # Note that we cannot use arg vars directly in if() because macro args are # not proper variables. set(_parent "${parent}") set(_subdir "${subdir}") if(_parent AND _subdir) set(_docpath "${_parent}/${_subdir}") if(KDE_INSTALL_${_parent}) set(_realpath "${KDE_INSTALL_${_parent}}/${_subdir}") else() set(_realpath "${_subdir}") endif() elseif(_parent) set(_docpath "${_parent}") set(_realpath "${KDE_INSTALL_${_parent}}") else() set(_docpath "${_subdir}") set(_realpath "${_subdir}") endif() if(KDE_INSTALL_${varname}) # make sure the cache documentation is set correctly get_property(_iscached CACHE KDE_INSTALL_${varname} PROPERTY VALUE SET) if (_iscached) # make sure the docs are still set if it was passed on the command line set_property(CACHE KDE_INSTALL_${varname} PROPERTY HELPSTRING "${docstring} (${_docpath})") # make sure the type is correct if it was passed on the command line set_property(CACHE KDE_INSTALL_${varname} PROPERTY TYPE PATH) endif() elseif(${_oldstylename}) if(NOT CMAKE_VERSION VERSION_LESS 3.0.0) message(DEPRECATION "${_oldstylename} is deprecated, use KDE_INSTALL_${varname} instead.") endif() # The old name was given (probably on the command line): move # it to the new name set(KDE_INSTALL_${varname} "${${_oldstylename}}" CACHE PATH "${docstring} (${_docpath})" FORCE) elseif(${_cmakename}) if(_cmakename_is_deprecated AND NOT CMAKE_VERSION VERSION_LESS 3.0.0) message(DEPRECATION "${_cmakename} is deprecated, use KDE_INSTALL_${varname} instead.") endif() # The CMAKE_ name was given (probably on the command line): move # it to the new name set(KDE_INSTALL_${varname} "${${_cmakename}}" CACHE PATH "${docstring} (${_docpath})" FORCE) else() # insert an empty value into the cache, indicating the default # should be used (including compatibility vars above) set(KDE_INSTALL_${varname} "" CACHE PATH "${docstring} (${_docpath})") set(KDE_INSTALL_${varname} "${_realpath}") endif() mark_as_advanced(KDE_INSTALL_${varname}) if(NOT IS_ABSOLUTE ${KDE_INSTALL_${varname}}) set(KDE_INSTALL_FULL_${varname} "${CMAKE_INSTALL_PREFIX}/${KDE_INSTALL_${varname}}") else() set(KDE_INSTALL_FULL_${varname} "${KDE_INSTALL_${varname}}") endif() # Override compatibility vars at runtime, even though we don't touch # them in the cache; this way, we keep the variables in sync where # KDEInstallDirs is included, but don't interfere with, say, # GNUInstallDirs in a parallel part of the CMake tree. if(_cmakename) set(${_cmakename} "${KDE_INSTALL_${varname}}") set(CMAKE_INSTALL_FULL_${varname} "${KDE_INSTALL_FULL_${varname}}") endif() if(_oldstylename) set(${_oldstylename} "${KDE_INSTALL_${varname}}") endif() endmacro() # varname: the variable name suffix (eg: BINDIR for KDE_INSTALL_BINDIR) # dir: the relative path of the default value of KDE_INSTALL_${varname} # relative to CMAKE_INSTALL_PREFIX: no leading / # docstring: documentation about the variable (not including the default value) # oldstylename (optional): the old-style name of the variable macro(_define_absolute varname dir docstring) _define_relative("${varname}" "" "${dir}" "${docstring}" ${ARGN}) endmacro() macro(_define_non_cache varname value) set(KDE_INSTALL_${varname} "${value}") if(NOT IS_ABSOLUTE ${KDE_INSTALL_${varname}}) set(KDE_INSTALL_FULL_${varname} "${CMAKE_INSTALL_PREFIX}/${KDE_INSTALL_${varname}}") else() set(KDE_INSTALL_FULL_${varname} "${KDE_INSTALL_${varname}}") endif() if(NOT KDE_INSTALL_DIRS_NO_CMAKE_VARIABLES) list(FIND _gnu_install_dirs_vars "${varname}" _list_offset) if(NOT KDE_INSTALL_DIRS_NO_DEPRECATED OR NOT _list_offset EQUAL -1) set(CMAKE_INSTALL_${varname} "${KDE_INSTALL_${varname}}") set(CMAKE_INSTALL_FULL_${varname} "${KDE_INSTALL_FULL_${varname}}") endif() endif() endmacro() if(APPLE) _define_absolute(BUNDLEDIR "/Applications/KDE" "application bundles" BUNDLE_INSTALL_DIR) endif() _define_absolute(EXECROOTDIR "" "executables and libraries" EXEC_INSTALL_PREFIX) _define_relative(BINDIR EXECROOTDIR "bin" "user executables" BIN_INSTALL_DIR) _define_relative(SBINDIR EXECROOTDIR "sbin" "system admin executables" SBIN_INSTALL_DIR) _define_relative(LIBDIR EXECROOTDIR "${_LIBDIR_DEFAULT}" "object code libraries" LIB_INSTALL_DIR) if(WIN32) _define_relative(LIBEXECDIR BINDIR "" "executables for internal use by programs and libraries" LIBEXEC_INSTALL_DIR) _define_non_cache(LIBEXECDIR_KF5 "${CMAKE_INSTALL_LIBEXECDIR}") else() _define_relative(LIBEXECDIR LIBDIR "libexec" "executables for internal use by programs and libraries" LIBEXEC_INSTALL_DIR) _define_non_cache(LIBEXECDIR_KF5 "${CMAKE_INSTALL_LIBEXECDIR}/kf5") endif() if(NOT KDE_INSTALL_DIRS_NO_DEPRECATED) set(KF5_LIBEXEC_INSTALL_DIR "${CMAKE_INSTALL_LIBEXECDIR_KF5}") endif() _define_relative(CMAKEPACKAGEDIR LIBDIR "cmake" "CMake packages, including config files" CMAKECONFIG_INSTALL_PREFIX) include("${ECM_MODULE_DIR}/ECMQueryQmake.cmake") set(_default_KDE_INSTALL_USE_QT_SYS_PATHS OFF) if(NOT DEFINED KDE_INSTALL_USE_QT_SYS_PATHS) query_qmake(qt_install_prefix_dir QT_INSTALL_PREFIX) if(qt_install_prefix_dir STREQUAL "${CMAKE_INSTALL_PREFIX}") message(STATUS "Installing in the same prefix as Qt, adopting their path scheme.") set(_default_KDE_INSTALL_USE_QT_SYS_PATHS ON) endif() endif() option (KDE_INSTALL_USE_QT_SYS_PATHS "Install mkspecs files, QCH files for Qt-based libs, Plugins and Imports to the Qt 5 install dir" "${_default_KDE_INSTALL_USE_QT_SYS_PATHS}") if(KDE_INSTALL_USE_QT_SYS_PATHS) # Qt-specific vars query_qmake(qt_plugins_dir QT_INSTALL_PLUGINS) _define_absolute(QTPLUGINDIR ${qt_plugins_dir} "Qt plugins" QT_PLUGIN_INSTALL_DIR) query_qmake(qt_imports_dir QT_INSTALL_IMPORTS) _define_absolute(QTQUICKIMPORTSDIR ${qt_imports_dir} "QtQuick1 imports" IMPORTS_INSTALL_DIR) query_qmake(qt_qml_dir QT_INSTALL_QML) _define_absolute(QMLDIR ${qt_qml_dir} "QtQuick2 imports" QML_INSTALL_DIR) else() _define_relative(QTPLUGINDIR LIBDIR "plugins" "Qt plugins" QT_PLUGIN_INSTALL_DIR) _define_relative(QTQUICKIMPORTSDIR QTPLUGINDIR "imports" "QtQuick1 imports" IMPORTS_INSTALL_DIR) _define_relative(QMLDIR LIBDIR "qml" "QtQuick2 imports" QML_INSTALL_DIR) endif() _define_relative(PLUGINDIR QTPLUGINDIR "" "Plugins" PLUGIN_INSTALL_DIR) _define_absolute(INCLUDEDIR "include" "C and C++ header files" INCLUDE_INSTALL_DIR) _define_non_cache(INCLUDEDIR_KF5 "${CMAKE_INSTALL_INCLUDEDIR}/KF5") if(NOT KDE_INSTALL_DIRS_NO_DEPRECATED) set(KF5_INCLUDE_INSTALL_DIR "${CMAKE_INSTALL_INCLUDEDIR_KF5}") endif() _define_absolute(LOCALSTATEDIR "var" "modifiable single-machine data") _define_absolute(SHAREDSTATEDIR "com" "modifiable architecture-independent data") if (WIN32) _define_relative(DATAROOTDIR BINDIR "data" "read-only architecture-independent data root" SHARE_INSTALL_PREFIX) else() _define_absolute(DATAROOTDIR "share" "read-only architecture-independent data root" SHARE_INSTALL_PREFIX) endif() _define_relative(DATADIR DATAROOTDIR "" "read-only architecture-independent data" DATA_INSTALL_DIR) _define_non_cache(DATADIR_KF5 "${CMAKE_INSTALL_DATADIR}/kf5") if(NOT KDE_INSTALL_DIRS_NO_DEPRECATED) set(KF5_DATA_INSTALL_DIR "${CMAKE_INSTALL_DATADIR_KF5}") endif() # Qt-specific data vars if(KDE_INSTALL_USE_QT_SYS_PATHS) query_qmake(qt_docs_dir QT_INSTALL_DOCS) _define_absolute(QTQCHDIR ${qt_docs_dir} "documentation bundles in QCH format for Qt-extending libraries") else() _define_relative(QTQCHDIR DATAROOTDIR "doc/qch" "documentation bundles in QCH format for Qt-extending libraries") endif() # KDE Framework-specific things _define_relative(DOCBUNDLEDIR DATAROOTDIR "doc/HTML" "documentation bundles generated using kdoctools" HTML_INSTALL_DIR) _define_relative(KCFGDIR DATAROOTDIR "config.kcfg" "kconfig description files" KCFG_INSTALL_DIR) _define_relative(KCONFUPDATEDIR DATAROOTDIR "kconf_update" "kconf_update scripts" KCONF_UPDATE_INSTALL_DIR) _define_relative(KSERVICES5DIR DATAROOTDIR "kservices5" "services for KDE Frameworks 5" SERVICES_INSTALL_DIR) _define_relative(KSERVICETYPES5DIR DATAROOTDIR "kservicetypes5" "service types for KDE Frameworks 5" SERVICETYPES_INSTALL_DIR) _define_relative(KNOTIFY5RCDIR DATAROOTDIR "knotifications5" "knotify description files" KNOTIFYRC_INSTALL_DIR) _define_relative(KXMLGUI5DIR DATAROOTDIR "kxmlgui5" "kxmlgui .rc files" KXMLGUI_INSTALL_DIR) _define_relative(KTEMPLATESDIR DATAROOTDIR "kdevappwizard/templates" "Kapptemplate and Kdevelop templates") # Cross-desktop or other system things _define_relative(ICONDIR DATAROOTDIR "icons" "icons" ICON_INSTALL_DIR) _define_relative(LOCALEDIR DATAROOTDIR "locale" "knotify description files" LOCALE_INSTALL_DIR) _define_relative(SOUNDDIR DATAROOTDIR "sounds" "sound files" SOUND_INSTALL_DIR) _define_relative(TEMPLATEDIR DATAROOTDIR "templates" "templates" TEMPLATES_INSTALL_DIR) _define_relative(WALLPAPERDIR DATAROOTDIR "wallpapers" "desktop wallpaper images" WALLPAPER_INSTALL_DIR) _define_relative(APPDIR DATAROOTDIR "applications" "application desktop files" XDG_APPS_INSTALL_DIR) _define_relative(DESKTOPDIR DATAROOTDIR "desktop-directories" "desktop directories" XDG_DIRECTORY_INSTALL_DIR) _define_relative(MIMEDIR DATAROOTDIR "mime/packages" "mime description files" XDG_MIME_INSTALL_DIR) _define_relative(METAINFODIR DATAROOTDIR "metainfo" "AppStream component metadata") _define_relative(QCHDIR DATAROOTDIR "doc/qch" "documentation bundles in QCH format") _define_relative(MANDIR DATAROOTDIR "man" "man documentation" MAN_INSTALL_DIR) _define_relative(INFODIR DATAROOTDIR "info" "info documentation") _define_relative(DBUSDIR DATAROOTDIR "dbus-1" "D-Bus") _define_relative(DBUSINTERFACEDIR DBUSDIR "interfaces" "D-Bus interfaces" DBUS_INTERFACES_INSTALL_DIR) _define_relative(DBUSSERVICEDIR DBUSDIR "services" "D-Bus session services" DBUS_SERVICES_INSTALL_DIR) _define_relative(DBUSSYSTEMSERVICEDIR DBUSDIR "system-services" "D-Bus system services" DBUS_SYSTEM_SERVICES_INSTALL_DIR) set(_default_sysconf_dir "etc") if (CMAKE_INSTALL_PREFIX STREQUAL "/usr") set(_default_sysconf_dir "/etc") endif() _define_absolute(SYSCONFDIR "${_default_sysconf_dir}" "read-only single-machine data" SYSCONF_INSTALL_DIR) _define_relative(CONFDIR SYSCONFDIR "xdg" "application configuration files" CONFIG_INSTALL_DIR) _define_relative(AUTOSTARTDIR CONFDIR "autostart" "autostart files" AUTOSTART_INSTALL_DIR) set(_mixed_core_path_styles FALSE) if (IS_ABSOLUTE "${KDE_INSTALL_BINDIR}") if (NOT IS_ABSOLUTE "${KDE_INSTALL_LIBDIR}" OR NOT IS_ABSOLUTE "${KDE_INSTALL_INCLUDEDIR}") set(_mixed_core_path_styles ) endif() else() if (IS_ABSOLUTE "${KDE_INSTALL_LIBDIR}" OR IS_ABSOLUTE "${KDE_INSTALL_INCLUDEDIR}") set(_mixed_core_path_styles TRUE) endif() endif() if (_mixed_core_path_styles) message(WARNING "KDE_INSTALL_BINDIR, KDE_INSTALL_LIBDIR and KDE_INSTALL_INCLUDEDIR should either all be absolute paths or all be relative paths.") endif() # For more documentation see above. # Later on it will be possible to extend this for installing OSX frameworks # The COMPONENT Devel argument has the effect that static libraries belong to the # "Devel" install component. If we use this also for all install() commands # for header files, it will be possible to install # -everything: make install OR cmake -P cmake_install.cmake # -only the development files: cmake -DCOMPONENT=Devel -P cmake_install.cmake # -everything except the development files: cmake -DCOMPONENT=Unspecified -P cmake_install.cmake # This can then also be used for packaging with cpack. # FIXME: why is INCLUDES (only) set for ARCHIVE targets? set(KDE_INSTALL_TARGETS_DEFAULT_ARGS RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT Devel INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" ) set(KF5_INSTALL_TARGETS_DEFAULT_ARGS RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT Devel INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR_KF5}" ) # on the Mac support an extra install directory for application bundles if(APPLE) set(KDE_INSTALL_TARGETS_DEFAULT_ARGS ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} BUNDLE DESTINATION "${BUNDLE_INSTALL_DIR}" ) set(KF5_INSTALL_TARGETS_DEFAULT_ARGS ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} BUNDLE DESTINATION "${BUNDLE_INSTALL_DIR}" ) endif() if(NOT KDE_INSTALL_DIRS_NO_DEPRECATED) set(INSTALL_TARGETS_DEFAULT_ARGS ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) endif() # new in cmake 2.8.9: this is used for all installed files which do not have a component set # so set the default component name to the name of the project, if a project name has been set: if(NOT "${PROJECT_NAME}" STREQUAL "Project") set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME "${PROJECT_NAME}") endif() Charm-1.12.0/cmake/ECM/modules/000077500000000000000000000000001331066577000157715ustar00rootroot00000000000000Charm-1.12.0/cmake/ECM/modules/ECMCoverageOption.cmake000066400000000000000000000044341331066577000222510ustar00rootroot00000000000000#.rst: # ECMCoverageOption # -------------------- # # Allow users to easily enable GCov code coverage support. # # Code coverage allows you to check how much of your codebase is covered by # your tests. This module makes it easy to build with support for # `GCov `_. # # When this module is included, a ``BUILD_COVERAGE`` option is added (default # OFF). Turning this option on enables GCC's coverage instrumentation, and # links against ``libgcov``. # # Note that this will probably break the build if you are not using GCC. # # Since 1.3.0. #============================================================================= # Copyright 2014 Aleix Pol Gonzalez # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. option(BUILD_COVERAGE "Build the project with gcov support" OFF) if(BUILD_COVERAGE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lgcov") endif() Charm-1.12.0/cmake/ECM/modules/ECMEnableSanitizers.cmake000066400000000000000000000147631331066577000225750ustar00rootroot00000000000000#.rst: # ECMEnableSanitizers # ------------------- # # Enable compiler sanitizer flags. # # The following sanitizers are supported: # # - Address Sanitizer # - Memory Sanitizer # - Thread Sanitizer # - Leak Sanitizer # - Undefined Behaviour Sanitizer # # All of them are implemented in Clang, depending on your version, and # there is an work in progress in GCC, where some of them are currently # implemented. # # This module will check your current compiler version to see if it # supports the sanitizers that you want to enable # # Usage # ===== # # Simply add:: # # include(ECMEnableSanitizers) # # to your ``CMakeLists.txt``. Note that this module is included in # KDECompilerSettings, so projects using that module do not need to also # include this one. # # The sanitizers are not enabled by default. Instead, you must set # ``ECM_ENABLE_SANITIZERS`` (either in your ``CMakeLists.txt`` or on the # command line) to a semicolon-separated list of sanitizers you wish to enable. # The options are: # # - address # - memory # - thread # - leak # - undefined # # The sanitizers "address", "memory" and "thread" are mutually exclusive. You # cannot enable two of them in the same build. # # "leak" requires the "address" sanitizer. # # .. note:: # # To reduce the overhead induced by the instrumentation of the sanitizers, it # is advised to enable compiler optimizations (``-O1`` or higher). # # Example # ======= # # This is an example of usage:: # # mkdir build # cd build # cmake -DECM_ENABLE_SANITIZERS='address;leak;undefined' .. # # .. note:: # # Most of the sanitizers will require Clang. To enable it, use:: # # -DCMAKE_CXX_COMPILER=clang++ # # Since 1.3.0. #============================================================================= # Copyright 2014 Mathieu Tarral # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # MACRO check_compiler_version #----------------------------- macro (check_compiler_version gcc_required_version clang_required_version) if ( ( CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS ${gcc_required_version} ) OR ( CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS ${clang_required_version} ) ) # error ! message(FATAL_ERROR "You ask to enable the sanitizer ${CUR_SANITIZER}, but your compiler ${CMAKE_CXX_COMPILER_ID} version ${CMAKE_CXX_COMPILER_VERSION} does not support it ! You should use at least GCC ${gcc_required_version} or Clang ${clang_required_version} (99.99 means not implemented yet)") endif () endmacro () # MACRO check_compiler_support #------------------------------ macro (enable_sanitizer_flags sanitize_option) if (${sanitize_option} MATCHES "address") check_compiler_version("4.8" "3.1") set(XSAN_COMPILE_FLAGS "-fsanitize=address -fno-omit-frame-pointer -fno-optimize-sibling-calls") set(XSAN_LINKER_FLAGS "asan") elseif (${sanitize_option} MATCHES "thread") check_compiler_version("4.8" "3.1") set(XSAN_COMPILE_FLAGS "-fsanitize=thread") set(XSAN_LINKER_FLAGS "tsan") elseif (${sanitize_option} MATCHES "memory") check_compiler_version("99.99" "3.1") set(XSAN_COMPILE_FLAGS "-fsanitize=memory") elseif (${sanitize_option} MATCHES "leak") check_compiler_version("4.9" "3.4") set(XSAN_COMPILE_FLAGS "-fsanitize=leak") set(XSAN_LINKER_FLAGS "lsan") elseif (${sanitize_option} MATCHES "undefined") check_compiler_version("4.9" "3.1") set(XSAN_COMPILE_FLAGS "-fsanitize=undefined -fno-omit-frame-pointer -fno-optimize-sibling-calls") else () message(FATAL_ERROR "Compiler sanitizer option \"${sanitize_option}\" not supported.") endif () endmacro () if (ECM_ENABLE_SANITIZERS) if (CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") # for each element of the ECM_ENABLE_SANITIZERS list foreach ( CUR_SANITIZER ${ECM_ENABLE_SANITIZERS} ) # lowercase filter string(TOLOWER ${CUR_SANITIZER} CUR_SANITIZER) # check option and enable appropriate flags enable_sanitizer_flags ( ${CUR_SANITIZER} ) # TODO: GCC will not link pthread library if enabled ASan if(CMAKE_C_COMPILER_ID MATCHES "Clang") set( CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${XSAN_COMPILE_FLAGS}" ) endif() set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${XSAN_COMPILE_FLAGS}" ) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") link_libraries(${XSAN_LINKER_FLAGS}) endif() if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") string(REPLACE "-Wl,--no-undefined" "" CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS}") string(REPLACE "-Wl,--no-undefined" "" CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS}") endif () endforeach() else() message(STATUS "Tried to enable sanitizers (-DECM_ENABLE_SANITIZERS=${ECM_ENABLE_SANITIZERS}), \ but compiler (${CMAKE_CXX_COMPILER_ID}) does not have sanitizer support") endif() endif() Charm-1.12.0/cmake/ECM/modules/ECMInstallIcons.cmake000066400000000000000000000304771331066577000217350ustar00rootroot00000000000000#.rst: # ECMInstallIcons # --------------- # # Installs icons, sorting them into the correct directories according to the # FreeDesktop.org icon naming specification. # # :: # # ecm_install_icons(ICONS [ [...]] # DESTINATION # [LANG ] # [THEME ]) # # The given icons, whose names must match the pattern:: # # --. # # will be installed to the appropriate subdirectory of DESTINATION according to # the FreeDesktop.org icon naming scheme. By default, they are installed to the # "hicolor" theme, but this can be changed using the THEME argument. If the # icons are localized, the LANG argument can be used to install them in a # locale-specific directory. # # ```` is a numeric pixel size (typically 16, 22, 32, 48, 64, 128 or 256) # or ``sc`` for scalable (SVG) files, ```` is one of the standard # FreeDesktop.org icon groups (actions, animations, apps, categories, devices, # emblems, emotes, intl, mimetypes, places, status) and ```` is one of # ``.png``, ``.mng`` or ``.svgz``. # # The typical installation directory is ``share/icons``. # # .. code-block:: cmake # # ecm_install_icons(ICONS 22-actions-menu_new.png # DESTINATION share/icons) # # The above code will install the file ``22-actions-menu_new.png`` as # ``${CMAKE_INSTALL_PREFIX}/share/icons//22x22/actions/menu_new.png`` # # Users of the :kde-module:`KDEInstallDirs` module would normally use # ``${ICON_INSTALL_DIR}`` as the DESTINATION, while users of the GNUInstallDirs # module should use ``${CMAKE_INSTALL_DATAROOTDIR}/icons``. # # An old form of arguments will also be accepted:: # # ecm_install_icons( []) # # This matches files named like:: # # --. # # where ```` is one of # * ``hi`` for hicolor # * ``lo`` for locolor # * ``cr`` for the Crystal icon theme # * ``ox`` for the Oxygen icon theme # * ``br`` for the Breeze icon theme # # With this syntax, the file ``hi22-actions-menu_new.png`` would be installed # into ``/hicolor/22x22/actions/menu_new.png`` # # Since pre-1.0.0. #============================================================================= # Copyright 2014 Alex Merry # Copyright 2013 David Edmundson # Copyright 2008 Chusslove Illich # Copyright 2006 Alex Neundorf # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. include(CMakeParseArguments) # A "map" of short type names to the directories. # Unknown names produce a warning. set(_ECM_ICON_GROUP_mimetypes "mimetypes") set(_ECM_ICON_GROUP_places "places") set(_ECM_ICON_GROUP_devices "devices") set(_ECM_ICON_GROUP_apps "apps") set(_ECM_ICON_GROUP_actions "actions") set(_ECM_ICON_GROUP_categories "categories") set(_ECM_ICON_GROUP_status "status") set(_ECM_ICON_GROUP_emblems "emblems") set(_ECM_ICON_GROUP_emotes "emotes") set(_ECM_ICON_GROUP_animations "animations") set(_ECM_ICON_GROUP_intl "intl") # For the "compatibility" syntax: a "map" of short theme names to the theme # directory set(_ECM_ICON_THEME_br "breeze") set(_ECM_ICON_THEME_ox "oxygen") set(_ECM_ICON_THEME_cr "crystalsvg") set(_ECM_ICON_THEME_lo "locolor") set(_ECM_ICON_THEME_hi "hicolor") macro(_ecm_install_icons_v1 _defaultpath) # the l10n-subdir if language given as second argument (localized icon) set(_lang ${ARGV1}) if(_lang) set(_l10n_SUBDIR l10n/${_lang}) else() set(_l10n_SUBDIR ".") endif() set(_themes) # first the png icons file(GLOB _icons *.png) foreach (_current_ICON ${_icons} ) # since CMake 2.6 regex matches are stored in special variables CMAKE_MATCH_x, if it didn't match, they are empty string(REGEX MATCH "^.*/([a-zA-Z]+)([0-9]+)\\-([a-z]+)\\-(.+\\.png)$" _dummy "${_current_ICON}") set(_type "${CMAKE_MATCH_1}") set(_size "${CMAKE_MATCH_2}") set(_group "${CMAKE_MATCH_3}") set(_name "${CMAKE_MATCH_4}") set(_theme_GROUP ${_ECM_ICON_THEME_${_type}}) if( _theme_GROUP) list(APPEND _themes "${_theme_GROUP}") _ECM_ADD_ICON_INSTALL_RULE(${CMAKE_CURRENT_BINARY_DIR}/install_icons.cmake ${_defaultpath}/${_theme_GROUP}/${_size}x${_size} ${_group} ${_current_ICON} ${_name} ${_l10n_SUBDIR}) endif() endforeach (_current_ICON) # mng icons file(GLOB _icons *.mng) foreach (_current_ICON ${_icons} ) # since CMake 2.6 regex matches are stored in special variables CMAKE_MATCH_x, if it didn't match, they are empty string(REGEX MATCH "^.*/([a-zA-Z]+)([0-9]+)\\-([a-z]+)\\-(.+\\.mng)$" _dummy "${_current_ICON}") set(_type "${CMAKE_MATCH_1}") set(_size "${CMAKE_MATCH_2}") set(_group "${CMAKE_MATCH_3}") set(_name "${CMAKE_MATCH_4}") set(_theme_GROUP ${_ECM_ICON_THEME_${_type}}) if( _theme_GROUP) list(APPEND _themes "${_theme_GROUP}") _ECM_ADD_ICON_INSTALL_RULE(${CMAKE_CURRENT_BINARY_DIR}/install_icons.cmake ${_defaultpath}/${_theme_GROUP}/${_size}x${_size} ${_group} ${_current_ICON} ${_name} ${_l10n_SUBDIR}) endif() endforeach (_current_ICON) # and now the svg icons file(GLOB _icons *.svgz) foreach (_current_ICON ${_icons} ) # since CMake 2.6 regex matches are stored in special variables CMAKE_MATCH_x, if it didn't match, they are empty string(REGEX MATCH "^.*/([a-zA-Z]+)sc\\-([a-z]+)\\-(.+\\.svgz)$" _dummy "${_current_ICON}") set(_type "${CMAKE_MATCH_1}") set(_group "${CMAKE_MATCH_2}") set(_name "${CMAKE_MATCH_3}") set(_theme_GROUP ${_ECM_ICON_THEME_${_type}}) if( _theme_GROUP) list(APPEND _themes "${_theme_GROUP}") _ECM_ADD_ICON_INSTALL_RULE(${CMAKE_CURRENT_BINARY_DIR}/install_icons.cmake ${_defaultpath}/${_theme_GROUP}/scalable ${_group} ${_current_ICON} ${_name} ${_l10n_SUBDIR}) endif() endforeach (_current_ICON) if (_themes) list(REMOVE_DUPLICATES _themes) foreach(_theme ${_themes}) _ecm_update_iconcache("${_defaultpath}" "${_theme}") endforeach() else() message(AUTHOR_WARNING "No suitably-named icons found") endif() endmacro() # only used internally by _ecm_install_icons_v1 macro(_ecm_add_icon_install_rule _install_SCRIPT _install_PATH _group _orig_NAME _install_NAME _l10n_SUBDIR) # if the string doesn't match the pattern, the result is the full string, so all three have the same content if (NOT ${_group} STREQUAL ${_install_NAME} ) set(_icon_GROUP ${_ECM_ICON_GROUP_${_group}}) if(NOT _icon_GROUP) message(WARNING "Icon ${_install_NAME} uses invalid category ${_group}, setting to 'actions'") set(_icon_GROUP "actions") endif() # message(STATUS "icon: ${_current_ICON} size: ${_size} group: ${_group} name: ${_name} l10n: ${_l10n_SUBDIR}") install(FILES ${_orig_NAME} DESTINATION ${_install_PATH}/${_icon_GROUP}/${_l10n_SUBDIR}/ RENAME ${_install_NAME} ) endif (NOT ${_group} STREQUAL ${_install_NAME} ) endmacro() # Updates the mtime of the icon theme directory, so caches that # watch for changes to the directory will know to update. # If present, this also runs gtk-update-icon-cache (which despite the name is also used by Qt). function(_ecm_update_iconcache installdir theme) find_program(GTK_UPDATE_ICON_CACHE_EXECUTABLE NAMES gtk-update-icon-cache) # We don't always have touch command (e.g. on Windows), so instead # create and delete a temporary file in the theme dir. install(CODE " set(DESTDIR_VALUE \"\$ENV{DESTDIR}\") if (NOT DESTDIR_VALUE) execute_process(COMMAND \"${CMAKE_COMMAND}\" -E touch \"${CMAKE_INSTALL_PREFIX}/${installdir}/${theme}\") set(HAVE_GTK_UPDATE_ICON_CACHE_EXEC ${GTK_UPDATE_ICON_CACHE_EXECUTABLE}) if (HAVE_GTK_UPDATE_ICON_CACHE_EXEC) execute_process(COMMAND ${GTK_UPDATE_ICON_CACHE_EXECUTABLE} -q -t -i . WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/${installdir}/${theme}\") endif () endif (NOT DESTDIR_VALUE) ") endfunction() function(ecm_install_icons) set(options) set(oneValueArgs DESTINATION LANG THEME) set(multiValueArgs ICONS) cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(NOT ARG_ICONS AND NOT ARG_DESTINATION) message(AUTHOR_WARNING "ecm_install_icons() with no ICONS argument is deprecated") _ecm_install_icons_v1(${ARGN}) return() endif() if(ARG_UNPARSED_ARGUMENTS) message(FATAL_ERROR "Unexpected arguments to ecm_install_icons: ${ARG_UNPARSED_ARGUMENTS}") endif() if(NOT ARG_DESTINATION) message(FATAL_ERROR "No DESTINATION argument given to ecm_install_icons") endif() if(NOT ARG_THEME) set(ARG_THEME "hicolor") endif() if(ARG_LANG) set(l10n_subdir "l10n/${ARG_LANG}/") endif() foreach(icon ${ARG_ICONS}) get_filename_component(filename "${icon}" NAME) string(REGEX MATCH "([0-9sc]+)\\-([a-z]+)\\-([^/]+)\\.([a-z]+)$" complete_match "${filename}") set(size "${CMAKE_MATCH_1}") set(group "${CMAKE_MATCH_2}") set(name "${CMAKE_MATCH_3}") set(ext "${CMAKE_MATCH_4}") if(NOT size OR NOT group OR NOT name OR NOT ext) message(WARNING "${icon} is not named correctly for ecm_install_icons - ignoring") elseif(NOT size STREQUAL "sc" AND NOT size GREATER 0) message(WARNING "${icon} size (${size}) is invalid - ignoring") else() if (NOT complete_match STREQUAL filename) # We can't stop accepting filenames with leading characters, # because that would break existing projects, so just warn # about them instead. message(AUTHOR_WARNING "\"${icon}\" has characters before the size; it should be renamed to \"${size}-${group}-${name}.${ext}\"") endif() if(NOT _ECM_ICON_GROUP_${group}) message(WARNING "${icon} group (${group}) is not recognized") endif() if(size STREQUAL "sc") if(NOT ext STREQUAL "svg" AND NOT ext STREQUAL "svgz") message(WARNING "Scalable icon ${icon} is not SVG or SVGZ") endif() set(size_dir "scalable") else() if(NOT ext STREQUAL "png" AND NOT ext STREQUAL "mng" AND NOT ext STREQUAL "svg" AND NOT ext STREQUAL "svgz") message(WARNING "Fixed-size icon ${icon} is not PNG/MNG/SVG/SVGZ") endif() set(size_dir "${size}x${size}") endif() install( FILES "${icon}" DESTINATION "${ARG_DESTINATION}/${ARG_THEME}/${size_dir}/${group}/${l10n_subdir}" RENAME "${name}.${ext}" ) endif() endforeach() _ecm_update_iconcache("${ARG_DESTINATION}" "${ARG_THEME}") endfunction() Charm-1.12.0/cmake/ECM/modules/ECMQueryQmake.cmake000066400000000000000000000021361331066577000214060ustar00rootroot00000000000000find_package(Qt5Core QUIET) if (Qt5Core_FOUND) set(_qmake_executable_default "qmake-qt5") endif () if (TARGET Qt5::qmake) get_target_property(_qmake_executable_default Qt5::qmake LOCATION) endif() set(QMAKE_EXECUTABLE ${_qmake_executable_default} CACHE FILEPATH "Location of the Qt5 qmake executable") # This is not public API (yet)! function(query_qmake result_variable qt_variable) if(NOT QMAKE_EXECUTABLE) set(${result_variable} "" PARENT_SCOPE) message(WARNING "Should specify a qmake Qt5 binary. Can't check ${qt_variable}") return() endif() execute_process( COMMAND ${QMAKE_EXECUTABLE} -query "${qt_variable}" RESULT_VARIABLE return_code OUTPUT_VARIABLE output ) if(return_code EQUAL 0) string(STRIP "${output}" output) file(TO_CMAKE_PATH "${output}" output_path) set(${result_variable} "${output_path}" PARENT_SCOPE) else() message(WARNING "Failed call: ${QMAKE_EXECUTABLE} -query \"${qt_variable}\"") message(FATAL_ERROR "QMake call failed: ${return_code}") endif() endfunction() Charm-1.12.0/cmake/ECM/modules/ECMUninstallTarget.cmake000066400000000000000000000057261331066577000224520ustar00rootroot00000000000000#.rst: # ECMUninstallTarget # ------------------ # # Add an ``uninstall`` target. # # By including this module, an ``uninstall`` target will be added to your CMake # project. This will remove all files installed (or updated) by a previous # invocation of the ``install`` target. It will not remove files created or # modified by an ``install(SCRIPT)`` or ``install(CODE)`` command; you should # create a custom uninstallation target for these and use ``add_dependency`` to # make the ``uninstall`` target depend on it: # # .. code-block:: cmake # # include(ECMUninstallTarget) # install(SCRIPT install-foo.cmake) # add_custom_target(uninstall_foo COMMAND ${CMAKE_COMMAND} -P uninstall-foo.cmake) # add_dependency(uninstall uninstall_foo) # # The target will fail if the ``install`` target has not yet been run (so it is # not possible to run CMake on the project and then immediately run the # ``uninstall`` target). # # .. warning:: # # CMake deliberately does not provide an ``uninstall`` target by default on # the basis that such a target has the potential to remove important files # from a user's computer. Use with caution. # # Since 1.7.0. #============================================================================= # Copyright 2015 Alex Merry # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. if (NOT TARGET uninstall) configure_file( "${CMAKE_CURRENT_LIST_DIR}/ecm_uninstall.cmake.in" "${CMAKE_BINARY_DIR}/ecm_uninstall.cmake" IMMEDIATE @ONLY ) add_custom_target(uninstall COMMAND "${CMAKE_COMMAND}" -P "${CMAKE_BINARY_DIR}/ecm_uninstall.cmake" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" ) endif() Charm-1.12.0/cmake/ECM/modules/ecm_uninstall.cmake.in000066400000000000000000000015121331066577000222340ustar00rootroot00000000000000if(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt") message(FATAL_ERROR "Cannot find install manifest: @CMAKE_BINARY_DIR@/install_manifest.txt") endif() file(READ "@CMAKE_BINARY_DIR@/install_manifest.txt" files) string(REGEX REPLACE "\n" ";" files "${files}") foreach(file ${files}) message(STATUS "Uninstalling $ENV{DESTDIR}${file}") if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") exec_program( "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" OUTPUT_VARIABLE rm_out RETURN_VALUE rm_retval ) if(NOT "${rm_retval}" STREQUAL 0) message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") endif() else() message(STATUS "File $ENV{DESTDIR}${file} does not exist.") endif() endforeach() Charm-1.12.0/debian.changelog000066400000000000000000000026471331066577000157410ustar00rootroot00000000000000charmtimetracker (1.11.4) stable; urgency=medium * 1.11.4 release -- Steffen Hansen Wed, 01 Nov 2016 12:00:00 +0100 charmtimetracker (1.11.3) stable; urgency=medium * 1.11.3 release -- Steffen Hansen Wed, 01 Nov 2016 10:00:00 +0100 charmtimetracker (1.11.1) stable; urgency=medium * 1.11.1 release -- Allen Winter Sat, 30 Apr 2016 09:00:00 -0500 charmtimetracker (1.11.0) stable; urgency=low * 1.11.0 release -- Allen Winter Mon, 25 Apr 2016 14:30:00 -0500 charmtimetracker (1.10.0) stable; urgency=low * 1.10.0 release -- Allen Winter Mon, 05 Oct 2015 13:30:00 -0500 charmtimetracker (1.9.0) stable; urgency=low * 1.9.0 release -- Allen Winter Thu, 26 Mar 2015 10:30:00 -0500 charmtimetracker (1.8.0) stable; urgency=low * 1.8.0 release -- Kevin Ottens Tue, 02 Jul 2013 12:11:38 +0200 charmtimetracker (1.7.0) stable; urgency=low * 1.7.0 release -- Frank Osterfeld Fri, 27 Jul 2012 14:10:14 +0000 charmtimetracker (1.6.0) stable; urgency=low * 1.6.0 release -- Mike McQuaid Thu, 23 Feb 2012 14:10:14 +0000 charmtimetracker (1.5.2) stable; urgency=low * Initial Debian Package -- Mike McQuaid Wed, 07 Dec 2011 14:18:38 +0000 Charm-1.12.0/debian.compat000066400000000000000000000000021331066577000152540ustar00rootroot000000000000009 Charm-1.12.0/debian.control000066400000000000000000000020651331066577000154640ustar00rootroot00000000000000Source: charmtimetracker Section: Miscellaneous Priority: optional Maintainer: Frank Osterfeld Build-Depends: debhelper (>=9), cdbs, cmake, libqt4-dev, libxss-dev, libqt4-sql-sqlite Standards-Version: 3.9.6 Homepage: https://github.com/KDAB/Charm Package: charmtimetracker Architecture: any Depends: ${misc:Depends}, ${shlibs:Depends}, libqt4-sql-sqlite Description: The Cross-Platform Time Tracker. Charm is a program for OS X, Linux and Windows that helps to keep track of time. It is built around two major ideas - tasks, and events. Tasks are the things time is spend on, repeatedly. For example, ironing laundry is a task. The laundry done for two hours on last Tuesday is an event in that task. When doing laundry multiple times, the events will be accumulated, and can later be printed in activity reports or weekly time sheets. So in case laundry would be done for three hours on Wednesday again, the activity report for the "Ironing Laundry" task would list the event on tuesday, the event on wednesday and a total of five hours. Charm-1.12.0/debian.rules000066400000000000000000000002301331066577000151260ustar00rootroot00000000000000#!/usr/bin/make -f DEB_CMAKE_EXTRA_FLAGS = -DCharm_VERSION=1.12.0 include /usr/share/cdbs/1/rules/debhelper.mk include /usr/share/cdbs/1/class/cmake.mk Charm-1.12.0/scripts/000077500000000000000000000000001331066577000143245ustar00rootroot00000000000000Charm-1.12.0/scripts/NullsoftInstaller.nsi000066400000000000000000000070531331066577000205300ustar00rootroot00000000000000; basic script template for NullsoftInstallerPackager ; ; Copyright 2016-2018 Hannnah von Reth ; Copyright 2010 Patrick Spendrin !include MUI2.nsh !include LogicLib.nsh ; registry stuff !define regkey "Software\${companyName}\${productName}" !define uninstkey "Software\Microsoft\Windows\CurrentVersion\Uninstall\${productName}" !define startmenu "$SMPROGRAMS\${productName}" !define uninstaller "uninstall.exe" Var StartMenuFolder ;Start Menu Folder Page Configuration !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKLM" !define MUI_STARTMENUPAGE_REGISTRY_KEY "${regkey}" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" ;-------------------------------- XPStyle on ShowInstDetails hide ShowUninstDetails hide SetCompressor /SOLID lzma Name "${productName}" Caption "Installing ${productName}" OutFile "${setupname}" !define MUI_ICON ${applicationIcon} !insertmacro MUI_PAGE_WELCOME ${productLicence} !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH ;uninstaller !insertmacro MUI_UNPAGE_WELCOME !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_UNPAGE_FINISH ;------- !insertmacro MUI_LANGUAGE "English" SetDateSave on SetDatablockOptimize on CRCCheck on SilentInstall normal InstallDir "${programFilesDir}\${productName}" InstallDirRegKey HKLM "${regkey}" "" ;-------------------------------- AutoCloseWindow false ; beginning (invisible) section Section "--hidden ${productName}" BASE SectionIn RO SetOutPath $INSTDIR SetShellVarContext all WriteRegStr HKLM "${regkey}" "Install_Dir" "$INSTDIR" WriteRegStr HKLM "${regkey}" "Version" "${productVersion}" WriteRegStr HKLM "${regkey}" "" "$INSTDIR\uninstall.exe" WriteRegStr HKLM "${uninstkey}" "DisplayName" "${productName} (remove only)" WriteRegStr HKLM "${uninstkey}" "DisplayIcon" "$INSTDIR\${applicationName}" WriteRegStr HKLM "${uninstkey}" "DisplayVersion" "${productVersion}" WriteRegStr HKLM "${uninstkey}" "UninstallString" '"$INSTDIR\${uninstaller}"' WriteRegStr HKLM "${uninstkey}" "Publisher" "${companyName}" SetOutPath $INSTDIR ; package all files, recursively, preserving attributes ; assume files are in the correct places File /a /r /x "*.nsi" /x "${setupname}" "${deployDir}\*.*" !if "${vcredist}" != "none" ExecWait '"$INSTDIR\${vcredist}" /passive' Delete "$INSTDIR\${vcredist}" !endif WriteUninstaller "${uninstaller}" ;Create shortcuts !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateDirectory "$SMPROGRAMS\$StartMenuFolder" CreateShortCut "$SMPROGRAMS\$StartMenuFolder\${productName}.lnk" "$INSTDIR\${applicationName}" !insertmacro MUI_STARTMENU_WRITE_END SectionEnd ; Uninstaller ; All section names prefixed by "Un" will be in the uninstaller UninstallText "This will uninstall ${productName}." Section "Uninstall" SetShellVarContext all SetShellVarContext all DeleteRegKey HKLM "${uninstkey}" DeleteRegKey HKLM "${regkey}" !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder RMDir /r "$SMPROGRAMS\$StartMenuFolder" RMDir /r "$INSTDIR" SectionEnd Function .onInit ReadRegStr $R0 HKLM "${uninstkey}" "UninstallString" StrCmp $R0 "" done ReadRegStr $INSTDIR HKLM "${regkey}" "Install_Dir" ;Run the uninstaller ;uninst: ClearErrors ExecWait '$R0 _?=$INSTDIR' ;Do not copy the uninstaller to a temp file done: FunctionEnd Charm-1.12.0/scripts/create-win-installer.py000066400000000000000000000152351331066577000207350ustar00rootroot00000000000000#!/usr/bin/env python # create-win-installer.py # # This file is part of Charm, a task-based time tracking application. # # Copyright (C) 2016-2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com # # Author: Hannah von Reth # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import print_function import os import subprocess import sys import shutil import argparse import glob class DeployHelper(object): def __init__(self, args): self.args = args self.gitDir = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) self.deployImage = os.path.realpath("./deployImage") if not os.path.exists(self.args.buildDir): self._die("Build dir %s does not exist" % self.args.buildDir) def _die(self, message): print(message, file=sys.stderr) exit(1) def _logExec(self, command): print(command) sys.stdout.flush() subprocess.check_call(command, shell=True) def _copyToImage(self, src, destDir = None, deploy = True): if destDir is None: destDir = self.deployImage print("Copy %s to %s" % (src, destDir)) shutil.copy(src, destDir) if deploy: self._logExec("windeployqt --%s --dir \"%s\" --qmldir \"%s\" \"%s\"" % (self.args.buildType, self.deployImage, self.gitDir, src)) def _sign(self, fileName): if self.args.sign: self._logExec("signtool.exe sign -t http://timestamp.globalsign.com/scripts/timestamp.dll -fd SHA256 -v \"%s\"" % fileName) def cleanImage(self): if os.path.exists(self.deployImage): shutil.rmtree(self.deployImage) os.makedirs(self.deployImage) def _locateDll(self, dllName, fatal=True): pathext = os.environ.get("PATHEXT") os.environ["PATHEXT"] = ".dll" found = shutil.which(dllName, path=os.pathsep.join([self.deployImage, os.environ.get("PATH")])) os.environ["PATHEXT"] = pathext if not found and fatal: self._die("Unable to locate %s" % dllName) return found def deploy(self): if self.args.deployDlls: for dll in self.args.deployDlls.split(";"): self._copyToImage(self._locateDll(dll)) app = os.path.join(self.args.buildDir, self.args.applicationFileName) shutil.copy(app, self.deployImage) self._logExec("windeployqt --%s --compiler-runtime --dir \"%s\" --qmldir \"%s\" \"%s\"" % (self.args.buildType, self.deployImage, self.gitDir, app)) for folder in self.args.pluginFolders.split(";"): for f in glob.glob(os.path.join(self.args.buildDir, folder, "*.dll")): self._copyToImage(f) if self.args.deployOpenSSL: foundSomething = False for dll in ["libeay32.dll", "libssl32.dll", "ssleay32.dll" ]: src = os.path.join(self.args.deployOpenSSL, dll ) if not os.path.exists(src): src = self._locateDll(dll, fatal=False) if src: foundSomething = True print(src) self._copyToImage(src, deploy=False) if not foundSomething: self._die("Failed to deploy openssl") if self.args.sign: for f in glob.glob(os.path.join(self.deployImage, "**/*.exe"), recursive=True): self._sign(f) for f in glob.glob(os.path.join(self.deployImage, "**/*.dll"), recursive=True): self._sign(f) def makeInstaller(self): if self.args.architecture == "x64": programDir = "$PROGRAMFILES64" else: programDir = "$PROGRAMFILES" defines = {} defines["productName"] = self.args.productName defines["companyName"] = self.args.companyName defines["productVersion"] = self.args.productVersion defines["setupname"] = self.args.installerName defines["applicationName"] = os.path.basename(self.args.applicationFileName) defines["applicationIcon"] = self.args.applicationIcon defines["programFilesDir"] = programDir defines["deployDir"] = self.deployImage defines["productLicence"] = "" if not self.args.productLicence else "!insertmacro MUI_PAGE_LICENSE \"%s\"" % self.args.productLicence redist = "vcredist_%s.exe" % self.args.architecture if os.path.exists(os.path.join(self.deployImage, redist)): defines["vcredist"] = "vcredist_%s.exe" % self.args.architecture else: defines["vcredist"] = "none" definestring = "" for key in defines: definestring += " /D%s=\"%s\"" % (key, defines[key]) command = "makensis /NOCD %s %s" %\ (definestring, os.path.join(os.path.dirname(__file__), "NullsoftInstaller.nsi")) self._logExec(command) installer = os.path.realpath(self.args.installerName) self._sign(installer) print("""Generated package file: %s """ % installer) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--architecture", action = "store", default="x64" ) parser.add_argument("--buildType", action = "store", default="release" ) parser.add_argument("--installerName", action = "store" ) parser.add_argument("--applicationFileName", action = "store" ) parser.add_argument("--applicationIcon", action = "store" ) parser.add_argument("--buildDir", action = "store" ) parser.add_argument("--pluginFolders", action = "store", default="" ) parser.add_argument("--productName", action = "store" ) parser.add_argument("--companyName", action = "store" ) parser.add_argument("--productVersion", action = "store" ) parser.add_argument("--productLicence", action = "store" ) parser.add_argument("--deployDlls", action = "store" ) parser.add_argument("--deployOpenSSL", action = "store" ) parser.add_argument("--sign", action = "store_true", default=False) args = parser.parse_args() helper = DeployHelper(args) helper.cleanImage() helper.deploy() helper.makeInstaller()
    Starting:%2 at %3
    Ending:%4 at %5
    Duration:%6.