pax_global_header00006660000000000000000000000064126034335310014512gustar00rootroot0000000000000052 comment=3bfa59c8d1fb30abee7f93d2144870c548e4c69f Charm-1.10.0/000077500000000000000000000000001260343353100126235ustar00rootroot00000000000000Charm-1.10.0/.gitignore000066400000000000000000000004071260343353100146140ustar00rootroot00000000000000.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.10.0/.krazy000066400000000000000000000005051260343353100137640ustar00rootroot00000000000000CHECKSETS qt4,c++,foss #exclude intrusive checks to investigate later EXCLUDE foreach,nullstrassign #KDAB-specific checks EXTRA kdabcopyright #additional checks EXTRA camelcase,null,defines,crud #skip Keychain SKIP /Charm/Keychain/ #if you have a build subdir, skip it SKIP /build- #other skips SKIP /CharmCMake.h.cmake Charm-1.10.0/CMakeLists.txt000066400000000000000000000226571260343353100153770ustar00rootroot00000000000000PROJECT( Charm ) 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() IF( NOT CMAKE_BUILD_TYPE ) SET( CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE ) ENDIF() IF( APPLE AND CMAKE_INSTALL_PREFIX MATCHES "/usr/local" ) SET( CMAKE_INSTALL_PREFIX "/Applications" ) ENDIF() MESSAGE( STATUS "Building Charm ${Charm_VERSION} in ${CMAKE_BUILD_TYPE} mode" ) CMAKE_MINIMUM_REQUIRED( VERSION 2.6.0 ) SET( CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMake" ) if(CMAKE_COMPILER_IS_GNUCXX OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") set(CMAKE_CXX_FLAGS "-std=c++0x ${CMAKE_CXX_FLAGS}") endif() SET( CMAKE_AUTOMOC ON ) OPTION( CHARM_FORCE_QT4 "Force building Charm with Qt4" OFF ) # try Qt5 first, and prefer that if found IF( NOT CHARM_FORCE_QT4 ) find_package( Qt5Core QUIET ) ENDIF() IF(Qt5Core_FOUND) find_package(Qt5Widgets REQUIRED) find_package(Qt5Xml REQUIRED) find_package(Qt5Network REQUIRED) find_package(Qt5Sql REQUIRED) find_package(Qt5Test REQUIRED) find_package(Qt5PrintSupport) IF(Qt5PrintSupport_FOUND) include_directories(${Qt5PrintSupport_INCLUDE_DIRS}) set(QTPRINT_LIBRARIES ${Qt5PrintSupport_LIBRARIES}) ENDIF() SET(CHARM_MAC_HIGHRES_SUPPORT_ENABLED ON) IF(WIN32) SET(QT_PLATFORM_SPECIFIC_LIBRARIES Qt5::WinMain) ELSE() SET(QT_PLATFORM_SPECIFIC_LIBRARIES) ENDIF() IF(APPLE) find_package(Qt5MacExtras REQUIRED) include_directories(${Qt5MacExtras_INCLUDE_DIRS}) set(QT_PLATFORM_SPECIFIC_LIBRARIES ${Qt5MacExtras_LIBRARIES}) ENDIF() IF(UNIX AND NOT APPLE) find_package(Qt5DBus) IF(Qt5DBus_FOUND) set(HAVE_DBUS ON) include_directories(${Qt5DBus_INCLUDE_DIRS}) set(QTDBUS_LIBRARIES ${Qt5DBus_LIBRARIES}) macro(qt_add_dbus_interface) qt5_add_dbus_interface(${ARGN}) endmacro() ELSE() set(QTDBUS_LIBRARIES "") macro(qt_add_dbus_interface) # do nothing endmacro() ENDIF() ENDIF() macro(qt_wrap_cpp) qt5_wrap_cpp(${ARGN}) endmacro() macro(qt_wrap_ui) qt5_wrap_ui(${ARGN}) endmacro() macro(qt_add_resources) qt5_add_resources(${ARGN}) endmacro() #set(QTCORE_LIBRARIES ${Qt5Core_LIBRARIES}) set(QT_QTTEST_LIBRARY ${Qt5Test_LIBRARIES}) set(QT_LIBRARIES ${Qt5Core_LIBRARIES} ${Qt5Widgets_LIBRARIES} ${Qt5Xml_LIBRARIES} ${Qt5Network_LIBRARIES} ${Qt5Sql_LIBRARIES} ${QTPRINT_LIBRARIES} ${QTDBUS_LIBRARIES} ${QT_PLATFORM_SPECIFIC_LIBRARIES} ) include_directories( ${Qt5Core_INCLUDE_DIRS} ${Qt5Widgets_INCLUDE_DIRS} ${Qt5Xml_INCLUDE_DIRS} ${Qt5Network_INCLUDE_DIRS} ${Qt5Sql_INCLUDE_DIRS} ) SET( CMAKE_CXX_FLAGS "${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS} ${CMAKE_CXX_FLAGS}" ) ELSE() # No Qt5 found, try Qt4 find_package(Qt4 COMPONENTS QtMain QtCore QtGui QtSql QtXml QtNetwork REQUIRED) INCLUDE( UseQt4 ) SET(CHARM_MAC_HIGHRES_SUPPORT_ENABLED OFF) IF(UNIX AND NOT APPLE) find_package(Qt4 COMPONENTS QtDBus) IF (QT_QTDBUS_FOUND) set(HAVE_DBUS ON) set(QTDBUS_LIBRARIES ${QT_QTDBUS_LIBRARY}) macro(qt_add_dbus_interface) qt4_add_dbus_interface(${ARGN}) endmacro() ENDIF() ENDIF() IF (NOT QT_QTDBUS_FOUND) set(QTDBUS_LIBRARIES "") macro(qt_add_dbus_interface) # do nothing endmacro() ENDIF() include_directories(${QT_INCLUDES}) #set(QTCORE_LIBRARIES ${QT_QTCORE_LIBRARY}) macro(qt_wrap_cpp) qt4_wrap_cpp(${ARGN}) endmacro() macro(qt_wrap_ui) qt4_wrap_ui(${ARGN}) endmacro() macro(qt_add_resources) qt4_add_resources(${ARGN}) endmacro() ENDIF() ENABLE_TESTING() IF( APPLE AND "${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}.${QT_VERSION_PATCH}" VERSION_EQUAL "4.8.0" ) MESSAGE( WARNING "Qt 4.8.0 is rather broken on OSX.\n" "Known bugs in Charm include global keyboard shortcuts.\n" "Qt 4.7.4 is the current recommended version on OSX." ) ENDIF() IF( CMAKE_BUILD_TYPE MATCHES "^[Rr]elease$" ) ADD_DEFINITIONS( -DQT_NO_DEBUG_OUTPUT ) ENDIF() # Always include the source and build directories in the include path # to save doing so manually in every subdirectory. SET( CMAKE_INCLUDE_CURRENT_DIR ON ) # Put the include directories which are in the source or build tree # before all other include directories so they are preferred over # any installed Charm headers. SET( CMAKE_INCLUDE_DIRECTORIES_PROJECT_BEFORE ON ) IF( CMAKE_COMPILER_IS_GNUCXX ) # Add additional GCC options. ADD_DEFINITIONS( -Wall -Wundef -Wcast-align -Wchar-subscripts -Wpointer-arith -Wwrite-strings -Wpacked -Wformat-security -Wmissing-format-attribute -Wold-style-cast ) ADD_DEFINITIONS( -fvisibility=hidden ) ELSEIF( CMAKE_CXX_COMPILER MATCHES "clang" ) ADD_DEFINITIONS( -Wall -Wextra -Wno-unused-parameter ) ADD_DEFINITIONS( -fvisibility=hidden ) ENDIF() IF( UNIX AND NOT APPLE ) set( Charm_EXECUTABLE charmtimetracker ) SET( BIN_INSTALL_DIR bin ) SET( DOC_INSTALL_DIR share/doc/${Charm_EXECUTABLE} ) ELSE() set( Charm_EXECUTABLE Charm ) SET( BIN_INSTALL_DIR . ) SET( DOC_INSTALL_DIR . ) ENDIF() SET( ICONS_DIR "${CMAKE_SOURCE_DIR}/Charm/Icons" ) 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() 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.txt" ) IF( NOT APPLE ) INSTALL( FILES "${LICENSE_FILE}" "${README_FILE}" DESTINATION ${DOC_INSTALL_DIR} ) ENDIF() # Only support CPack packaging on newer versions of CMake. IF( NOT "${CMAKE_VERSION}" VERSION_LESS "2.8.4" ) SET( CPACK_GENERATOR "TBZ2" ) SET( CPACK_PACKAGE_VERSION_MAJOR "${Charm_VERSION_MAJOR}" ) SET( CPACK_PACKAGE_VERSION_MINOR "${Charm_VERSION_MINOR}" ) SET( CPACK_PACKAGE_VERSION_PATCH "${Charm_VERSION_PATCH}" ) SET( CPACK_PACKAGE_VERSION "${Charm_VERSION}" ) SET( CPACK_PACKAGE_VENDOR "KDAB" ) SET( CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/${README_FILE}" ) IF( WIN32 ) SET( CPACK_GENERATOR "NSIS" "ZIP" ) SET( CPACK_PACKAGE_EXECUTABLES "Charm" "Charm" ) SET( CPACK_PACKAGE_INSTALL_DIRECTORY "Charm" ) SET( CPACK_PACKAGE_FILE_NAME "Charm ${Charm_VERSION}" ) SET( CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/${LICENSE_FILE}" ) SET( CPACK_NSIS_EXECUTABLES_DIRECTORY "${BIN_INSTALL_DIR}" ) SET( CPACK_NSIS_MUI_ICON "${ICONS_DIR}/Charm.ico" ) SET( CPACK_PACKAGE_ICON "${ICONS_DIR}\\\\CharmNSISHeader.bmp" ) SET( CPACK_NSIS_URL_INFO_ABOUT "http://www.kdab.com/" ) SET( CPACK_NSIS_INSTALLED_ICON_NAME "Charm${CMAKE_EXECUTABLE_SUFFIX}" ) SET( CPACK_NSIS_MENU_LINKS "${LICENSE_FILE}" "License" "${README_FILE}" "Readme" ) SET( CPACK_NSIS_MUI_FINISHPAGE_RUN "${CPACK_NSIS_INSTALLED_ICON_NAME}" ) ELSEIF( APPLE ) SET( CPACK_GENERATOR "DragNDrop" ) SET( CPACK_DMG_FORMAT "UDBZ" ) SET( CPACK_DMG_VOLUME_NAME "Charm" ) SET( CPACK_SYSTEM_NAME "OSX" ) SET( CPACK_PACKAGE_FILE_NAME "Charm-${Charm_VERSION}" ) SET( CPACK_PACKAGE_ICON "${ICONS_DIR}/CharmDMG.icns" ) SET( CPACK_DMG_DS_STORE "${ICONS_DIR}/CharmDSStore" ) SET( CPACK_DMG_BACKGROUND_IMAGE "${ICONS_DIR}/CharmDMGBackground.png" ) ELSEIF( UNIX ) SET( CPACK_SYSTEM_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}" ) ENDIF() INCLUDE( CPack ) ENDIF() Charm-1.10.0/Charm.pro000066400000000000000000000154111260343353100144010ustar00rootroot00000000000000!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/EventWindow.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/TasksWindow.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/TasksWindow.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/EventWindow.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.10.0/Charm/000077500000000000000000000000001260343353100136555ustar00rootroot00000000000000Charm-1.10.0/Charm/ApplicationCore.cpp000066400000000000000000000671141260343353100174460ustar00rootroot00000000000000/* ApplicationCore.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "Uniquifier.h" #include "Core/CharmConstants.h" #include "Core/CharmExceptions.h" #include "Core/SqLiteStorage.h" #include "HttpClient/HttpJob.h" #include "Idle/IdleDetector.h" #include "Widgets/ConfigurationDialog.h" #include "Widgets/NotificationPopup.h" #include #include #include #include #include #include #include #include #include #include #if QT_VERSION < 0x050000 #include #else #include #endif #include #include #ifdef CHARM_CI_SUPPORT # include "CI/CharmCommandInterface.h" #endif ApplicationCore* ApplicationCore::m_instance = nullptr; ApplicationCore::ApplicationCore( QObject* parent ) : QObject( parent ) , m_closedWindow( nullptr ) , m_actionStopAllTasks( this ) , m_windows( QList () << &m_tasksWindow << &m_eventWindow << &m_timeTracker ) , m_actionQuit( this ) , m_state(Constructed) , m_systrayContextMenuStartTask( m_timeTracker.menu()->title() ) , 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_idleDetector( nullptr ) , m_cmdInterface( nullptr ) , m_timeTrackerHiddenFromSystrayToggle( false ) , m_tasksWindowHiddenFromSystrayToggle( false ) , m_eventWindowHiddenFromSystrayToggle( false ) , m_dateChangeWatcher( new DateChangeWatcher( this ) ) { // QApplication setup QApplication::setQuitOnLastWindowClosed(false); // application metadata setup // note that this modifies the behaviour of QSettings: QCoreApplication::setOrganizationName("KDAB"); QCoreApplication::setOrganizationDomain("kdab.com"); QCoreApplication::setApplicationName("Charm"); QCoreApplication::setApplicationVersion(CHARM_VERSION); QLocalSocket uniqueApplicationSocket; QString serverName( "com.kdab.charm" ); #ifndef NDEBUG serverName.append( "_debug" ); #endif uniqueApplicationSocket.connectToServer(serverName, QIODevice::ReadOnly); if (uniqueApplicationSocket.waitForConnected(1000)) throw AlreadyRunningException(); connect(&m_uniqueApplicationServer, SIGNAL(newConnection()), this, SLOT(slotHandleUniqueApplicationConnection())); QFile::remove(QDir::tempPath() + '/' + 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, SIGNAL(readyToQuit()), SLOT( slotControllerReadyToQuit())); connectControllerAndModel(&m_controller, m_model.charmDataModel()); Charm::connectControllerAndView(&m_controller, &mainView()); Q_FOREACH( auto window, m_windows ) { if ( window != &mainView() ) { // main view acts as the main relay connect( window, SIGNAL(emitCommand(CharmCommand*)), &mainView(), SLOT(sendCommand(CharmCommand*)) ); connect( window, SIGNAL(emitCommandRollback(CharmCommand*)), &mainView(), SLOT(sendCommandRollback(CharmCommand*)) ); } else connect( window, SIGNAL(showNotification(QString,QString)), SLOT(slotShowNotification(QString,QString)) ); // save the configuration (configuration is managed by the application) connect( window, SIGNAL(saveConfiguration()), SLOT(slotSaveConfiguration()) ); connect( window, SIGNAL(visibilityChanged(bool)), this, SLOT(slotCharmWindowVisibilityChanged(bool)) ); } // my own signals: connect(this, SIGNAL(goToState(State)), SLOT(setState(State)), Qt::QueuedConnection); // system tray icon: m_actionStopAllTasks.setText( tr( "Stop &All Active Tasks" ) ); m_actionStopAllTasks.setShortcut( Qt::Key_Escape ); m_actionStopAllTasks.setShortcutContext( Qt::ApplicationShortcut ); mainView().addAction(&m_actionStopAllTasks); // for the shortcut to work connect( &m_actionStopAllTasks, SIGNAL(triggered()), SLOT(slotStopAllTasks()) ); int index = m_windows.indexOf( &m_timeTracker ); auto window = m_windows[index]; m_systrayContextMenu.addAction( window->openCharmAction() ); m_systrayContextMenu.addSeparator(); m_systrayContextMenu.addAction( &m_actionStopAllTasks ); m_systrayContextMenu.addSeparator(); m_trayIcon.setContextMenu( &m_systrayContextMenu ); m_trayIcon.setToolTip( tr( "No active events" ) ); m_trayIcon.setIcon( Data::charmTrayIcon() ); m_trayIcon.show(); QApplication::setWindowIcon( Data::charmIcon() ); Q_FOREACH( auto window, m_windows ) { if ( window != &m_timeTracker ) m_systrayContextMenu.addAction( window->showAction() ); } m_systrayContextMenu.addSeparator(); m_systrayContextMenu.addMenu( &m_systrayContextMenuStartTask ); m_systrayContextMenu.addSeparator(); m_systrayContextMenu.addAction( &m_actionQuit ); // set up actions: connect( &m_systrayContextMenuStartTask, SIGNAL(aboutToShow()), SLOT(slotStartTaskMenuAboutToShow()) ); m_actionQuit.setShortcut( Qt::CTRL + Qt::Key_Q ); m_actionQuit.setText( tr( "Quit" ) ); m_actionQuit.setIcon( Data::quitCharmIcon() ); connect( &m_actionQuit, SIGNAL(triggered(bool)), SLOT(slotQuitApplication()) ); m_actionAboutDialog.setText( tr( "About Charm" ) ); connect( &m_actionAboutDialog, SIGNAL(triggered()), &mainView(), SLOT(slotAboutDialog()) ); m_actionPreferences.setText( tr( "Preferences" ) ); m_actionPreferences.setIcon( Data::configureIcon() ); connect( &m_actionPreferences, SIGNAL(triggered(bool)), &mainView(), SLOT(slotEditPreferences(bool)) ); m_actionPreferences.setEnabled( true ); m_actionImportFromXml.setText( tr( "Import Database from Previous Export..." ) ); connect( &m_actionImportFromXml, SIGNAL(triggered()), &mainView(), SLOT(slotImportFromXml()) ); m_actionExportToXml.setText( tr( "Export Database..." ) ); connect( &m_actionExportToXml, SIGNAL(triggered()), &mainView(), SLOT(slotExportToXml()) ); m_actionSyncTasks.setText( tr( "Update Task Definitions..." ) ); connect( &m_actionSyncTasks, SIGNAL(triggered()), &mainView(), SLOT(slotSyncTasks()) ); m_actionImportTasks.setText( tr( "Import and Merge Task Definitions..." ) ); connect( &m_actionImportTasks, SIGNAL(triggered()), &mainView(), SLOT(slotImportTasks()) ); m_actionExportTasks.setText( tr( "Export Task Definitions..." ) ); connect( &m_actionExportTasks, SIGNAL(triggered()), &mainView(), SLOT(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, SIGNAL(triggered()), &mainView(), SLOT(slotCheckForUpdatesManual()) ); m_actionEnterVacation.setText( tr( "Enter Vacation...") ); connect( &m_actionEnterVacation, SIGNAL(triggered()), &mainView(), SLOT(slotEnterVacation()) ); m_actionActivityReport.setText( tr( "Activity Report..." ) ); m_actionActivityReport.setShortcut( Qt::CTRL + Qt::Key_A ); connect( &m_actionActivityReport, SIGNAL(triggered()), &mainView(), SLOT(slotActivityReport()) ); m_actionWeeklyTimesheetReport.setText( tr( "Weekly Timesheet...") ); m_actionWeeklyTimesheetReport.setShortcut( Qt::CTRL + Qt::Key_R ); connect( &m_actionWeeklyTimesheetReport, SIGNAL(triggered()), &mainView(), SLOT(slotWeeklyTimesheetReport()) ); m_actionMonthlyTimesheetReport.setText( tr( "Monthly Timesheet...") ); m_actionMonthlyTimesheetReport.setShortcut( Qt::CTRL + Qt::Key_M ); connect( &m_actionMonthlyTimesheetReport, SIGNAL(triggered()), &mainView(), SLOT(slotMonthlyTimesheetReport()) ); // set up idle detection m_idleDetector = IdleDetector::createIdleDetector( this ); Q_ASSERT( m_idleDetector ); connect( m_idleDetector, SIGNAL(maybeIdle()), SLOT(slotMaybeIdle()) ); setHttpActionsVisible(HttpJob::credentialsAvailable()); // add default plugin path for deployment QCoreApplication::addLibraryPath( QCoreApplication::applicationDirPath() + "/plugins" ); if ( QCoreApplication::applicationDirPath().endsWith(QLatin1String("MacOS") )) QCoreApplication::addLibraryPath( QCoreApplication::applicationDirPath() + "/../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::slotStartTaskMenuAboutToShow() { m_systrayContextMenuStartTask.clear(); m_systrayContextMenuStartTask.addActions( m_timeTracker.menu()->actions() ); } void ApplicationCore::slotHandleUniqueApplicationConnection() { QLocalSocket* socket = m_uniqueApplicationServer.nextPendingConnection(); delete socket; openAWindow( true ); } void ApplicationCore::openAWindow( bool raise ) { CharmWindow* windowToOpen = 0; foreach( CharmWindow* window, m_windows ) if ( !window->isHidden() ) windowToOpen = window; if ( !windowToOpen && m_closedWindow ) windowToOpen = m_closedWindow; if ( !windowToOpen ) windowToOpen = &mainView(); windowToOpen->show(); if ( raise ) windowToOpen->raise(); if( windowToOpen == m_closedWindow ) m_closedWindow = nullptr; } void ApplicationCore::createWindowMenu( QMenuBar *menuBar ) { auto menu = new QMenu( menuBar ); menu->setTitle( tr( "Window" ) ); Q_FOREACH( CharmWindow* window, m_windows ) { menu->addAction( window->showHideAction() ); } 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 ( !QString::fromLatin1(UPDATE_CHECK_URL).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 ( !QString::fromLatin1(UPDATE_CHECK_URL).isEmpty() ) { menu->addAction( &m_actionCheckForUpdates ); } #endif menuBar->addMenu( menu ); } 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; std::for_each( m_windows.begin(), m_windows.end(), std::bind2nd( std::mem_fun( &CharmWindow::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(); QString message = QObject::tr( "" "

Your current Charm database is too old to use with this version. You have two " "options here:

    " "
  • Start over with an empty database by moving or deleting your ~/.Charm 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 " "~/.Charm folder and restart this version of Charm and select File->Import from " "previous export and select the file you saved in the previous step.
  • " "
"); showCritical( QObject::tr("Charm Database Error"), message ); slotQuitApplication(); return; } } void ApplicationCore::leaveConnectingState() { } void ApplicationCore::enterConnectedState() { #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/"); #if QT_VERSION < 0x050000 return QDesktopServices::storageLocation(QDesktopServices::DataLocation) + QLatin1Char('/'); #else return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/'); #endif } 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 = "Charm.db"; const QString storageDatabaseFileDebug = "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; } void ApplicationCore::toggleShowHide() { if ( m_timeTracker.isHidden() && m_tasksWindow.isHidden() && m_eventWindow.isHidden() ) { int raised = 0; if ( m_eventWindowHiddenFromSystrayToggle ) { CharmWindow::showHideView( &m_eventWindow ); m_eventWindowHiddenFromSystrayToggle = false; ++raised; } if ( m_tasksWindowHiddenFromSystrayToggle ) { CharmWindow::showHideView( &m_tasksWindow ); m_tasksWindowHiddenFromSystrayToggle = false; ++raised; } if ( m_timeTrackerHiddenFromSystrayToggle || raised == 0 ) { // if no view was visible and the user did not toggle other views before, raise the timetracker m_timeTracker.showHideView(); m_timeTrackerHiddenFromSystrayToggle = false; } } else { if ( m_timeTracker.isVisible() ) { m_timeTracker.hide(); m_timeTrackerHiddenFromSystrayToggle = true; } if ( m_tasksWindow.isVisible() ) { m_tasksWindow.hide(); m_tasksWindowHiddenFromSystrayToggle = true; } if ( m_eventWindow.isVisible() ) { m_eventWindow.hide(); m_eventWindowHiddenFromSystrayToggle = true; } } } QString ApplicationCore::titleString( const QString& text ) const { QString dbInfo; const QString userName = CONFIGURATION.user.name(); if ( !text.isEmpty() ) { if ( !userName.isEmpty()) { dbInfo = QString("%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 } std::for_each( m_windows.begin(), m_windows.end(), std::mem_fun( &CharmWindow::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 ) { if ( idleDetector()->idlePeriods().count() == 1 ) { m_timeTracker.maybeIdle( idleDetector() ); } // otherwise, the dialog will be showing already } // 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 } CharmWindow& ApplicationCore::mainView() { return m_timeTracker; } TrayIcon& ApplicationCore::trayIcon() { return m_trayIcon; } void ApplicationCore::slotCharmWindowVisibilityChanged( bool visible ) { if( !visible ) m_closedWindow = dynamic_cast< CharmWindow* >( sender() ); } 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) { m_tasksWindow.saveGuiState(); m_eventWindow.saveGuiState(); m_timeTracker.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 ); } } #include "moc_ApplicationCore.cpp" Charm-1.10.0/Charm/ApplicationCore.h000066400000000000000000000130231260343353100171010ustar00rootroot00000000000000/* ApplicationCore.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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/StorageInterface.h" #include "Widgets/CharmWindow.h" #include "Widgets/TasksWindow.h" #include "Widgets/EventWindow.h" #include "Widgets//TimeTrackingWindow.h" #include "Widgets/TrayIcon.h" #include "ModelConnector.h" // FIXME read configuration name from command line class CharmCommandInterface; class IdleDetector; class QSessionManager; class ApplicationCore : public QObject { Q_OBJECT public: explicit ApplicationCore( QObject* parent = nullptr ); ~ApplicationCore(); 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(); public slots: void setState( State state ); void slotStopAllTasks(); void slotQuitApplication(); void slotControllerReadyToQuit(); void slotSaveConfiguration(); void slotGoToConnectedState(); void toggleShowHide(); void setHttpActionsVisible( bool visible ); void saveState( QSessionManager & manager ); void commitData( QSessionManager & manager ); private slots: // void slotMainWindowVisibilityChanged( bool ); // void slotTimeTrackerVisibilityChanged( bool ); void slotCurrentBackendStatusChanged( const QString& text ); void slotMaybeIdle(); void slotCharmWindowVisibilityChanged( bool visibility ); void slotHandleUniqueApplicationConnection(); void slotStartTaskMenuAboutToShow(); void slotShowNotification( const QString& title, const QString& message ); signals: void goToState( State state ); protected: void openAWindow( bool raise = false ); CharmWindow* m_closedWindow; QAction m_actionStopAllTasks; const QList m_windows; 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; ModelConnector m_model; Controller m_controller; TrayIcon m_trayIcon; QMenu m_systrayContextMenu; QMenu m_systrayContextMenuStartTask; 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; TasksWindow m_tasksWindow; EventWindow m_eventWindow; IdleDetector* m_idleDetector; CharmCommandInterface* m_cmdInterface; bool m_timeTrackerHiddenFromSystrayToggle; bool m_tasksWindowHiddenFromSystrayToggle; bool m_eventWindowHiddenFromSystrayToggle; QLocalServer m_uniqueApplicationServer; // 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.10.0/Charm/CI/000077500000000000000000000000001260343353100141505ustar00rootroot00000000000000Charm-1.10.0/Charm/CI/CharmCommandInterface.cpp000066400000000000000000000050151260343353100210270ustar00rootroot00000000000000/* CharmCommandInterface.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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.10.0/Charm/CI/CharmCommandInterface.h000066400000000000000000000025501260343353100204750ustar00rootroot00000000000000/* CharmCommandInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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 slots: void configurationChanged(); private: QList m_servers; }; #endif // CHARM_CI_CHARMCOMMANDINTERFACE_H Charm-1.10.0/Charm/CI/CharmCommandProtocol.h000066400000000000000000000036001260343353100203730ustar00rootroot00000000000000/* CharmCommandProtocol.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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.10.0/Charm/CI/CharmCommandServer.cpp000066400000000000000000000026301260343353100203750ustar00rootroot00000000000000/* CharmCommandServer.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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.10.0/Charm/CI/CharmCommandServer.h000066400000000000000000000024311260343353100200410ustar00rootroot00000000000000/* CharmCommandServer.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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.10.0/Charm/CI/CharmCommandSession.cpp000066400000000000000000000226551260343353100205630ustar00rootroot00000000000000/* CharmCommandSession.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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(QString("%1 %2\n") .arg(CHARM_CI_EVENT_TASK_ADDED) .arg(DATAMODEL->taskIdAndSmartNameString(id)) .toLatin1()); } void CharmCommandSession::taskModified( TaskId id ) { if (!m_device) return; m_device->write(QString("%1 %2\n") .arg(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(QString("%1 %2\n") .arg(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(QString("%1 %2\n") .arg(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(QString("%1 %2\n") .arg(CHARM_CI_SERVER_ACK) .arg(comment) .toLatin1()); } void CharmCommandSession::sendNak(const QString &comment) { m_device->write(QString("%1 %2\n") .arg(CHARM_CI_SERVER_NAK) .arg(comment) .toLatin1()); } void CharmCommandSession::sendComment(const QString &comment) { m_device->write(QString("%1 %2\n") .arg(CHARM_CI_SERVER_COMMENT) .arg(comment) .toLatin1()); } void CharmCommandSession::startHandshake() { sendComment("Charm Command Line Interface"); m_device->write(QString("%1 %2\n") .arg(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 = buffer.readLine(); if (reply.startsWith(CHARM_CI_HANDSHAKE_RECV, Qt::CaseInsensitive)) { sendAck("Entering Command Mode"); startCommand(); } else if (reply.startsWith(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 = buffer.readLine().trimmed(); const QStringList segment = command.split(' ', QString::SkipEmptyParts); if (segment.isEmpty()) { qDebug("Received empty command..."); return; } if (segment[0].compare(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("UNKNOWN TASK"); } else if (segment[0].compare(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("UNKNOWN TASK"); } else if (segment[0].compare(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("UNKNOWN TASK"); } else if (segment[0].compare(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(QString("%0 %1\n") .arg(event.taskId(), 4, 10, QChar('0')) .arg(event.duration()).toLatin1()); } } else sendNak("WORK HARDER"); } else if (segment[0].compare(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("INVALID REQUEST"); } else if (segment[0].compare(CHARM_CI_COMMAND_DISCONNECT, Qt::CaseInsensitive) == 0) { qDebug("BYE command received. Closing connection."); m_device->close(); } /* unknown command sent */ else sendNak("UNKNOWN COMMAND"); } Charm-1.10.0/Charm/CI/CharmCommandSession.h000066400000000000000000000050151260343353100202170ustar00rootroot00000000000000/* CharmCommandSession.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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 id ) {}; void eventAdded( EventId id ) {}; void eventModified( EventId id, Event discardedEvent ) {}; void eventAboutToBeDeleted( EventId id ) {}; void eventDeleted( EventId id ) {}; void eventActivated( EventId id ); void eventDeactivated( EventId id ); protected: void reset(); private 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.10.0/Charm/CI/CharmLocalCommandServer.cpp000066400000000000000000000041201260343353100213440ustar00rootroot00000000000000/* CharmLocalCommandServer.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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() + '/' + "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.10.0/Charm/CI/CharmLocalCommandServer.h000066400000000000000000000025461260343353100210230ustar00rootroot00000000000000/* CharmLocalCommandServer.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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 slots: void onNewConnection(); private: QLocalServer* m_server; }; #endif // CHARM_CI_CHARMLOCALSERVER_H Charm-1.10.0/Charm/CI/CharmTCPCommandServer.cpp000066400000000000000000000057331260343353100207530ustar00rootroot00000000000000/* CharmTCPCommandServer.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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.10.0/Charm/CI/CharmTCPCommandServer.h000066400000000000000000000033011260343353100204050ustar00rootroot00000000000000/* CharmTCPCommandServer.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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 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.10.0/Charm/CMakeLists.txt000066400000000000000000000213741260343353100164240ustar00rootroot00000000000000INCLUDE_DIRECTORIES( ${Charm_SOURCE_DIR} ${Charm_BINARY_DIR} ) if(POLICY CMP0020) CMAKE_POLICY(SET CMP0020 NEW) endif() 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/HttpJob.cpp HttpClient/GetProjectCodesJob.cpp HttpClient/UploadTimesheetJob.cpp HttpClient/GetUserInfoJob.cpp HttpClient/CheckForUpdatesJob.cpp Idle/IdleDetector.cpp Reports/TimesheetInfo.cpp Reports/MonthlyTimesheetXmlWriter.cpp Reports/WeeklyTimesheetXmlWriter.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/EventWindow.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/TasksWindow.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 ) SET(CharmApplication_LIBS) 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 ) FIND_PACKAGE( X11 ) IF( X11_FOUND AND X11_Xscreensaver_LIB ) MESSAGE( "X11 idle detection enabled." ) INCLUDE_DIRECTORIES( ${X11_INCLUDE_DIR} ) LIST( APPEND CharmApplication_SRCS Idle/X11IdleDetector.cpp ) LIST( APPEND CharmApplication_LIBS ${X11_X11_LIB} ${X11_Xscreensaver_LIB} ) SET( CHARM_IDLE_DETECTION_AVAILABLE_X11 "1" CACHE INTERNAL "" ) ELSE() MESSAGE( "Install X11/XScreenSaver headers and library for X11 idle detection." ) ENDIF() ENDIF() ENDIF() LIST( APPEND CharmApplication_SRCS Keychain/keychain.cpp ) IF (APPLE) LIST( APPEND CharmApplication_SRCS Keychain/keychain_mac.cpp 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} ) ELSEIF ( WIN32 ) LIST( APPEND CharmApplication_SRCS Keychain/keychain_win.cpp ) ELSEIF ( UNIX ) IF (HAVE_DBUS) LIST( APPEND CharmApplication_SRCS Keychain/gnomekeyring.cpp Keychain/keychain_unix.cpp ) QT_ADD_DBUS_INTERFACE( CharmApplication_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Keychain/org.kde.KWallet.xml kwallet_interface KWalletInterface ) LIST( APPEND CharmApplication_LIBS ${QT_QTDBUS_LIBRARY} ) ELSE() LIST( APPEND CharmApplication_SRCS Keychain/keychain_unsecure.cpp ) ENDIF() ENDIF() QT_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 ) QT_ADD_RESOURCES( Resources_SRCS CharmResources.qrc ) ADD_LIBRARY( CharmApplication STATIC ${CharmApplication_SRCS} ${UiGenerated_SRCS} ) TARGET_LINK_LIBRARIES(CharmApplication ${CharmApplication_LIBS}) SET( Charm_SRCS Charm.cpp ) 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() IF( MSVC ) SET( Resources_SRCS ${Resources_SRCS} Charm.rc ) ENDIF() ADD_EXECUTABLE( ${Charm_EXECUTABLE} WIN32 MACOSX_BUNDLE ${Charm_SRCS} ${Resources_SRCS} ) TARGET_LINK_LIBRARIES( ${Charm_EXECUTABLE} CharmApplication CharmCore ${QT_LIBRARIES} ) IF( WIN32 ) TARGET_LINK_LIBRARIES( ${Charm_EXECUTABLE} Crypt32 ) ENDIF() MESSAGE( STATUS "Charm will be installed to ${CMAKE_INSTALL_PREFIX}" ) IF( UNIX AND NOT APPLE ) SET( XDG_APPS_INSTALL_DIR share/applications ) INSTALL( FILES charmtimetracker.desktop DESTINATION ${XDG_APPS_INSTALL_DIR} ) INSTALL( FILES Icons/Charm-128x128.png DESTINATION share/icons/hicolor/128x128/apps ) ENDIF() INSTALL( TARGETS ${Charm_EXECUTABLE} DESTINATION ${BIN_INSTALL_DIR} ) 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() EXECUTE_PROCESS( COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_BINARY_DIR}/${EXECUTABLE} ${Charm_BINARY_DIR}/${EXECUTABLE} ) # Only support CPack packaging on newer versions of CMake. IF( NOT "${CMAKE_VERSION}" VERSION_LESS "2.8.4" ) IF( CMAKE_BUILD_TYPE MATCHES "^([Dd][Ee][Bb][Uu][Gg])" ) SET( CMAKE_INSTALL_DEBUG_LIBRARIES_ONLY TRUE ) ENDIF() SET( CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION "${BIN_INSTALL_DIR}" ) INCLUDE( InstallRequiredSystemLibraries ) IF( NOT BIN_INSTALL_DIR STREQUAL "." ) SET( EXECUTABLE ${BIN_INSTALL_DIR}/${EXECUTABLE} ) ENDIF() IF( WIN32 ) FIND_PACKAGE( OpenSSL REQUIRED ) INSTALL( FILES ${OPENSSL_INCLUDE_DIR}/../libeay32.dll ${OPENSSL_INCLUDE_DIR}/../ssleay32.dll DESTINATION ${BIN_INSTALL_DIR} ) ENDIF() IF( APPLE OR WIN32 ) INCLUDE( DeployQt4 ) INSTALL_QT4_EXECUTABLE( "${EXECUTABLE}" "qsqlite" ) ENDIF() # Plugin deployment for Windows/Qt5 IF( WIN32 AND Qt5Core_FOUND ) # Deploy platform plugin to platforms/ (plugins/platforms isn't found, for whatever reason) GET_TARGET_PROPERTY(_qmake_executable Qt5::qmake IMPORTED_LOCATION) EXEC_PROGRAM( ${_qmake_executable} ARGS -query QT_INSTALL_PLUGINS OUTPUT_VARIABLE _loc ) INSTALL( FILES ${_loc}/platforms/qwindows.dll DESTINATION platforms ) # Deploy qsqlite plugin GET_TARGET_PROPERTY( _loc Qt5::QSQLiteDriverPlugin LOCATION ) INSTALL( FILES ${_loc} DESTINATION plugins/sqldrivers ) ENDIF() ENDIF() Charm-1.10.0/Charm/Charm.cpp000066400000000000000000000074051260343353100154210ustar00rootroot00000000000000/* Charm.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 "ApplicationCore.h" #include "MacApplicationCore.h" #include "Core/CharmExceptions.h" #include "CharmCMake.h" static std::shared_ptr createApplicationCore() { #ifdef Q_OS_OSX return std::make_shared(); #else return std::make_shared(); #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 ) { if (argc == 2 && qstrcmp(argv[1], "--version") == 0) { using namespace std; cout << "Charm version " << CHARM_VERSION << endl; return 0; } 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 ); } try { QApplication app( argc, argv ); const std::shared_ptr core( createApplicationCore() ); QObject::connect( &app, SIGNAL(commitDataRequest(QSessionManager&)), core.get(), SLOT(commitData(QSessionManager&)) ); QObject::connect( &app, SIGNAL(saveStateRequest(QSessionManager&)), core.get(), SLOT(saveState(QSessionManager&)) ); 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.10.0/Charm/Charm.rc000066400000000000000000000000761260343353100152400ustar00rootroot00000000000000IDI_ICON1 ICON DISCARDABLE "Icons/Charm.ico" Charm-1.10.0/Charm/CharmResources.qrc000066400000000000000000000027641260343353100173220ustar00rootroot00000000000000 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/Charm-256x256.png Icons/document-close.png Icons/document-edit.png Icons/media-playback-start.png Icons/media-playback-stop.png Icons/Charm-128x128.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.10.0/Charm/Commands/000077500000000000000000000000001260343353100154165ustar00rootroot00000000000000Charm-1.10.0/Charm/Commands/CommandAddTask.cpp000066400000000000000000000030631260343353100207360ustar00rootroot00000000000000/* CommandAddTask.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/ControllerInterface.h" #include "Core/CommandEmitterInterface.h" CommandAddTask::CommandAddTask( const Task& task, QObject* parent ) : CharmCommand( tr("Add Task"), parent ) , m_task( task ) , m_success( false ) { } CommandAddTask::~CommandAddTask() { } bool CommandAddTask::prepare() { return true; } bool CommandAddTask::execute( ControllerInterface* 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; } #include "moc_CommandAddTask.cpp" Charm-1.10.0/Charm/Commands/CommandAddTask.h000066400000000000000000000025171260343353100204060ustar00rootroot00000000000000/* CommandAddTask.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); bool prepare() override; bool execute( ControllerInterface* ) override; bool finalize() override; private: Task m_task; bool m_success; }; #endif Charm-1.10.0/Charm/Commands/CommandDeleteEvent.cpp000066400000000000000000000035561260343353100216360ustar00rootroot00000000000000/* CommandDeleteEvent.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/ControllerInterface.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( ControllerInterface* controller ) { qDebug() << "CommandDeleteEvent::execute: deleting:"; m_event.dump(); return controller->deleteEvent( m_event ); } bool CommandDeleteEvent::rollback(ControllerInterface *controller) { int oldId = m_event.id(); m_event = controller->cloneEvent(m_event); int newId = m_event.id(); if(oldId != newId) 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); } #include "moc_CommandDeleteEvent.cpp" Charm-1.10.0/Charm/Commands/CommandDeleteEvent.h000066400000000000000000000027051260343353100212760ustar00rootroot00000000000000/* CommandDeleteEvent.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); bool prepare() override; bool execute( ControllerInterface* ) override; bool rollback( ControllerInterface* ) override; bool finalize() override; public slots: void eventIdChanged(int,int) override; private: Event m_event; }; #endif Charm-1.10.0/Charm/Commands/CommandDeleteTask.cpp000066400000000000000000000032401260343353100214450ustar00rootroot00000000000000/* CommandDeleteTask.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/ControllerInterface.h" CommandDeleteTask::CommandDeleteTask( const Task& task, QObject* parent ) : CharmCommand( tr("Delete Task"), parent ) , m_task( task ) , m_success( false ) { } CommandDeleteTask::~CommandDeleteTask() { } bool CommandDeleteTask::prepare() { return true; } bool CommandDeleteTask::execute( ControllerInterface* 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; } #include "moc_CommandDeleteTask.cpp" Charm-1.10.0/Charm/Commands/CommandDeleteTask.h000066400000000000000000000025411260343353100211150ustar00rootroot00000000000000/* CommandDeleteTask.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); bool prepare() override; bool execute( ControllerInterface* ) override; bool finalize() override; private: Task m_task; bool m_success; }; #endif Charm-1.10.0/Charm/Commands/CommandExportToXml.cpp000066400000000000000000000043601260343353100216710ustar00rootroot00000000000000/* CommandExportToXml.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/ControllerInterface.h" #include #include #include CommandExportToXml::CommandExportToXml( QString filename, QObject* parent ) : CharmCommand( tr("Export to XML"), parent ) , m_error( false ) , m_filename( filename ) { } CommandExportToXml::~CommandExportToXml() { } bool CommandExportToXml::prepare() { return true; } bool CommandExportToXml::execute( ControllerInterface* 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; } #include "moc_CommandExportToXml.cpp" Charm-1.10.0/Charm/Commands/CommandExportToXml.h000066400000000000000000000025051260343353100213350ustar00rootroot00000000000000/* CommandExportToXml.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); bool prepare() override; bool execute( ControllerInterface* ) override; bool finalize() override; private: bool m_error; QString m_errorString; QString m_filename; }; #endif Charm-1.10.0/Charm/Commands/CommandImportFromXml.cpp000066400000000000000000000044451260343353100222070ustar00rootroot00000000000000/* CommandImportFromXml.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/ControllerInterface.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( ControllerInterface* 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; } #include "moc_CommandImportFromXml.cpp" Charm-1.10.0/Charm/Commands/CommandImportFromXml.h000066400000000000000000000024711260343353100216510ustar00rootroot00000000000000/* CommandImportFromXml.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); bool prepare() override; bool execute( ControllerInterface* ) override; bool finalize() override; private: QString m_error; QString m_filename; }; #endif Charm-1.10.0/Charm/Commands/CommandMakeAndActivateEvent.cpp000066400000000000000000000042271260343353100234110ustar00rootroot00000000000000/* CommandMakeAndActivateEvent.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/ControllerInterface.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( ControllerInterface* 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; } } #include "moc_CommandMakeAndActivateEvent.cpp" Charm-1.10.0/Charm/Commands/CommandMakeAndActivateEvent.h000066400000000000000000000027231260343353100230550ustar00rootroot00000000000000/* CommandMakeAndActivateEvent.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); bool prepare() override; bool execute( ControllerInterface* ) 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.10.0/Charm/Commands/CommandMakeEvent.cpp000066400000000000000000000064211260343353100213030ustar00rootroot00000000000000/* CommandMakeEvent.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/ControllerInterface.h" #include "Widgets/EventView.h" #include CommandMakeEvent::CommandMakeEvent( const Task& task, QObject* parent ) : CharmCommand( tr("Create Event"), parent ) , m_rollback( false ) , m_task( task ) { } CommandMakeEvent::CommandMakeEvent( const Event& event, QObject* parent ) : CharmCommand( tr("Create Event"), parent ) , m_rollback( false ) , m_event( event ) { } CommandMakeEvent::~CommandMakeEvent() { } bool CommandMakeEvent::prepare() { return true; } bool CommandMakeEvent::execute( ControllerInterface* 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(ControllerInterface *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); } #include "moc_CommandMakeEvent.cpp" Charm-1.10.0/Charm/Commands/CommandMakeEvent.h000066400000000000000000000033201260343353100207430ustar00rootroot00000000000000/* CommandMakeEvent.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); bool prepare() override; bool execute( ControllerInterface* ) override; bool rollback( ControllerInterface* ) override; bool finalize() override; public slots: void eventIdChanged(int,int) override; Q_SIGNALS: void finishedOk( const Event& ); private: bool m_rollback; //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.10.0/Charm/Commands/CommandModifyEvent.cpp000066400000000000000000000035301260343353100216530ustar00rootroot00000000000000/* CommandModifyEvent.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/ControllerInterface.h" #include "Core/StorageInterface.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( ControllerInterface* controller ) { // qDebug() << "CommandModifyEvent::execute: committing:"; // m_event.dump(); return controller->modifyEvent( m_event ); } bool CommandModifyEvent::rollback(ControllerInterface *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); } } #include "moc_CommandModifyEvent.cpp" Charm-1.10.0/Charm/Commands/CommandModifyEvent.h000066400000000000000000000027511260343353100213240ustar00rootroot00000000000000/* CommandModifyEvent.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); bool prepare() override; bool execute( ControllerInterface* ) override; bool rollback( ControllerInterface* ) override; bool finalize() override; public slots: void eventIdChanged(int,int) override; private: Event m_event; Event m_oldEvent; }; #endif Charm-1.10.0/Charm/Commands/CommandModifyTask.cpp000066400000000000000000000032361260343353100214770ustar00rootroot00000000000000/* CommandModifyTask.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/ControllerInterface.h" CommandModifyTask::CommandModifyTask( const Task& task, QObject* parent ) : CharmCommand( tr("Edit Task"), parent ) , m_task( task ) , m_success( false ) { } CommandModifyTask::~CommandModifyTask() { } bool CommandModifyTask::prepare() { return true; } bool CommandModifyTask::execute( ControllerInterface* 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; } #include "moc_CommandModifyTask.cpp" Charm-1.10.0/Charm/Commands/CommandModifyTask.h000066400000000000000000000025411260343353100211420ustar00rootroot00000000000000/* CommandModifyTask.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); bool prepare() override; bool execute( ControllerInterface* ) override; bool finalize() override; private: Task m_task; bool m_success; }; #endif Charm-1.10.0/Charm/Commands/CommandRelayCommand.cpp000066400000000000000000000041101260343353100217700ustar00rootroot00000000000000/* CommandRelayCommand.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 ) , m_payload( 0 ) { // 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( ControllerInterface* controller ) { return m_payload->execute( controller ); } bool CommandRelayCommand::rollback( ControllerInterface* controller ) { return m_payload->rollback( controller ); } bool CommandRelayCommand::finalize() { QApplication::restoreOverrideCursor(); m_payload->owner()->commitCommand( m_payload ); return true; } #include "moc_CommandRelayCommand.cpp" Charm-1.10.0/Charm/Commands/CommandRelayCommand.h000066400000000000000000000030671260343353100214470ustar00rootroot00000000000000/* CommandRelayCommand.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); void setCommand( CharmCommand* command ); bool prepare() override; bool execute( ControllerInterface* ) override; bool rollback( ControllerInterface* ) override; bool finalize() override; private: CharmCommand* m_payload; }; #endif Charm-1.10.0/Charm/Commands/CommandSetAllTasks.cpp000066400000000000000000000027771260343353100216300ustar00rootroot00000000000000/* CommandSetAllTasks.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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/ControllerInterface.h" CommandSetAllTasks::CommandSetAllTasks( const TaskList& tasks, QObject* parent ) : CharmCommand( tr("Import Tasks"), parent ) , m_tasks( tasks ) , m_success( false ) { } CommandSetAllTasks::~CommandSetAllTasks() { } bool CommandSetAllTasks::prepare() { return true; } bool CommandSetAllTasks::execute( ControllerInterface* controller ) { m_success = controller->setAllTasks( m_tasks ); return m_success; } bool CommandSetAllTasks::finalize() { return m_success; } #include "moc_CommandSetAllTasks.cpp" Charm-1.10.0/Charm/Commands/CommandSetAllTasks.h000066400000000000000000000024611260343353100212630ustar00rootroot00000000000000/* CommandSetAllTasks.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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(); bool prepare() override; bool execute( ControllerInterface* ) override; bool finalize() override; private: TaskList m_tasks; bool m_success; }; #endif Charm-1.10.0/Charm/Data.cpp000066400000000000000000000135371260343353100152430ustar00rootroot00000000000000/* Data.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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() { Q_ASSERT_X(!QPixmap(":/Charm/charmicon.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/charmicon.png")); 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 = QLatin1String(":/Charm/charmtray_mac.png"); #else static const QString iconPath = QLatin1String(":/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 = QLatin1String(":/Charm/charmtrayactive_mac.png"); #else static const QString iconPath = QLatin1String(":/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() { Q_ASSERT_X(!QPixmap(":/Charm/go.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/go.png")); return icon; } const QIcon& Data::stopIcon() { Q_ASSERT_X(!QPixmap(":/Charm/stop.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/stop.png")); return icon; } const QIcon& Data::newTaskIcon() { Q_ASSERT_X(!QPixmap(":/Charm/newtask.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/newtask.png")); return icon; } const QIcon& Data::newSubtaskIcon() { Q_ASSERT_X(!QPixmap(":/Charm/newsubtask.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/newsubtask.png")); return icon; } const QIcon& Data::editTaskIcon() { // FIXME same as edit-event icon Q_ASSERT_X(!QPixmap(":/Charm/edit.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/edit.png")); return icon; } const QIcon& Data::deleteTaskIcon() { Q_ASSERT_X(!QPixmap(":/Charm/deletetask.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/deletetask.png")); return icon; } const QIcon& Data::searchIcon() { Q_ASSERT_X(!QPixmap(":/Charm/search.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/search.png")); return icon; } const QIcon& Data::editEventIcon() { Q_ASSERT_X(!QPixmap(":/Charm/edit.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/edit.png")); return icon; } const QIcon& Data::createReportIcon() { Q_ASSERT_X(!QPixmap(":/Charm/createreport.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/createreport.png")); return icon; } const QPixmap& Data::activePixmap() { static QPixmap pixmap(":/Charm/active.png"); Q_ASSERT_X(!pixmap.isNull(), Q_FUNC_INFO, "Required resource not available"); return pixmap; } const QIcon& Data::quitCharmIcon() { Q_ASSERT_X(!QPixmap(":/Charm/quitcharm.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/quitcharm.png")); return icon; } const QIcon& Data::configureIcon() { Q_ASSERT_X(!QPixmap(":/Charm/configure.png").isNull(), Q_FUNC_INFO, "Required resource not available"); static QIcon icon(QPixmap(":/Charm/configure.png")); return icon; } const QPixmap& Data::editorLockedPixmap() { static QPixmap pixmap(":/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(":/Charm/editor_dirty.png"); Q_ASSERT_X(!pixmap.isNull(), Q_FUNC_INFO, "Required resource not available"); return pixmap; } Charm-1.10.0/Charm/Data.h000066400000000000000000000036461260343353100147100ustar00rootroot00000000000000/* Data.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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.10.0/Charm/EventModelAdapter.cpp000066400000000000000000000103601260343353100177240ustar00rootroot00000000000000/* EventModelAdapter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 id ) { 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(); } } #include "moc_EventModelAdapter.cpp" Charm-1.10.0/Charm/EventModelAdapter.h000066400000000000000000000057471260343353100174060ustar00rootroot00000000000000/* EventModelAdapter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 ); virtual ~EventModelAdapter(); 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 parentTask, int pos ) override {} void taskAdded( TaskId id ) override {} void taskModified( TaskId id ) override {} void taskParentChanged( TaskId, TaskId, TaskId ) 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 ) 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; 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.10.0/Charm/EventModelFilter.cpp000066400000000000000000000100601260343353100175660ustar00rootroot00000000000000/* EventModelFilter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 ) , m_filterId() { 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 ); return leftEvent.startDateTime() < rightEvent.startDateTime(); } 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(); if ( m_start.isValid() && startDate < 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; } #include "moc_EventModelFilter.cpp" Charm-1.10.0/Charm/EventModelFilter.h000066400000000000000000000046131260343353100172420ustar00rootroot00000000000000/* EventModelFilter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 ); virtual ~EventModelFilter(); /** 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; 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.10.0/Charm/GUIState.cpp000066400000000000000000000061051260343353100160100ustar00rootroot00000000000000/* GUIState.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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() : m_selectedTask( 0 ) , m_showExpired( false ) , m_showCurrents( false ) { } 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( QVariant variant, values ) { ids << variant.value(); } setExpandedTasks( ids ); setShowExpired( settings.value( MetaKey_MainWindowGUIStateShowExpiredTasks ).toBool() ); setShowCurrents( settings.value( MetaKey_MainWindowGUIStateShowCurrentTasks ).toBool() ); } } Charm-1.10.0/Charm/GUIState.h000066400000000000000000000033331260343353100154550ustar00rootroot00000000000000/* GUIState.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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; // show also expired tasks bool m_showCurrents; // show only selected tasks }; #endif Charm-1.10.0/Charm/HttpClient/000077500000000000000000000000001260343353100157335ustar00rootroot00000000000000Charm-1.10.0/Charm/HttpClient/CheckForUpdatesJob.cpp000066400000000000000000000076461260343353100221210ustar00rootroot00000000000000/* CheckForUpdatesJob.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2015 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, SIGNAL(finished(QNetworkReply*)), SLOT(jobFinished(QNetworkReply*))); 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() ).arg( 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( QLatin1String( "version" ) ); QDomElement linkElement = versionElement.nextSiblingElement( QLatin1String( "link" ) ); const QString releaseVersion = versionElement.text(); m_jobData.releaseVersion = releaseVersion; QUrl link( linkElement.text() ); m_jobData.link = link; QString releaseInfoLink( linkElement.nextSiblingElement( QLatin1String( "releaseinfolink" ) ).text() ); m_jobData.releaseInformationLink = releaseInfoLink; } void CheckForUpdatesJob::setUrl( const QUrl& url ) { m_url = url; } #include "moc_CheckForUpdatesJob.cpp" Charm-1.10.0/Charm/HttpClient/CheckForUpdatesJob.h000066400000000000000000000036251260343353100215570ustar00rootroot00000000000000/* CheckForUpdatesJob.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2015 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(); void start(); void setUrl( const QUrl& url ); void setVerbose( bool verbose ); signals: void finished( CheckForUpdatesJob::JobData data ); private slots: void jobFinished( QNetworkReply* reply ); private: void parseXmlData( const QByteArray& data ); QUrl m_url; JobData m_jobData; }; #endif // CHECKFORUPDATESJOB Charm-1.10.0/Charm/HttpClient/GetProjectCodesJob.cpp000066400000000000000000000047521260343353100221260ustar00rootroot00000000000000/* GetProjectCodesJob.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2015 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) , m_verbose( true ) { QSettings s; s.beginGroup(QLatin1String("httpconfig")); setDownloadUrl(s.value(QLatin1String("projectCodeDownloadUrl")).toUrl()); } GetProjectCodesJob::~GetProjectCodesJob() { } QByteArray GetProjectCodesJob::payload() const { return m_payload; } bool GetProjectCodesJob::execute(int state, QNetworkAccessManager *manager) { if (state != GetProjectCodes) return HttpJob::execute(state, manager); QNetworkRequest request(m_downloadUrl); QNetworkReply *reply = manager->get(request); if (reply->error() != QNetworkReply::NoError) setErrorAndEmitFinished(SomethingWentWrong, reply->errorString()); return true; } bool GetProjectCodesJob::handle(QNetworkReply *reply) { /* check for failure */ if (reply->error() != QNetworkReply::NoError) { setErrorAndEmitFinished(SomethingWentWrong, reply->errorString()); return false; } if (state() != GetProjectCodes) return HttpJob::handle(reply); m_payload = reply->readAll(); delayedNext(); return true; } 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; } #include "moc_GetProjectCodesJob.cpp" Charm-1.10.0/Charm/HttpClient/GetProjectCodesJob.h000066400000000000000000000031031260343353100215600ustar00rootroot00000000000000/* GetProjectCodesJob.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2015 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(); QByteArray payload() const; QUrl downloadUrl() const; void setDownloadUrl(const QUrl& url); void setVerbose(bool verbose); bool isVerbose() const; public slots: bool execute(int state, QNetworkAccessManager *manager) override; bool handle(QNetworkReply *reply) override; protected: enum State { GetProjectCodes = HttpJob::Base }; private: QByteArray m_payload; QUrl m_downloadUrl; bool m_verbose; }; #endif Charm-1.10.0/Charm/HttpClient/GetUserInfoJob.cpp000066400000000000000000000045011260343353100212640ustar00rootroot00000000000000/* GetUserInfoJob.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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 "GetUserInfoJob.h" #include #include #include #include GetUserInfoJob::GetUserInfoJob(QObject* parent, const QString &schema) : HttpJob(parent), m_schema(schema) { QSettings s; s.beginGroup(m_schema); setDownloadUrl(s.value(QLatin1String("userInfoDownloadUrl")).toUrl()); } GetUserInfoJob::~GetUserInfoJob() { } QByteArray GetUserInfoJob::userInfo() const { return m_userInfo; } QString GetUserInfoJob::schema() const { return m_schema; } void GetUserInfoJob::setSchema(const QString &schema) { m_schema = schema; } bool GetUserInfoJob::execute(int state, QNetworkAccessManager *manager) { if (state != GetProjectCodes) return HttpJob::execute(state, manager); QNetworkRequest request(m_downloadUrl); QNetworkReply *reply = manager->get(request); if (reply->error() != QNetworkReply::NoError) setErrorAndEmitFinished(SomethingWentWrong, reply->errorString()); return true; } bool GetUserInfoJob::handle(QNetworkReply *reply) { /* check for failure */ if (reply->error() != QNetworkReply::NoError) { setErrorAndEmitFinished(SomethingWentWrong, reply->errorString()); return false; } m_userInfo = reply->readAll(); delayedNext(); return true; } QUrl GetUserInfoJob::downloadUrl() const { return m_downloadUrl; } void GetUserInfoJob::setDownloadUrl(const QUrl& url) { m_downloadUrl = url; } Charm-1.10.0/Charm/HttpClient/GetUserInfoJob.h000066400000000000000000000032271260343353100207350ustar00rootroot00000000000000/* GetUserInfoJob.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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 GETUSERINFOJOB_H #define GETUSERINFOJOB_H #include "HttpJob.h" #include #include #include #include class GetUserInfoJob : public HttpJob { Q_OBJECT public: explicit GetUserInfoJob(QObject* parent=nullptr, const QString &schema = " "); ~GetUserInfoJob(); QByteArray userInfo() const; QUrl downloadUrl() const; void setDownloadUrl(const QUrl& url); QString schema() const; void setSchema(const QString &schema); public slots: bool execute(int state, QNetworkAccessManager *manager) override; bool handle(QNetworkReply *reply) override; protected: enum State { GetProjectCodes = HttpJob::Base }; private: QByteArray m_userInfo; QUrl m_downloadUrl; QString m_schema; }; #endif // GETUSERINFOJOB_H Charm-1.10.0/Charm/HttpClient/HttpJob.cpp000066400000000000000000000224571260343353100200230ustar00rootroot00000000000000/* HttpJob.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2015 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 "Keychain/keychain.h" #include #include #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(5,0,0) #include #endif static void setLastAuthenticationFailed(bool failed) { QSettings settings; settings.beginGroup("httpconfig"); settings.setValue(QLatin1String("lastAuthenticationFailed"), failed); } bool HttpJob::lastAuthenticationFailed() { QSettings settings; settings.beginGroup("httpconfig"); return settings.value(QLatin1String("lastAuthenticationFailed"), false).toBool(); } bool HttpJob::credentialsAvailable() { QSettings settings; settings.beginGroup("httpconfig"); return !settings.value(QLatin1String("username")).toString().isEmpty() && settings.value(QLatin1String("portalUrl")).toUrl().isValid() && settings.value(QLatin1String("loginUrl")).toUrl().isValid(); } QString HttpJob::extractErrorMessageFromReply(const QByteArray& xml) { QXmlStreamReader reader(xml); while (!reader.atEnd() && !reader.hasError()) { reader.readNext(); if (reader.isStartElement() && reader.name() == QLatin1String("div") && reader.attributes().value(QLatin1String("class")) == QLatin1String("ErrorResultMessage")) { return reader.readElementText(); } } return QString(); } HttpJob::HttpJob(QObject* parent) : QObject(parent) , m_networkManager(new QNetworkAccessManager(this)) , m_username() , m_password() , m_currentState(Ready) , m_errorCode(NoError) , m_lastAuthenticationFailed(true) , m_authenticationDoneAlready(false) , m_passwordReadError(false) { connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), SLOT(handle(QNetworkReply*))); connect(m_networkManager, SIGNAL(authenticationRequired(QNetworkReply*,QAuthenticator*)), SLOT(authenticationRequired(QNetworkReply*,QAuthenticator*))); QSettings settings; settings.beginGroup("httpconfig"); setUsername(settings.value(QLatin1String("username")).toString()); setPortalUrl(settings.value(QLatin1String("portalUrl")).toUrl()); setLoginUrl(settings.value(QLatin1String("loginUrl")).toUrl()); m_lastAuthenticationFailed = settings.value("lastAuthenticationFailed", false).toBool(); } 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; } QUrl HttpJob::portalUrl() const { return m_portalUrl; } void HttpJob::setPortalUrl(const QUrl &value) { m_portalUrl = value; } QUrl HttpJob::loginUrl() const { return m_loginUrl; } void HttpJob::setLoginUrl(const QUrl &value) { m_loginUrl = value; } int HttpJob::state() const { return m_currentState; } 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() || m_loginUrl.isEmpty() || m_portalUrl.isEmpty()) { setErrorAndEmitFinished(NotConfigured, tr("Timesheet upload and task list download not configured. Download and import the task list manually to configure them.")); return; } auto readJob = new ReadPasswordJob(QLatin1String("Charm"), this); connect(readJob, SIGNAL(finished(QKeychain::Job*)), this, SLOT(passwordRead(QKeychain::Job*))); readJob->setKey(QLatin1String("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(); const bool authenticationFailed = lastAuthenticationFailed(); if (oldpass.isEmpty() || authenticationFailed) { emit passwordRequested(); 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(QLatin1String("Charm"), this); connect(writeJob, SIGNAL(finished(QKeychain::Job*)), this, SLOT(passwordWritten())); writeJob->setKey(QLatin1String("lotsofcake")); writeJob->setTextData(m_password); writeJob->start(); } else { passwordWritten(); } } void HttpJob::passwordRequestCanceled() { setErrorAndEmitFinished(Canceled, tr("Canceled")); } void HttpJob::passwordWritten() { emit transferStarted(); delayedNext(); } void HttpJob::cancel() { QMetaObject::invokeMethod(this, "doCancel", Qt::QueuedConnection); } void HttpJob::doCancel() { setErrorAndEmitFinished(Canceled, tr("Canceled")); } void HttpJob::next() { /* go to the next state */ ++m_currentState; /* skip login if authenticationRequired() was called meanwhile */ if (m_authenticationDoneAlready && m_currentState == Login) ++m_currentState; /* finish if next state is not found */ if (!execute(m_currentState, m_networkManager)) { emitFinished(); return; } } void HttpJob::delayedNext() { QMetaObject::invokeMethod(this, "next", Qt::QueuedConnection); } bool HttpJob::execute(int state, QNetworkAccessManager *manager) { switch (state) { case Init: case Portal: { QNetworkRequest request(m_portalUrl); QNetworkReply *reply = manager->get(request); if (reply->error() != QNetworkReply::NoError) setErrorAndEmitFinished(SomethingWentWrong, reply->errorString()); } return true; case Login: { #if QT_VERSION < QT_VERSION_CHECK(5,0,0) QUrl data; data.addQueryItem("j_username", m_username); data.addQueryItem("j_password", m_password); QByteArray encodedQueryPlusPlus = data.encodedQuery().replace('+', "%2b").replace(' ', "+"); #else QUrlQuery urlQuery; urlQuery.addQueryItem("j_username", m_username); urlQuery.addQueryItem("j_password", m_password); QByteArray encodedQueryPlusPlus = urlQuery.query(QUrl::FullyEncoded).toUtf8().replace('+', "%2b"); #endif QNetworkRequest request(m_loginUrl); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setHeader(QNetworkRequest::ContentLengthHeader, encodedQueryPlusPlus.size()); QNetworkReply *reply = manager->post(request, encodedQueryPlusPlus); if (reply->error() != QNetworkReply::NoError) setErrorAndEmitFinished(SomethingWentWrong, reply->errorString()); } return true; default: break; } return false; } bool HttpJob::handle(QNetworkReply *reply) { // check for failure if (reply->error() != QNetworkReply::NoError) { setErrorAndEmitFinished(SomethingWentWrong, reply->errorString()); return false; } switch (m_currentState) { case Init: case Portal: { delayedNext(); return true; } case Login: { if (reply->header(QNetworkRequest::LocationHeader).isNull()) { setErrorAndEmitFinished(AuthenticationFailed, tr("Login failed. Wrong username or password.")); } else { delayedNext(); } return true; } default: break; } return false; } void HttpJob::authenticationRequired(QNetworkReply *reply , QAuthenticator *authenticator) { authenticator->setUser(m_username); authenticator->setPassword(m_password); m_authenticationDoneAlready = true; } void HttpJob::emitFinished() { if (m_errorCode == AuthenticationFailed) setLastAuthenticationFailed(true); else if (m_errorCode == NoError) setLastAuthenticationFailed(false); m_networkManager->disconnect(this); emit finished(this); deleteLater(); } void HttpJob::setErrorAndEmitFinished(int code, const QString& errorString) { m_errorCode = code; m_errorString = errorString; emitFinished(); } #include "moc_HttpJob.cpp" Charm-1.10.0/Charm/HttpClient/HttpJob.h000066400000000000000000000063121260343353100174600ustar00rootroot00000000000000/* HttpJob.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2015 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: static bool credentialsAvailable(); static bool lastAuthenticationFailed(); enum Error { NoError=0, Canceled, NotConfigured, AuthenticationFailed, SomethingWentWrong }; explicit HttpJob(QObject* parent=nullptr); ~HttpJob(); QString username() const; void setUsername(const QString &value); QString password() const; void setPassword(const QString &value); QUrl portalUrl() const; void setPortalUrl(const QUrl &value); QUrl loginUrl() const; void setLoginUrl(const QUrl &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(); protected: enum State { Ready = 0, Init, Login, Portal, Base }; int state() const; virtual bool execute(int state, QNetworkAccessManager *manager); void emitFinished(); void setErrorAndEmitFinished(int code, const QString& errorString); void delayedNext(); static QString extractErrorMessageFromReply(const QByteArray& xml); protected Q_SLOTS: virtual bool handle(QNetworkReply *reply); private Q_SLOTS: void doStart(); void doCancel(); void next(); void passwordRead(QKeychain::Job*); void passwordWritten(); void authenticationRequired(QNetworkReply *reply , QAuthenticator *authenticator); private: QNetworkAccessManager *m_networkManager; QString m_username; QString m_password; int m_currentState; int m_errorCode; QString m_errorString; QUrl m_loginUrl; QUrl m_portalUrl; bool m_lastAuthenticationFailed; bool m_authenticationDoneAlready; bool m_passwordReadError; }; #endif Charm-1.10.0/Charm/HttpClient/UploadTimesheetJob.cpp000066400000000000000000000077361260343353100222030ustar00rootroot00000000000000/* UploadTimesheetJob.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2015 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 // Required for Qt 4 #include UploadTimesheetJob::UploadTimesheetJob(QObject* parent) : HttpJob(parent), m_fileName("payload") { QSettings s; s.beginGroup(QLatin1String("httpconfig")); setUploadUrl(s.value(QLatin1String("timesheetUploadUrl")).toUrl()); } 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; } bool UploadTimesheetJob::execute(int state, QNetworkAccessManager *manager) { if (state != UploadTimesheet) return HttpJob::execute(state, manager); QByteArray data; QByteArray uploadName; /* validate filename */ if (!m_fileName.contains(QRegExp("^WeeklyTimeSheet-\\d\\d\\d\\d-\\d\\d$"))) { qDebug("Invalid filename encountered, using default (\"payload\")."); uploadName = "payload"; } else uploadName = m_fileName.toUtf8(); /* 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"; /* eot */ data += "--KDAB--\r\n"; QNetworkRequest request(m_uploadUrl); request.setHeader(QNetworkRequest::ContentTypeHeader, "multipart/form-data; boundary=KDAB"); request.setHeader(QNetworkRequest::ContentLengthHeader, data.size()); QNetworkReply *reply = manager->post(request, data); if (reply->error() != QNetworkReply::NoError) setErrorAndEmitFinished(SomethingWentWrong, reply->errorString()); return true; } bool UploadTimesheetJob::handle(QNetworkReply *reply) { /* check for failure */ if (reply->error() != QNetworkReply::NoError) { setErrorAndEmitFinished(SomethingWentWrong, reply->errorString()); return false; } if (state() != UploadTimesheet) return HttpJob::handle(reply); const QByteArray answer = reply->readAll(); if (answer.contains("SuccessResultMessage")) { delayedNext(); } else { const QString errorMessage = extractErrorMessageFromReply(answer); setErrorAndEmitFinished(SomethingWentWrong, !errorMessage.isEmpty() ? errorMessage : tr("An error occurred, could not extract details")); } return true; } #include "moc_UploadTimesheetJob.cpp" Charm-1.10.0/Charm/HttpClient/UploadTimesheetJob.h000066400000000000000000000031751260343353100216410ustar00rootroot00000000000000/* UploadTimesheetJob.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2015 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: explicit UploadTimesheetJob(QObject* parent=nullptr); ~UploadTimesheetJob(); QByteArray payload() const; void setPayload(const QByteArray &payload); QString fileName() const; void setFileName(const QString &fileName); QUrl uploadUrl() const; void setUploadUrl(const QUrl& url); public slots: bool execute(int state, QNetworkAccessManager *manager) override; bool handle(QNetworkReply *reply) override; protected: enum State { UploadTimesheet = HttpJob::Base }; private: QByteArray m_payload; QString m_fileName; QUrl m_uploadUrl; }; #endif Charm-1.10.0/Charm/Icons/000077500000000000000000000000001260343353100147305ustar00rootroot00000000000000Charm-1.10.0/Charm/Icons/Charm-128x128.png000066400000000000000000000360001260343353100174220ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/Charm-16x16.png000066400000000000000000000013751260343353100172610ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/Charm-256x256.png000066400000000000000000001427551260343353100174450ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/Charm-48x48.png000066400000000000000000000070571260343353100172760ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/Charm-64x64.png000066400000000000000000000116251260343353100172660ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/Charm.icns000066400000000000000000002715611260343353100166540ustar00rootroot00000000000000icnssqis32 !+*++*, 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.10.0/Charm/Icons/Charm.ico000066400000000000000000000727261260343353100164740ustar00rootroot00000000000000 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.10.0/Charm/Icons/Charm.svgz000066400000000000000000004221601260343353100167020ustar00rootroot00000000000000xז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.10.0/Charm/Icons/CharmDMG.icns000066400000000000000000002345601260343353100172020ustar00rootroot00000000000000icns9pis32@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.10.0/Charm/Icons/CharmDSStore000066400000000000000000000360041260343353100171540ustar00rootroot00000000000000Bud1    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.10.0/Charm/Icons/application-exit.png000066400000000000000000000015121260343353100207070ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/arrow-right.png000066400000000000000000000010171260343353100177020ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/back.png000066400000000000000000000006241260343353100163400ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/configure.png000066400000000000000000000013151260343353100174170ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/document-close.png000066400000000000000000000017721260343353100203660ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/document-edit.png000066400000000000000000000020451260343353100202000ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/document-encrypt.png000066400000000000000000000010611260343353100207340ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/document-export.png000066400000000000000000000014711260343353100205760ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/document-new.png000066400000000000000000000015511260343353100200450ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/edit-find-replace.png000066400000000000000000000022661260343353100207200ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/favorites.png000066400000000000000000000014021260343353100174350ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/filenew.png000066400000000000000000000014711260343353100170720ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/locationbar_erase.png000066400000000000000000000006141260343353100211130ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/media-playback-start.png000066400000000000000000000013401260343353100214320ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/media-playback-stop.png000066400000000000000000000013401260343353100212620ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/tray/000077500000000000000000000000001260343353100157075ustar00rootroot00000000000000Charm-1.10.0/Charm/Icons/tray/charmtray22.png000066400000000000000000000023341260343353100205550ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/tray/charmtray24.png000066400000000000000000000021551260343353100205600ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/tray/charmtray_mac.png000066400000000000000000000007241260343353100212320ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/tray/charmtrayactive22.png000066400000000000000000000022021260343353100217430ustar00rootroot00000000000000PNG  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.10.0/Charm/Icons/tray/charmtrayactive_mac.png000066400000000000000000000007351260343353100224300ustar00rootroot00000000000000PNG  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.10.0/Charm/Idle/000077500000000000000000000000001260343353100145325ustar00rootroot00000000000000Charm-1.10.0/Charm/Idle/IdleDetector.cpp000066400000000000000000000103321260343353100176040ustar00rootroot00000000000000/* IdleDetector.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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 "Core/Configuration.h" #include #include IdleDetector::IdleDetector( QObject* parent ) : QObject( parent ) , m_idlenessDuration( CHARM_IDLE_TIME ) // from CharmCMake.h , m_available( true ) { } 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_X11 X11IdleDetector* detector = new X11IdleDetector( parent ); detector->setAvailable( X11IdleDetector::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 ) { 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"; emit maybeIdle(); } } void IdleDetector::clear() { m_idlePeriods.clear(); } #include "moc_IdleDetector.cpp" Charm-1.10.0/Charm/Idle/IdleDetector.h000066400000000000000000000052501260343353100172540ustar00rootroot00000000000000/* IdleDetector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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; bool m_available; }; #endif Charm-1.10.0/Charm/Idle/MacIdleDetector.h000066400000000000000000000025051260343353100176750ustar00rootroot00000000000000/* MacIdleDetector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2011-2015 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.10.0/Charm/Idle/MacIdleDetector.mm000066400000000000000000000042651260343353100200640ustar00rootroot00000000000000#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.10.0/Charm/Idle/WindowsIdleDetector.cpp000066400000000000000000000040461260343353100211640ustar00rootroot00000000000000/* WindowsIdleDetector.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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, SIGNAL(timeout()), this, SLOT(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() ) ); } #include "moc_WindowsIdleDetector.cpp" Charm-1.10.0/Charm/Idle/WindowsIdleDetector.h000066400000000000000000000024131260343353100206250ustar00rootroot00000000000000/* WindowsIdleDetector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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(); private Q_SLOTS: void timeout(); private: QTimer m_timer; }; #endif // WINDOWSIDLEDETECTOR_H Charm-1.10.0/Charm/Idle/X11IdleDetector.cpp000066400000000000000000000051561260343353100201060ustar00rootroot00000000000000/* X11IdleDetector.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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" //TODO for Qt5 port to XCB... #if QT_VERSION < QT_VERSION_CHECK(5,0,0) #include #include #include #include #endif bool X11IdleDetector::idleCheckPossible() { #if QT_VERSION < QT_VERSION_CHECK(5,0,0) int event_base, error_base; if(XScreenSaverQueryExtension(QX11Info::display(), &event_base, &error_base)) return true; #endif return false; } X11IdleDetector::X11IdleDetector( QObject* parent ) : IdleDetector( parent ) { connect( &m_timer, SIGNAL(timeout()), this, SLOT(checkIdleness()) ); m_timer.start( idlenessDuration() * 1000 / 5 ); m_heartbeat = QDateTime::currentDateTime(); } void X11IdleDetector::onIdlenessDurationChanged() { m_timer.stop(); m_timer.start( idlenessDuration() * 1000 / 5 ); } void X11IdleDetector::checkIdleness() { #if QT_VERSION < QT_VERSION_CHECK(5,0,0) XScreenSaverInfo* _mit_info = XScreenSaverAllocInfo(); if (!_mit_info) return; XScreenSaverQueryInfo(QX11Info::display(), QX11Info::appRootWindow(), _mit_info); const int idleSecs = _mit_info->idle / 1000; XFree(_mit_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() ) ); #endif m_heartbeat = QDateTime::currentDateTime(); } #include "moc_X11IdleDetector.cpp" Charm-1.10.0/Charm/Idle/X11IdleDetector.h000066400000000000000000000025321260343353100175460ustar00rootroot00000000000000/* X11IdleDetector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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 class X11IdleDetector : public IdleDetector { Q_OBJECT public: explicit X11IdleDetector( QObject* parent ); static bool idleCheckPossible(); protected: void onIdlenessDurationChanged(); private slots: void checkIdleness(); private: QDateTime m_heartbeat; QTimer m_timer; }; #endif /* X11IDLEDETECTOR_H */ Charm-1.10.0/Charm/Keychain/000077500000000000000000000000001260343353100154105ustar00rootroot00000000000000Charm-1.10.0/Charm/Keychain/gnomekeyring.cpp000066400000000000000000000060321260343353100206130ustar00rootroot00000000000000#include "gnomekeyring_p.h" const char* GnomeKeyring::GNOME_KEYRING_DEFAULT = NULL; bool GnomeKeyring::isAvailable() { const GnomeKeyring& keyring = instance(); return keyring.isLoaded() && keyring.NETWORK_PASSWORD && keyring.is_available && keyring.find_password && keyring.store_password && keyring.delete_password && keyring.is_available(); } GnomeKeyring::gpointer GnomeKeyring::store_network_password( const gchar* keyring, const gchar* display_name, const gchar* user, const gchar* server, const gchar* password, OperationDoneCallback callback, gpointer data, GDestroyNotify destroy_data ) { if ( !isAvailable() ) return 0; return instance().store_password( instance().NETWORK_PASSWORD, keyring, display_name, password, callback, data, destroy_data, "user", user, "server", server, static_cast(0) ); } GnomeKeyring::gpointer GnomeKeyring::find_network_password( const gchar* user, const gchar* server, OperationGetStringCallback callback, gpointer data, GDestroyNotify destroy_data ) { if ( !isAvailable() ) return 0; return instance().find_password( instance().NETWORK_PASSWORD, callback, data, destroy_data, "user", user, "server", server, static_cast(0) ); } GnomeKeyring::gpointer GnomeKeyring::delete_network_password( const gchar* user, const gchar* server, OperationDoneCallback callback, gpointer data, GDestroyNotify destroy_data ) { if ( !isAvailable() ) return 0; return instance().delete_password( instance().NETWORK_PASSWORD, callback, data, destroy_data, "user", user, "server", server, static_cast(0) ); } GnomeKeyring::GnomeKeyring() : QLibrary("gnome-keyring", 0) { static const PasswordSchema schema = { ITEM_NETWORK_PASSWORD, {{ "user", ATTRIBUTE_TYPE_STRING }, { "server", ATTRIBUTE_TYPE_STRING }, { 0, static_cast( 0 ) }} }; NETWORK_PASSWORD = &schema; is_available = reinterpret_cast( resolve( "gnome_keyring_is_available" ) ); find_password = reinterpret_cast( resolve( "gnome_keyring_find_password" ) ); store_password = reinterpret_cast( resolve( "gnome_keyring_store_password" ) ); delete_password = reinterpret_cast( resolve( "gnome_keyring_delete_password" ) ); } GnomeKeyring& GnomeKeyring::instance() { static GnomeKeyring keyring; return keyring; } Charm-1.10.0/Charm/Keychain/gnomekeyring_p.h000066400000000000000000000062601260343353100206020ustar00rootroot00000000000000#ifndef QTKEYCHAIN_GNOME_P_H #define QTKEYCHAIN_GNOME_P_H #include class GnomeKeyring : private QLibrary { public: enum Result { RESULT_OK, RESULT_DENIED, RESULT_NO_KEYRING_DAEMON, RESULT_ALREADY_UNLOCKED, RESULT_NO_SUCH_KEYRING, RESULT_BAD_ARGUMENTS, RESULT_IO_ERROR, RESULT_CANCELLED, RESULT_KEYRING_ALREADY_EXISTS, RESULT_NO_MATCH }; enum ItemType { ITEM_GENERIC_SECRET = 0, ITEM_NETWORK_PASSWORD, ITEM_NOTE, ITEM_CHAINED_KEYRING_PASSWORD, ITEM_ENCRYPTION_KEY_PASSWORD, ITEM_PK_STORAGE = 0x100 }; enum AttributeType { ATTRIBUTE_TYPE_STRING, ATTRIBUTE_TYPE_UINT32 }; typedef char gchar; typedef void* gpointer; typedef bool gboolean; typedef struct { ItemType item_type; struct { const gchar* name; AttributeType type; } attributes[32]; } PasswordSchema; typedef void ( *OperationGetStringCallback )( Result result, const char* string, gpointer data ); typedef void ( *OperationDoneCallback )( Result result, gpointer data ); typedef void ( *GDestroyNotify )( gpointer data ); static const char* GNOME_KEYRING_DEFAULT; static bool isAvailable(); static gpointer store_network_password( const gchar* keyring, const gchar* display_name, const gchar* user, const gchar* server, const gchar* password, OperationDoneCallback callback, gpointer data, GDestroyNotify destroy_data ); static gpointer find_network_password( const gchar* user, const gchar* server, OperationGetStringCallback callback, gpointer data, GDestroyNotify destroy_data ); static gpointer delete_network_password( const gchar* user, const gchar* server, OperationDoneCallback callback, gpointer data, GDestroyNotify destroy_data ); private: GnomeKeyring(); static GnomeKeyring& instance(); const PasswordSchema* NETWORK_PASSWORD; typedef gboolean ( is_available_fn )( void ); typedef gpointer ( store_password_fn )( const PasswordSchema* schema, const gchar* keyring, const gchar* display_name, const gchar* password, OperationDoneCallback callback, gpointer data, GDestroyNotify destroy_data, ... ); typedef gpointer ( find_password_fn )( const PasswordSchema* schema, OperationGetStringCallback callback, gpointer data, GDestroyNotify destroy_data, ... ); typedef gpointer ( delete_password_fn )( const PasswordSchema* schema, OperationDoneCallback callback, gpointer data, GDestroyNotify destroy_data, ... ); is_available_fn* is_available; find_password_fn* find_password; store_password_fn* store_password; delete_password_fn* delete_password; }; #endif Charm-1.10.0/Charm/Keychain/keychain.cpp000066400000000000000000000133371260343353100177160ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2011-2014 Frank Osterfeld * * * * 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. For licensing and distribution * * details, check the accompanying file 'COPYING'. * *****************************************************************************/ #include "keychain.h" #include "keychain_p.h" using namespace QKeychain; Job::Job( const QString& service, QObject *parent ) : QObject( parent ) , d ( new JobPrivate( service ) ) { } Job::~Job() { delete d; } QString Job::service() const { return d->service; } QSettings* Job::settings() const { return d->settings; } void Job::setSettings( QSettings* settings ) { d->settings = settings; } void Job::start() { QMetaObject::invokeMethod( this, "doStart", Qt::QueuedConnection ); } bool Job::autoDelete() const { return d->autoDelete; } void Job::setAutoDelete( bool autoDelete ) { d->autoDelete = autoDelete; } bool Job::insecureFallback() const { return d->insecureFallback; } void Job::setInsecureFallback( bool insecureFallback ) { d->insecureFallback = insecureFallback; } void Job::emitFinished() { emit finished( this ); if ( d->autoDelete ) deleteLater(); } void Job::emitFinishedWithError( Error error, const QString& errorString ) { d->error = error; d->errorString = errorString; emitFinished(); } Error Job::error() const { return d->error; } QString Job::errorString() const { return d->errorString; } void Job::setError( Error error ) { d->error = error; } void Job::setErrorString( const QString& errorString ) { d->errorString = errorString; } ReadPasswordJob::ReadPasswordJob( const QString& service, QObject* parent ) : Job( service, parent ) , d( new ReadPasswordJobPrivate( this ) ) {} ReadPasswordJob::~ReadPasswordJob() { delete d; } QString ReadPasswordJob::textData() const { return QString::fromUtf8( d->data ); } QByteArray ReadPasswordJob::binaryData() const { return d->data; } QString ReadPasswordJob::key() const { return d->key; } void ReadPasswordJob::setKey( const QString& key ) { d->key = key; } void ReadPasswordJob::doStart() { JobExecutor::instance()->enqueue( this ); } WritePasswordJob::WritePasswordJob( const QString& service, QObject* parent ) : Job( service, parent ) , d( new WritePasswordJobPrivate( this ) ) { } WritePasswordJob::~WritePasswordJob() { delete d; } QString WritePasswordJob::key() const { return d->key; } void WritePasswordJob::setKey( const QString& key ) { d->key = key; } void WritePasswordJob::setBinaryData( const QByteArray& data ) { d->binaryData = data; d->mode = WritePasswordJobPrivate::Binary; } void WritePasswordJob::setTextData( const QString& data ) { d->textData = data; d->mode = WritePasswordJobPrivate::Text; } void WritePasswordJob::doStart() { JobExecutor::instance()->enqueue( this ); } DeletePasswordJob::DeletePasswordJob( const QString& service, QObject* parent ) : Job( service, parent ) , d( new DeletePasswordJobPrivate( this ) ) { } DeletePasswordJob::~DeletePasswordJob() { delete d; } void DeletePasswordJob::doStart() { //Internally, to delete a password we just execute a write job with no data set (null byte array). //In all current implementations, this deletes the entry so this is sufficient WritePasswordJob* job = new WritePasswordJob( service(), this ); connect( job, SIGNAL(finished(QKeychain::Job*)), d, SLOT(jobFinished(QKeychain::Job*)) ); job->setInsecureFallback(true); job->setSettings(settings()); job->setKey( d->key ); job->doStart(); } QString DeletePasswordJob::key() const { return d->key; } void DeletePasswordJob::setKey( const QString& key ) { d->key = key; } void DeletePasswordJobPrivate::jobFinished( Job* job ) { q->setError( job->error() ); q->setErrorString( job->errorString() ); q->emitFinished(); } JobExecutor::JobExecutor() : QObject( 0 ) , m_runningJob( 0 ) { } void JobExecutor::enqueue( Job* job ) { m_queue.append( job ); startNextIfNoneRunning(); } void JobExecutor::startNextIfNoneRunning() { if ( m_queue.isEmpty() || m_runningJob ) return; QPointer next; while ( !next && !m_queue.isEmpty() ) { next = m_queue.first(); m_queue.pop_front(); } if ( next ) { connect( next, SIGNAL(finished(QKeychain::Job*)), this, SLOT(jobFinished(QKeychain::Job*)) ); connect( next, SIGNAL(destroyed(QObject*)), this, SLOT(jobDestroyed(QObject*)) ); m_runningJob = next; if ( ReadPasswordJob* rpj = qobject_cast( m_runningJob ) ) rpj->d->scheduledStart(); else if ( WritePasswordJob* wpj = qobject_cast( m_runningJob) ) wpj->d->scheduledStart(); } } void JobExecutor::jobDestroyed( QObject* object ) { Q_UNUSED( object ) // for release mode Q_ASSERT( object == m_runningJob ); m_runningJob->disconnect( this ); m_runningJob = 0; startNextIfNoneRunning(); } void JobExecutor::jobFinished( Job* job ) { Q_UNUSED( job ) // for release mode Q_ASSERT( job == m_runningJob ); m_runningJob->disconnect( this ); m_runningJob = 0; startNextIfNoneRunning(); } JobExecutor* JobExecutor::s_instance = 0; JobExecutor* JobExecutor::instance() { if ( !s_instance ) s_instance = new JobExecutor; return s_instance; } Charm-1.10.0/Charm/Keychain/keychain.h000066400000000000000000000072241260343353100173610ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2011-2014 Frank Osterfeld * * * * 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. For licensing and distribution * * details, check the accompanying file 'COPYING'. * *****************************************************************************/ #ifndef KEYCHAIN_H #define KEYCHAIN_H #include #include class QSettings; #define QTKEYCHAIN_VERSION 0x000100 namespace QKeychain { /** * Error codes */ enum Error { NoError=0, /**< No error occurred, operation was successful */ EntryNotFound, /**< For the given key no data was found */ CouldNotDeleteEntry, /**< Could not delete existing secret data */ AccessDeniedByUser, /**< User denied access to keychain */ AccessDenied, /**< Access denied for other reasons */ NoBackendAvailable, /**< No platform-specific keychain service available */ NotImplemented, /**< Not implemented on platform */ OtherError /**< Something else went wrong (errorString() might provide details) */ }; class JobExecutor; class JobPrivate; class Job : public QObject { Q_OBJECT public: explicit Job( const QString& service, QObject* parent=0 ); ~Job(); QSettings* settings() const; void setSettings( QSettings* settings ); void start(); QString service() const; Error error() const; QString errorString() const; bool autoDelete() const; void setAutoDelete( bool autoDelete ); bool insecureFallback() const; void setInsecureFallback( bool insecureFallback ); Q_SIGNALS: void finished( QKeychain::Job* ); protected: Q_INVOKABLE virtual void doStart() = 0; void setError( Error error ); void setErrorString( const QString& errorString ); void emitFinished(); void emitFinishedWithError(Error, const QString& errorString); private: JobPrivate* const d; }; class ReadPasswordJobPrivate; class ReadPasswordJob : public Job { Q_OBJECT public: explicit ReadPasswordJob( const QString& service, QObject* parent=0 ); ~ReadPasswordJob(); QString key() const; void setKey( const QString& key ); QByteArray binaryData() const; QString textData() const; protected: void doStart(); private: friend class QKeychain::ReadPasswordJobPrivate; friend class QKeychain::JobExecutor; ReadPasswordJobPrivate* const d; }; class WritePasswordJobPrivate; class WritePasswordJob : public Job { Q_OBJECT public: explicit WritePasswordJob( const QString& service, QObject* parent=0 ); ~WritePasswordJob(); QString key() const; void setKey( const QString& key ); void setBinaryData( const QByteArray& data ); void setTextData( const QString& data ); protected: void doStart(); private: friend class QKeychain::JobExecutor; friend class QKeychain::WritePasswordJobPrivate; friend class DeletePasswordJob; WritePasswordJobPrivate* const d; }; class DeletePasswordJobPrivate; class DeletePasswordJob : public Job { Q_OBJECT public: explicit DeletePasswordJob( const QString& service, QObject* parent=0 ); ~DeletePasswordJob(); QString key() const; void setKey( const QString& key ); protected: void doStart(); private: friend class QKeychain::DeletePasswordJobPrivate; DeletePasswordJobPrivate* const d; }; } // namespace QtKeychain #endif Charm-1.10.0/Charm/Keychain/keychain_mac.cpp000066400000000000000000000135221260343353100205320ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2011-2014 Frank Osterfeld * * * * 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. For licensing and distribution * * details, check the accompanying file 'COPYING'. * *****************************************************************************/ #include "keychain_p.h" #include #include #include using namespace QKeychain; template struct Releaser { explicit Releaser( const T& v ) : value( v ) {} ~Releaser() { CFRelease( value ); } const T value; }; static QString strForStatus( OSStatus os ) { const Releaser str( SecCopyErrorMessageString( os, 0 ) ); const char * const buf = CFStringGetCStringPtr( str.value, kCFStringEncodingUTF8 ); if ( !buf ) return QObject::tr( "%1 (OSStatus %2)" ) .arg( "OSX Keychain Error" ).arg( os ); return QObject::tr( "%1 (OSStatus %2)" ) .arg( QString::fromUtf8( buf, strlen( buf ) ) ).arg( os ); } static OSStatus readPw( QByteArray* pw, const QString& service, const QString& account, SecKeychainItemRef* ref ) { Q_ASSERT( pw ); pw->clear(); const QByteArray serviceData = service.toUtf8(); const QByteArray accountData = account.toUtf8(); void* data = 0; UInt32 len = 0; const OSStatus ret = SecKeychainFindGenericPassword( NULL, // default keychain serviceData.size(), serviceData.constData(), accountData.size(), accountData.constData(), &len, &data, ref ); if ( ret == noErr ) { *pw = QByteArray( reinterpret_cast( data ), len ); const OSStatus ret2 = SecKeychainItemFreeContent ( 0, data ); if ( ret2 != noErr ) qWarning() << "Could not free item content: " << strForStatus( ret2 ); } return ret; } void ReadPasswordJobPrivate::scheduledStart() { QString errorString; Error error = NoError; const OSStatus ret = readPw( &data, q->service(), q->key(), 0 ); switch ( ret ) { case noErr: break; case errSecItemNotFound: errorString = tr("Password not found"); error = EntryNotFound; break; default: errorString = strForStatus( ret ); error = OtherError; break; } q->emitFinishedWithError( error, errorString ); } static QKeychain::Error deleteEntryImpl( const QString& service, const QString& account, QString* err ) { SecKeychainItemRef ref; QByteArray pw; const OSStatus ret1 = readPw( &pw, service, account, &ref ); if ( ret1 == errSecItemNotFound ) return NoError; // No item stored, we're done if ( ret1 != noErr ) { *err = strForStatus( ret1 ); //TODO map error code, set errstr return OtherError; } const Releaser releaser( ref ); const OSStatus ret2 = SecKeychainItemDelete( ref ); if ( ret2 == noErr ) return NoError; //TODO map error code *err = strForStatus( ret2 ); return CouldNotDeleteEntry; } static QKeychain::Error writeEntryImpl( const QString& service, const QString& account, const QByteArray& data, QString* err ) { Q_ASSERT( err ); err->clear(); const QByteArray serviceData = service.toUtf8(); const QByteArray accountData = account.toUtf8(); const OSStatus ret = SecKeychainAddGenericPassword( NULL, //default keychain serviceData.size(), serviceData.constData(), accountData.size(), accountData.constData(), data.size(), data.constData(), NULL //item reference ); if ( ret != noErr ) { switch ( ret ) { case errSecDuplicateItem: { Error derr = deleteEntryImpl( service, account, err ); if ( derr != NoError ) return CouldNotDeleteEntry; else return writeEntryImpl( service, account, data, err ); } default: *err = strForStatus( ret ); return OtherError; } } return NoError; } void WritePasswordJobPrivate::scheduledStart() { QString errorString; Error error = NoError; if ( mode == Delete ) { const Error derr = deleteEntryImpl( q->service(), key, &errorString ); if ( derr != NoError ) error = CouldNotDeleteEntry; q->emitFinishedWithError( error, errorString ); return; } const QByteArray data = mode == Text ? textData.toUtf8() : binaryData; error = writeEntryImpl( q->service(), key, data, &errorString ); q->emitFinishedWithError( error, errorString ); } Charm-1.10.0/Charm/Keychain/keychain_p.h000066400000000000000000000107011260343353100176720ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2011-2014 Frank Osterfeld * * * * 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. For licensing and distribution * * details, check the accompanying file 'COPYING'. * *****************************************************************************/ #ifndef KEYCHAIN_P_H #define KEYCHAIN_P_H #include #include #include #include #include #if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) #include #include "kwallet_interface.h" #else class QDBusPendingCallWatcher; #endif #include "keychain.h" namespace QKeychain { class JobExecutor; class JobPrivate : public QObject { Q_OBJECT public: JobPrivate( const QString& service_ ) : error( NoError ) , service( service_ ) , autoDelete( true ) , insecureFallback( false ) {} QKeychain::Error error; QString errorString; QString service; bool autoDelete; bool insecureFallback; QPointer settings; }; class ReadPasswordJobPrivate : public QObject { Q_OBJECT public: explicit ReadPasswordJobPrivate( ReadPasswordJob* qq ) : q( qq ), walletHandle( 0 ), dataType( Text ) {} void scheduledStart(); ReadPasswordJob* const q; QByteArray data; QString key; int walletHandle; enum DataType { Binary, Text }; DataType dataType; #if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) org::kde::KWallet* iface; static void gnomeKeyring_cb( int result, const char* string, ReadPasswordJobPrivate* data ); friend class QKeychain::JobExecutor; void fallbackOnError(const QDBusError& err); private Q_SLOTS: void kwalletWalletFound( QDBusPendingCallWatcher* watcher ); void kwalletOpenFinished( QDBusPendingCallWatcher* watcher ); void kwalletEntryTypeFinished( QDBusPendingCallWatcher* watcher ); void kwalletReadFinished( QDBusPendingCallWatcher* watcher ); #else //moc's too dumb to respect above macros, so just define empty slot implementations private Q_SLOTS: void kwalletWalletFound( QDBusPendingCallWatcher* ) {} void kwalletOpenFinished( QDBusPendingCallWatcher* ) {} void kwalletEntryTypeFinished( QDBusPendingCallWatcher* ) {} void kwalletReadFinished( QDBusPendingCallWatcher* ) {} #endif }; class WritePasswordJobPrivate : public QObject { Q_OBJECT public: explicit WritePasswordJobPrivate( WritePasswordJob* qq ) : q( qq ), mode( Delete ) {} void scheduledStart(); enum Mode { Delete, Text, Binary }; static QString modeToString(Mode m); static Mode stringToMode(const QString& s); WritePasswordJob* const q; Mode mode; QString key; QByteArray binaryData; QString textData; #if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) org::kde::KWallet* iface; static void gnomeKeyring_cb( int result, WritePasswordJobPrivate* self ); friend class QKeychain::JobExecutor; void fallbackOnError(const QDBusError& err); private Q_SLOTS: void kwalletWalletFound( QDBusPendingCallWatcher* watcher ); void kwalletOpenFinished( QDBusPendingCallWatcher* watcher ); void kwalletWriteFinished( QDBusPendingCallWatcher* watcher ); #else private Q_SLOTS: void kwalletWalletFound( QDBusPendingCallWatcher* ) {} void kwalletOpenFinished( QDBusPendingCallWatcher* ) {} void kwalletWriteFinished( QDBusPendingCallWatcher* ) {} #endif }; class DeletePasswordJobPrivate : public QObject { Q_OBJECT public: explicit DeletePasswordJobPrivate( DeletePasswordJob* qq ) : q( qq ) {} void doStart(); DeletePasswordJob* const q; QString key; private Q_SLOTS: void jobFinished( QKeychain::Job* ); }; class JobExecutor : public QObject { Q_OBJECT public: static JobExecutor* instance(); void enqueue( Job* job ); private: explicit JobExecutor(); void startNextIfNoneRunning(); private Q_SLOTS: void jobFinished( QKeychain::Job* ); void jobDestroyed( QObject* object ); private: static JobExecutor* s_instance; Job* m_runningJob; QVector > m_queue; }; } #endif // KEYCHAIN_P_H Charm-1.10.0/Charm/Keychain/keychain_unix.cpp000066400000000000000000000454661260343353100207710ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2011-2014 Frank Osterfeld * * * * 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. For licensing and distribution * * details, check the accompanying file 'COPYING'. * *****************************************************************************/ #include "keychain_p.h" #include "gnomekeyring_p.h" #include #include using namespace QKeychain; static QString typeKey( const QString& key ) { return QString::fromLatin1( "%1/type" ).arg( key ); } static QString dataKey( const QString& key ) { return QString::fromLatin1( "%1/data" ).arg( key ); } enum KeyringBackend { Backend_GnomeKeyring, Backend_Kwallet4, Backend_Kwallet5 }; enum DesktopEnvironment { DesktopEnv_Gnome, DesktopEnv_Kde4, DesktopEnv_Plasma5, DesktopEnv_Unity, DesktopEnv_Xfce, DesktopEnv_Other }; // the following detection algorithm is derived from chromium, // licensed under BSD, see base/nix/xdg_util.cc static DesktopEnvironment getKdeVersion() { QString value = qgetenv("KDE_SESSION_VERSION"); if ( value == "5" ) { return DesktopEnv_Plasma5; } else if (value == "4" ) { return DesktopEnv_Kde4; } else { // most likely KDE3 return DesktopEnv_Other; } } static DesktopEnvironment detectDesktopEnvironment() { QByteArray xdgCurrentDesktop = qgetenv("XDG_CURRENT_DESKTOP"); if ( xdgCurrentDesktop == "GNOME" ) { return DesktopEnv_Gnome; } else if ( xdgCurrentDesktop == "Unity" ) { return DesktopEnv_Unity; } else if ( xdgCurrentDesktop == "KDE" ) { return getKdeVersion(); } QByteArray desktopSession = qgetenv("DESKTOP_SESSION"); if ( desktopSession == "gnome" ) { return DesktopEnv_Gnome; } else if ( desktopSession == "kde" ) { return getKdeVersion(); } else if ( desktopSession == "kde4" ) { return DesktopEnv_Kde4; } else if ( desktopSession.contains("xfce") || desktopSession == "xubuntu" ) { return DesktopEnv_Xfce; } if ( !qgetenv("GNOME_DESKTOP_SESSION_ID").isEmpty() ) { return DesktopEnv_Gnome; } else if ( !qgetenv("KDE_FULL_SESSION").isEmpty() ) { return getKdeVersion(); } return DesktopEnv_Other; } static KeyringBackend detectKeyringBackend() { switch (detectDesktopEnvironment()) { case DesktopEnv_Kde4: return Backend_Kwallet4; break; case DesktopEnv_Plasma5: return Backend_Kwallet5; break; // fall through case DesktopEnv_Gnome: case DesktopEnv_Unity: case DesktopEnv_Xfce: case DesktopEnv_Other: default: if ( GnomeKeyring::isAvailable() ) { return Backend_GnomeKeyring; } else { return Backend_Kwallet4; } } } static KeyringBackend getKeyringBackend() { static KeyringBackend backend = detectKeyringBackend(); return backend; } static void kwalletReadPasswordScheduledStartImpl(const char * service, const char * path, ReadPasswordJobPrivate * priv) { if ( QDBusConnection::sessionBus().isConnected() ) { priv->iface = new org::kde::KWallet( QLatin1String(service), QLatin1String(path), QDBusConnection::sessionBus(), priv ); const QDBusPendingReply reply = priv->iface->networkWallet(); QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher( reply, priv ); priv->connect( watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), priv, SLOT(kwalletWalletFound(QDBusPendingCallWatcher*)) ); } else { // D-Bus is not reachable so none can tell us something about KWalletd QDBusError err( QDBusError::NoServer, priv->tr("D-Bus is not running") ); priv->fallbackOnError( err ); } } void ReadPasswordJobPrivate::scheduledStart() { switch ( getKeyringBackend() ) { case Backend_GnomeKeyring: if ( !GnomeKeyring::find_network_password( key.toUtf8().constData(), q->service().toUtf8().constData(), reinterpret_cast( &ReadPasswordJobPrivate::gnomeKeyring_cb ), this, 0 ) ) q->emitFinishedWithError( OtherError, tr("Unknown error") ); break; case Backend_Kwallet4: kwalletReadPasswordScheduledStartImpl("org.kde.kwalletd", "/modules/kwalletd", this); break; case Backend_Kwallet5: kwalletReadPasswordScheduledStartImpl("org.kde.kwalletd5", "/modules/kwalletd5", this); break; } } void ReadPasswordJobPrivate::kwalletWalletFound(QDBusPendingCallWatcher *watcher) { watcher->deleteLater(); const QDBusPendingReply reply = *watcher; const QDBusPendingReply pendingReply = iface->open( reply.value(), 0, q->service() ); QDBusPendingCallWatcher* pendingWatcher = new QDBusPendingCallWatcher( pendingReply, this ); connect( pendingWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletOpenFinished(QDBusPendingCallWatcher*)) ); } static QPair mapGnomeKeyringError( int result ) { Q_ASSERT( result != GnomeKeyring::RESULT_OK ); switch ( result ) { case GnomeKeyring::RESULT_DENIED: return qMakePair( AccessDenied, QObject::tr("Access to keychain denied") ); case GnomeKeyring::RESULT_NO_KEYRING_DAEMON: return qMakePair( NoBackendAvailable, QObject::tr("No keyring daemon") ); case GnomeKeyring::RESULT_ALREADY_UNLOCKED: return qMakePair( OtherError, QObject::tr("Already unlocked") ); case GnomeKeyring::RESULT_NO_SUCH_KEYRING: return qMakePair( OtherError, QObject::tr("No such keyring") ); case GnomeKeyring::RESULT_BAD_ARGUMENTS: return qMakePair( OtherError, QObject::tr("Bad arguments") ); case GnomeKeyring::RESULT_IO_ERROR: return qMakePair( OtherError, QObject::tr("I/O error") ); case GnomeKeyring::RESULT_CANCELLED: return qMakePair( OtherError, QObject::tr("Cancelled") ); case GnomeKeyring::RESULT_KEYRING_ALREADY_EXISTS: return qMakePair( OtherError, QObject::tr("Keyring already exists") ); case GnomeKeyring::RESULT_NO_MATCH: return qMakePair( EntryNotFound, QObject::tr("No match") ); default: break; } return qMakePair( OtherError, QObject::tr("Unknown error") ); } void ReadPasswordJobPrivate::gnomeKeyring_cb( int result, const char* string, ReadPasswordJobPrivate* self ) { if ( result == GnomeKeyring::RESULT_OK ) { if ( self->dataType == ReadPasswordJobPrivate::Text ) self->data = string; else self->data = QByteArray::fromBase64( string ); self->q->emitFinished(); } else { const QPair errorResult = mapGnomeKeyringError( result ); self->q->emitFinishedWithError( errorResult.first, errorResult.second ); } } void ReadPasswordJobPrivate::fallbackOnError(const QDBusError& err ) { QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.data(); if ( q->insecureFallback() && actual->contains( dataKey( key ) ) ) { const WritePasswordJobPrivate::Mode mode = WritePasswordJobPrivate::stringToMode( actual->value( typeKey( key ) ).toString() ); if (mode == WritePasswordJobPrivate::Binary) dataType = Binary; else dataType = Text; data = actual->value( dataKey( key ) ).toByteArray(); q->emitFinished(); } else { if ( err.type() == QDBusError::ServiceUnknown ) //KWalletd not running q->emitFinishedWithError( NoBackendAvailable, tr("No keychain service available") ); else q->emitFinishedWithError( OtherError, tr("Could not open wallet: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); } } void ReadPasswordJobPrivate::kwalletOpenFinished( QDBusPendingCallWatcher* watcher ) { watcher->deleteLater(); const QDBusPendingReply reply = *watcher; QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.data(); if ( reply.isError() ) { fallbackOnError( reply.error() ); return; } if ( actual->contains( dataKey( key ) ) ) { // We previously stored data in the insecure QSettings, but now have KWallet available. // Do the migration data = actual->value( dataKey( key ) ).toByteArray(); const WritePasswordJobPrivate::Mode mode = WritePasswordJobPrivate::stringToMode( actual->value( typeKey( key ) ).toString() ); actual->remove( key ); q->emitFinished(); WritePasswordJob* j = new WritePasswordJob( q->service(), 0 ); j->setSettings( q->settings() ); j->setKey( key ); j->setAutoDelete( true ); if ( mode == WritePasswordJobPrivate::Binary ) j->setBinaryData( data ); else if ( mode == WritePasswordJobPrivate::Text ) j->setTextData( QString::fromUtf8( data ) ); else Q_ASSERT( false ); j->start(); return; } walletHandle = reply.value(); if ( walletHandle < 0 ) { q->emitFinishedWithError( AccessDenied, tr("Access to keychain denied") ); return; } const QDBusPendingReply nextReply = iface->entryType( walletHandle, q->service(), key, q->service() ); QDBusPendingCallWatcher* nextWatcher = new QDBusPendingCallWatcher( nextReply, this ); connect( nextWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletEntryTypeFinished(QDBusPendingCallWatcher*)) ); } //Must be in sync with KWallet::EntryType (kwallet.h) enum KWalletEntryType { Unknown=0, Password, Stream, Map }; void ReadPasswordJobPrivate::kwalletEntryTypeFinished( QDBusPendingCallWatcher* watcher ) { watcher->deleteLater(); if ( watcher->isError() ) { const QDBusError err = watcher->error(); q->emitFinishedWithError( OtherError, tr("Could not determine data type: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); return; } const QDBusPendingReply reply = *watcher; const int value = reply.value(); switch ( value ) { case Unknown: q->emitFinishedWithError( EntryNotFound, tr("Entry not found") ); return; case Password: dataType = Text; break; case Stream: dataType = Binary; break; case Map: q->emitFinishedWithError( EntryNotFound, tr("Unsupported entry type 'Map'") ); return; default: q->emitFinishedWithError( OtherError, tr("Unknown kwallet entry type '%1'").arg( value ) ); return; } const QDBusPendingCall nextReply = dataType == Text ? QDBusPendingCall( iface->readPassword( walletHandle, q->service(), key, q->service() ) ) : QDBusPendingCall( iface->readEntry( walletHandle, q->service(), key, q->service() ) ); QDBusPendingCallWatcher* nextWatcher = new QDBusPendingCallWatcher( nextReply, this ); connect( nextWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletReadFinished(QDBusPendingCallWatcher*)) ); } void ReadPasswordJobPrivate::kwalletReadFinished( QDBusPendingCallWatcher* watcher ) { watcher->deleteLater(); if ( watcher->isError() ) { const QDBusError err = watcher->error(); q->emitFinishedWithError( OtherError, tr("Could not read password: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); return; } if ( dataType == Binary ) { QDBusPendingReply reply = *watcher; data = reply.value(); } else { QDBusPendingReply reply = *watcher; data = reply.value().toUtf8(); } q->emitFinished(); } static void kwalletWritePasswordScheduledStart( const char * service, const char * path, WritePasswordJobPrivate * priv ) { if ( QDBusConnection::sessionBus().isConnected() ) { priv->iface = new org::kde::KWallet( QLatin1String(service), QLatin1String(path), QDBusConnection::sessionBus(), priv ); const QDBusPendingReply reply = priv->iface->networkWallet(); QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher( reply, priv ); priv->connect( watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), priv, SLOT(kwalletWalletFound(QDBusPendingCallWatcher*)) ); } else { // D-Bus is not reachable so none can tell us something about KWalletd QDBusError err( QDBusError::NoServer, priv->tr("D-Bus is not running") ); priv->fallbackOnError( err ); } } void WritePasswordJobPrivate::scheduledStart() { switch ( getKeyringBackend() ) { case Backend_GnomeKeyring: if ( mode == WritePasswordJobPrivate::Delete ) { if ( !GnomeKeyring::delete_network_password( key.toUtf8().constData(), q->service().toUtf8().constData(), reinterpret_cast( &WritePasswordJobPrivate::gnomeKeyring_cb ), this, 0 ) ) q->emitFinishedWithError( OtherError, tr("Unknown error") ); } else { QByteArray password = mode == WritePasswordJobPrivate::Text ? textData.toUtf8() : binaryData.toBase64(); QByteArray service = q->service().toUtf8(); if ( !GnomeKeyring::store_network_password( GnomeKeyring::GNOME_KEYRING_DEFAULT, service.constData(), key.toUtf8().constData(), service.constData(), password.constData(), reinterpret_cast( &WritePasswordJobPrivate::gnomeKeyring_cb ), this, 0 ) ) q->emitFinishedWithError( OtherError, tr("Unknown error") ); } break; case Backend_Kwallet4: kwalletWritePasswordScheduledStart("org.kde.kwalletd", "/modules/kwalletd", this); break; case Backend_Kwallet5: kwalletWritePasswordScheduledStart("org.kde.kwalletd5", "/modules/kwalletd5", this); break; } } QString WritePasswordJobPrivate::modeToString(Mode m) { switch (m) { case Delete: return QLatin1String("Delete"); case Text: return QLatin1String("Text"); case Binary: return QLatin1String("Binary"); } Q_ASSERT_X(false, Q_FUNC_INFO, "Unhandled Mode value"); return QString(); } WritePasswordJobPrivate::Mode WritePasswordJobPrivate::stringToMode(const QString& s) { if (s == QLatin1String("Delete") || s == QLatin1String("0")) return Delete; if (s == QLatin1String("Text") || s == QLatin1String("1")) return Text; if (s == QLatin1String("Binary") || s == QLatin1String("2")) return Binary; qCritical("Unexpected mode string '%s'", qPrintable(s)); return Text; } void WritePasswordJobPrivate::fallbackOnError(const QDBusError &err) { QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.data(); if ( !q->insecureFallback() ) { q->emitFinishedWithError( OtherError, tr("Could not open wallet: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); return; } if ( mode == Delete ) { actual->remove( key ); actual->sync(); q->emitFinished(); return; } actual->setValue( QString::fromLatin1( "%1/type" ).arg( key ), mode ); if ( mode == Text ) actual->setValue( QString::fromLatin1( "%1/data" ).arg( key ), textData.toUtf8() ); else if ( mode == Binary ) actual->setValue( QString::fromLatin1( "%1/data" ).arg( key ), binaryData ); actual->sync(); q->emitFinished(); } void WritePasswordJobPrivate::gnomeKeyring_cb( int result, WritePasswordJobPrivate* self ) { if ( result == GnomeKeyring::RESULT_OK ) { self->q->emitFinished(); } else { const QPair errorResult = mapGnomeKeyringError( result ); self->q->emitFinishedWithError( errorResult.first, errorResult.second ); } } void WritePasswordJobPrivate::kwalletWalletFound(QDBusPendingCallWatcher *watcher) { watcher->deleteLater(); const QDBusPendingReply reply = *watcher; const QDBusPendingReply pendingReply = iface->open( reply.value(), 0, q->service() ); QDBusPendingCallWatcher* pendingWatcher = new QDBusPendingCallWatcher( pendingReply, this ); connect( pendingWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletOpenFinished(QDBusPendingCallWatcher*)) ); } void WritePasswordJobPrivate::kwalletOpenFinished( QDBusPendingCallWatcher* watcher ) { watcher->deleteLater(); QDBusPendingReply reply = *watcher; QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.data(); if ( reply.isError() ) { fallbackOnError( reply.error() ); return; } if ( actual->contains( key ) ) { // If we had previously written to QSettings, but we now have a kwallet available, migrate and delete old insecure data actual->remove( key ); actual->sync(); } const int handle = reply.value(); if ( handle < 0 ) { q->emitFinishedWithError( AccessDenied, tr("Access to keychain denied") ); return; } QDBusPendingReply nextReply; if ( !textData.isEmpty() ) nextReply = iface->writePassword( handle, q->service(), key, textData, q->service() ); else if ( !binaryData.isEmpty() ) nextReply = iface->writeEntry( handle, q->service(), key, binaryData, q->service() ); else nextReply = iface->removeEntry( handle, q->service(), key, q->service() ); QDBusPendingCallWatcher* nextWatcher = new QDBusPendingCallWatcher( nextReply, this ); connect( nextWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletWriteFinished(QDBusPendingCallWatcher*)) ); } void WritePasswordJobPrivate::kwalletWriteFinished( QDBusPendingCallWatcher* watcher ) { watcher->deleteLater(); QDBusPendingReply reply = *watcher; if ( reply.isError() ) { const QDBusError err = reply.error(); q->emitFinishedWithError( OtherError, tr("Could not open wallet: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); return; } q->emitFinished(); } Charm-1.10.0/Charm/Keychain/keychain_unsecure.cpp000066400000000000000000000032531260343353100216230ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2011 Frank Osterfeld * * * * 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. For licensing and distribution * * details, check the accompanying file 'COPYING'. * *****************************************************************************/ #include "keychain_p.h" #include #include using namespace QKeychain; void ReadPasswordJobPrivate::scheduledStart() { QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.data(); data = actual->value( key ).toByteArray(); q->emitFinished(); } void WritePasswordJobPrivate::scheduledStart() { if ( mode == Delete ) { QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.data(); actual->remove( key ); actual->sync(); q->emitFinished(); } else { QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.data(); QByteArray data = mode == Binary ? binaryData : textData.toUtf8(); actual->setValue( key, data ); actual->sync(); q->emitFinished(); } } Charm-1.10.0/Charm/Keychain/keychain_win.cpp000066400000000000000000000104421260343353100205650ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2011-2014 Frank Osterfeld * * * * 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. For licensing and distribution * * details, check the accompanying file 'COPYING'. * *****************************************************************************/ #include "keychain_p.h" #include #include #include #include using namespace QKeychain; void ReadPasswordJobPrivate::scheduledStart() { //Use settings member if there, create local settings object if not std::auto_ptr local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.get(); QByteArray encrypted = actual->value( key ).toByteArray(); if ( encrypted.isNull() ) { q->emitFinishedWithError( EntryNotFound, tr("Entry not found") ); return; } DATA_BLOB blob_in, blob_out; blob_in.pbData = reinterpret_cast( encrypted.data() ); blob_in.cbData = encrypted.size(); const BOOL ret = CryptUnprotectData( &blob_in, NULL, NULL, NULL, NULL, 0, &blob_out ); if ( !ret ) { q->emitFinishedWithError( OtherError, tr("Could not decrypt data") ); return; } data = QByteArray( reinterpret_cast( blob_out.pbData ), blob_out.cbData ); SecureZeroMemory( blob_out.pbData, blob_out.cbData ); LocalFree( blob_out.pbData ); q->emitFinished(); } void WritePasswordJobPrivate::scheduledStart() { if ( mode == Delete ) { //Use settings member if there, create local settings object if not std::auto_ptr local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.get(); actual->remove( key ); actual->sync(); if ( actual->status() != QSettings::NoError ) { const QString err = actual->status() == QSettings::AccessError ? tr("Could not delete encrypted data from settings: access error") : tr("Could not delete encrypted data from settings: format error"); q->emitFinishedWithError( OtherError, err ); } else { q->emitFinished(); } return; } QByteArray data = mode == Binary ? binaryData : textData.toUtf8(); DATA_BLOB blob_in, blob_out; blob_in.pbData = reinterpret_cast( data.data() ); blob_in.cbData = data.size(); const BOOL res = CryptProtectData( &blob_in, L"QKeychain-encrypted data", NULL, NULL, NULL, 0, &blob_out ); if ( !res ) { q->emitFinishedWithError( OtherError, tr("Encryption failed") ); //TODO more details available? return; } const QByteArray encrypted( reinterpret_cast( blob_out.pbData ), blob_out.cbData ); LocalFree( blob_out.pbData ); //Use settings member if there, create local settings object if not std::auto_ptr local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.get(); actual->setValue( key, encrypted ); actual->sync(); if ( actual->status() != QSettings::NoError ) { const QString errorString = actual->status() == QSettings::AccessError ? tr("Could not store encrypted data in settings: access error") : tr("Could not store encrypted data in settings: format error"); q->emitFinishedWithError( OtherError, errorString ); return; } q->emitFinished(); } Charm-1.10.0/Charm/Keychain/org.kde.KWallet.xml000066400000000000000000000250121260343353100210250ustar00rootroot00000000000000 Charm-1.10.0/Charm/MacApplicationCore.h000066400000000000000000000031501260343353100175220ustar00rootroot00000000000000/* MacApplicationCore.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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( 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 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.10.0/Charm/MacApplicationCore.mm000066400000000000000000000130721260343353100177100ustar00rootroot00000000000000#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( QObject* parent ) : ApplicationCore( 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(); Q_FOREACH( CharmWindow* window, m_windows ) m_dockMenu.addAction( window->showHideAction() ); 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() ); Q_FOREACH( CharmWindow* window, m_windows ) window->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() { openAWindow(); } 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.10.0/Charm/MacOSXBundleInfo.plist.in000066400000000000000000000025021260343353100203760ustar00rootroot00000000000000 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.10.0/Charm/ModelConnector.cpp000066400000000000000000000060171260343353100173000ustar00rootroot00000000000000/* ModelConnector.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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, SIGNAL(makeAndActivateEvent(Task)), SLOT(slotMakeAndActivateEvent(Task)) ); connect( &m_dataModel, SIGNAL(requestEventModification(Event,Event)), SLOT(slotRequestEventModification(Event,Event)) ); connect( &m_dataModel, SIGNAL(sysTrayUpdate(QString,bool)), SLOT(slotSysTrayUpdate(QString,bool)) ); } 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() ) { qDebug() << "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() ); } #include "moc_ModelConnector.cpp" Charm-1.10.0/Charm/ModelConnector.h000066400000000000000000000040171260343353100167430ustar00rootroot00000000000000/* ModelConnector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 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.10.0/Charm/QtQuick/000077500000000000000000000000001260343353100152365ustar00rootroot00000000000000Charm-1.10.0/Charm/QtQuick/Charm.cpp000066400000000000000000000021411260343353100167720ustar00rootroot00000000000000/* Charm.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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.10.0/Charm/QtQuick/deployment.pri000066400000000000000000000007431260343353100201360ustar00rootroot00000000000000android-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.10.0/Charm/QtQuick/qml.qrc000066400000000000000000000001331260343353100165330ustar00rootroot00000000000000 qml/main.qml Charm-1.10.0/Charm/QtQuick/qml/000077500000000000000000000000001260343353100160275ustar00rootroot00000000000000Charm-1.10.0/Charm/QtQuick/qml/main.qml000066400000000000000000000115421260343353100174710ustar00rootroot00000000000000/* main.qml This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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.10.0/Charm/Reports/000077500000000000000000000000001260343353100153135ustar00rootroot00000000000000Charm-1.10.0/Charm/Reports/MonthlyTimesheetXmlWriter.cpp000066400000000000000000000152261260343353100232050ustar00rootroot00000000000000/* MonthlyTimesheetXmlWriter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "CharmCMake.h" #include "Core/CharmDataModel.h" #include #include MonthlyTimesheetXmlWriter::MonthlyTimesheetXmlWriter() : m_dataModel( nullptr ) , m_yearOfMonth ( 0 ) , m_monthNumber( 0 ) , m_numberOfWeeks( 0 ) , m_rootTask() {} void MonthlyTimesheetXmlWriter::setDataModel( const CharmDataModel* dataModel ) { m_dataModel = dataModel; } void MonthlyTimesheetXmlWriter::setYearOfMonth( int yearOfMonth ) { m_yearOfMonth = yearOfMonth; } void MonthlyTimesheetXmlWriter::setMonthNumber( int monthNumber ) { m_monthNumber = monthNumber; } void MonthlyTimesheetXmlWriter::setEvents( const EventList& events ) { m_events = events; } void MonthlyTimesheetXmlWriter::setNumberOfWeeks( int numberOfWeeks ) { m_numberOfWeeks = numberOfWeeks; } void MonthlyTimesheetXmlWriter::setRootTask( TaskId rootTask ) { m_rootTask = rootTask; } QByteArray MonthlyTimesheetXmlWriter::saveToXml() const { QDomDocument document = XmlSerialization::createXmlTemplate( "monthly-timesheet" ); // find metadata and report element: QDomElement root = document.documentElement(); QDomElement metadata = XmlSerialization::metadataElement( document ); QDomElement charmVersion = document.createElement( "charmversion" ); QDomText charmVersionString = document.createTextNode( CHARM_VERSION ); charmVersion.appendChild( charmVersionString ); metadata.appendChild( charmVersion ); QDomElement report = XmlSerialization::reportElement( document ); Q_ASSERT( !root.isNull() && !metadata.isNull() && !report.isNull() ); // extend metadata tag: add year, and serial (month) number: { QDomElement yearElement = document.createElement( "year" ); metadata.appendChild( yearElement ); QDomText text = document.createTextNode( QString::number( m_yearOfMonth ) ); yearElement.appendChild( text ); QDomElement monthElement = document.createElement( "serial-number" ); monthElement.setAttribute( "semantics", "month-number" ); metadata.appendChild( monthElement ); QDomText monthtext = document.createTextNode( QString::number( m_monthNumber ) ); monthElement.appendChild( monthtext ); } typedef QMap< TaskId, QVector > SecondsMap; SecondsMap secondsMap; TimeSheetInfoList timeSheetInfo = TimeSheetInfo::filteredTaskWithSubTasks( TimeSheetInfo::taskWithSubTasks( m_dataModel, m_numberOfWeeks, m_rootTask, secondsMap ), false ); // here, we don't care about active or not, because we only report on the tasks // extend report tag: add tasks and effort structure { // tasks QDomElement tasks = document.createElement( "tasks" ); report.appendChild( tasks ); Q_FOREACH ( 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( "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 += " / "; } 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 ) ); } } #if 0 qDebug() << "MonthlyTimeSheetReport::slotSaveToXml: generated XML:" << endl << document.toString( 4 ); #endif return document.toByteArray( 4 ); } Charm-1.10.0/Charm/Reports/MonthlyTimesheetXmlWriter.h000066400000000000000000000032161260343353100226460ustar00rootroot00000000000000/* MonthlyTimesheetXmlWriter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "Core/Event.h" #include "Core/Task.h" class QByteArray; class CharmDataModel; class MonthlyTimesheetXmlWriter { public: MonthlyTimesheetXmlWriter(); /** * @throws XmlSerializationException */ QByteArray saveToXml() const; void setDataModel( const CharmDataModel* dataModel ); void setYearOfMonth( int yearOfMonth ); void setMonthNumber( int monthNumber ); void setNumberOfWeeks( int numberOfWeeks ); void setEvents( const EventList& events ); void setRootTask( TaskId rootTask ); private: const CharmDataModel* m_dataModel; int m_yearOfMonth; int m_monthNumber; int m_numberOfWeeks; TaskId m_rootTask; EventList m_events; }; #endif Charm-1.10.0/Charm/Reports/TimesheetInfo.cpp000066400000000000000000000073121260343353100205650ustar00rootroot00000000000000/* TimesheetInfo.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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) : indentation( 0 ) , seconds( segments ) , taskId( 0 ) , aggregated( false ) { 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 = QString::fromLatin1( "%1" ).arg( taskId, taskPaddingLength, 10, QChar( '0' ) ); return QString::fromLatin1("%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.10.0/Charm/Reports/TimesheetInfo.h000066400000000000000000000034461260343353100202360ustar00rootroot00000000000000/* TimesheetInfo.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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; QString taskName; QVector seconds; TaskId taskId; bool aggregated; }; #endif Charm-1.10.0/Charm/Reports/WeeklyTimesheetXmlWriter.cpp000066400000000000000000000157731260343353100230220ustar00rootroot00000000000000/* WeeklyTimesheetXmlWriter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "CharmCMake.h" #include "Core/CharmDataModel.h" #include #include static const int DaysInWeek = 7; WeeklyTimesheetXmlWriter::WeeklyTimesheetXmlWriter() : m_dataModel( nullptr ) , m_year( 0 ) , m_weekNumber( 0 ) , m_rootTask() { } void WeeklyTimesheetXmlWriter::setDataModel( const CharmDataModel* model ) { m_dataModel = model; } void WeeklyTimesheetXmlWriter::setYear( int year ) { m_year = year; } void WeeklyTimesheetXmlWriter::setWeekNumber( int weekNumber ) { m_weekNumber = weekNumber; } void WeeklyTimesheetXmlWriter::setEvents( const EventList& events ) { m_events = events; } void WeeklyTimesheetXmlWriter::setRootTask( TaskId rootTask ) { m_rootTask = rootTask; } QByteArray WeeklyTimesheetXmlWriter::saveToXml() const { // now create the report: QDomDocument document = XmlSerialization::createXmlTemplate( "weekly-timesheet" ); // find metadata and report element: QDomElement root = document.documentElement(); QDomElement metadata = XmlSerialization::metadataElement( document ); QDomElement charmVersion = document.createElement( "charmversion" ); QDomText charmVersionString = document.createTextNode( CHARM_VERSION ); charmVersion.appendChild( charmVersionString ); metadata.appendChild( charmVersion ); QDomElement report = XmlSerialization::reportElement( document ); Q_ASSERT( !root.isNull() && !metadata.isNull() && !report.isNull() ); // extend metadata tag: add year, and serial (week) number: { QDomElement yearElement = document.createElement( "year" ); metadata.appendChild( yearElement ); QDomText text = document.createTextNode( QString::number( m_year ) ); yearElement.appendChild( text ); QDomElement weekElement = document.createElement( "serial-number" ); weekElement.setAttribute( "semantics", "week-number" ); metadata.appendChild( weekElement ); QDomText weektext = document.createTextNode( QString::number( m_weekNumber ) ); weekElement.appendChild( weektext ); } typedef QMap< TaskId, QVector > SecondsMap; SecondsMap secondsMap; TimeSheetInfoList timeSheetInfo = TimeSheetInfo::filteredTaskWithSubTasks( TimeSheetInfo::taskWithSubTasks( m_dataModel, DaysInWeek, m_rootTask, secondsMap ), false ); // here, we don't care about active or not, because we only report on the tasks // extend report tag: add tasks and effort structure { // tasks QDomElement tasks = document.createElement( "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 ) ); // TaskId parentTask = DATAMODEL->parentItem( modelTask ).task().id(); // QDomElement task = document.createElement( "task" ); // task.setAttribute( "taskid", QString::number( info.taskId ) ); // if ( parentTask != 0 ) // task.setAttribute( "parent", QString::number( parentTask ) ); // QDomText name = document.createTextNode( modelTask.name() ); // task.appendChild( name ); // tasks.appendChild( task ); } } { // effort // make effort element: QDomElement effort = document.createElement( "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 += " / "; } 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 ) ); } } // qDebug() << "WeeklyTimeSheetReport::slotSaveToXml: generated XML:" << endl // << document.toString( 4 ); // return document.toByteArray( 4 ); } Charm-1.10.0/Charm/Reports/WeeklyTimesheetXmlWriter.h000066400000000000000000000030421260343353100224510ustar00rootroot00000000000000/* WeeklyTimesheetXmlWriter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "Core/Event.h" #include "Core/Task.h" class QByteArray; class CharmDataModel; class WeeklyTimesheetXmlWriter { public: WeeklyTimesheetXmlWriter(); /** * @throws XmlSerializationException */ QByteArray saveToXml() const; void setDataModel( const CharmDataModel* model); void setYear( int year ); void setWeekNumber( int weekNumber ); void setEvents( const EventList& events ); void setRootTask( TaskId rootTask ); private: const CharmDataModel* m_dataModel; int m_year; int m_weekNumber; TaskId m_rootTask; EventList m_events; }; #endif Charm-1.10.0/Charm/TaskModelAdapter.cpp000066400000000000000000000271621260343353100175550ustar00rootroot00000000000000/* TaskModelAdapter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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& parent ) 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_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_Comment: 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 id ) { 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 id ) { endRemoveRows(); } void TaskModelAdapter::eventAdded( EventId id ) { const Event& event = m_dataModel->eventForId( id ); taskModified( event.taskId() ); } void TaskModelAdapter::eventModified( EventId id, Event oldEvent ) { 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; } TaskIdList TaskModelAdapter::childrenIds( const Task& task ) const { const TaskTreeItem& item = m_dataModel->taskTreeItem( task.id() ); return item.childIds(); } bool TaskModelAdapter::taskIdExists( TaskId taskId ) const { return m_dataModel->taskExists( taskId ); } void TaskModelAdapter::commitCommand( CharmCommand* command ) { Q_ASSERT( command->owner() == this ); command->finalize(); } #include "moc_TaskModelAdapter.cpp" Charm-1.10.0/Charm/TaskModelAdapter.h000066400000000000000000000105271260343353100172170ustar00rootroot00000000000000/* TaskModelAdapter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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_Comment, TasksViewRole_TaskId, TasksViewRole_Filter ///< Role for search/filter }; 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(); // 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; TaskIdList childrenIds( const Task& task ) const; // reimplement CommandEmitterInterface: void commitCommand( CharmCommand* ) override; signals: void eventActivationNotice( EventId id ); void eventDeactivationNotice( EventId id ); private: const TaskTreeItem* itemFor ( const QModelIndex& ) const; QModelIndex indexForTaskTreeItem( const TaskTreeItem& item, int column = 0 ) const; QPointer m_dataModel; }; #endif Charm-1.10.0/Charm/UndoCharmCommandWrapper.cpp000066400000000000000000000025501260343353100211030ustar00rootroot00000000000000/* UndoCharmCommandWrapper.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2015 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.10.0/Charm/UndoCharmCommandWrapper.h000066400000000000000000000026631260343353100205550ustar00rootroot00000000000000/* UndoCharmCommandWrapper.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2015 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.10.0/Charm/Uniquifier.h000066400000000000000000000024471260343353100161550ustar00rootroot00000000000000/* Uniquifier.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2009-2015 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 UNIQUIFIER_H #define UNIQUIFIER_H // Usage: // void blurb() { // static bool inProgress = false; // if ( inProgress == true ) return; // Uniquifier u( &inProgress ); // // ... this code will be called only once // } class Uniquifier { public: explicit Uniquifier( bool* guard ) { m_guard = guard; *m_guard = true; } ~Uniquifier() { *m_guard = false; } private: bool *m_guard; }; #endif Charm-1.10.0/Charm/ViewFilter.cpp000066400000000000000000000106401260343353100164420ustar00rootroot00000000000000/* ViewFilter.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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, SIGNAL(eventActivationNotice(EventId)), SIGNAL(eventActivationNotice(EventId)) ); connect( &m_model, SIGNAL(eventDeactivationNotice(EventId)), SIGNAL(eventDeactivationNotice(EventId)) ); 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() || hasValidChildren( task ) ); accepted &= ok; break; } case Configuration::TaskPrefilter_SubscribedOnly: accepted &= task.subscribed(); break; case Configuration::TaskPrefilter_SubscribedAndCurrentOnly: accepted &= ( task.subscribed() && task.isCurrentlyValid() ); break; default: break; } return accepted; } bool ViewFilter::filterAcceptsColumn( int source_column, const QModelIndex& ) const { return true; } bool ViewFilter::taskIdExists( TaskId taskId ) const { return m_model.taskIdExists( taskId ); } bool ViewFilter::hasValidChildren( Task task ) const { if ( taskHasChildren( task ) ) { const TaskIdList idList = m_model.childrenIds( task ); for ( int i = 0; i < idList.count(); ++i ) { const Task childTask = DATAMODEL->getTask( idList[i] ); if ( childTask.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 ); } #include "moc_ViewFilter.cpp" Charm-1.10.0/Charm/ViewFilter.h000066400000000000000000000045411260343353100161120ustar00rootroot00000000000000/* ViewFilter.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 ); virtual ~ViewFilter(); // 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; signals: void eventActivationNotice( EventId id ); void eventDeactivationNotice( EventId id ); private: bool hasValidChildren(Task task) const; TaskModelAdapter m_model; }; #endif Charm-1.10.0/Charm/ViewHelpers.cpp000066400000000000000000000100251260343353100166140ustar00rootroot00000000000000/* ViewHelpers.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 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*)) ); } struct StartsEarlier { bool operator()( const EventId& leftId, const EventId& rightId ) const { const Event& left = DATAMODEL->eventForId( leftId ); const Event& right = DATAMODEL->eventForId( rightId ); return left.startDateTime() < right.startDateTime(); } }; EventIdList Charm::eventIdsSortedByStartTime( EventIdList ids ) { qStableSort( ids.begin(), ids.end(), StartsEarlier() ); return ids; } 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( ' ', 0, 0, QString::SectionIncludeTrailingSep ); const int projectCodeWidth = metrics.width( projectCode ); if ( width > projectCodeWidth ) { const QString& taskName = text.section( ' ', 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( ":/Charm/report_stylesheet.sty" ); if ( stylesheet.open( QIODevice::ReadOnly | QIODevice::Text ) ) { style = 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() ) { qDebug() << "reportStylesheet: default style sheet is empty, too bad"; } } else { qDebug() << "reportStylesheet: cannot load report style sheet:" << stylesheet.errorString(); } return style; } Charm-1.10.0/Charm/ViewHelpers.h000066400000000000000000000033261260343353100162670ustar00rootroot00000000000000/* ViewHelpers.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 { void connectControllerAndView( Controller*, CharmWindow* ); EventIdList eventIdsSortedByStartTime( EventIdList ); /** 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 ); } #endif Charm-1.10.0/Charm/ViewModeInterface.h000066400000000000000000000024721260343353100173730ustar00rootroot00000000000000/* ViewModeInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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_VIEWMODEINTERFACE_H #define CHARM_VIEWMODEINTERFACE_H #include class ModelConnector; // FIXME obsolete, merge into CharmWindow class ViewModeInterface { public: virtual ~ViewModeInterface() {} virtual void saveGuiState() = 0; virtual void restoreGuiState() = 0; virtual void stateChanged( State previous ) = 0; virtual void configurationChanged() = 0; virtual void setModel( ModelConnector* ) = 0; }; #endif Charm-1.10.0/Charm/WeeklySummary.cpp000066400000000000000000000053451260343353100172060ustar00rootroot00000000000000/* WeeklySummary.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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() : task( 0 ), durations( DAYS_IN_WEEK, 0 ) { } QVector WeeklySummary::summariesForTimespan( CharmDataModel* dataModel, const TimeSpan& timespan ) { 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: TaskIdList::iterator 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.10.0/Charm/WeeklySummary.h000066400000000000000000000024251260343353100166470ustar00rootroot00000000000000/* WeeklySummary.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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& timespan ); WeeklySummary(); TaskId task; QString taskname; QVector durations; }; #endif // WEEKLYSUMMARY_H Charm-1.10.0/Charm/Widgets/000077500000000000000000000000001260343353100152635ustar00rootroot00000000000000Charm-1.10.0/Charm/Widgets/ActivityReport.cpp000066400000000000000000000404541260343353100207660ustar00rootroot00000000000000/* ActivityReport.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "SelectTaskDialog.h" #include "ViewHelpers.h" #include "Core/Configuration.h" #include "Core/Dates.h" #include #include #include #include #include #include #include #include #include #include "ui_ActivityReportConfigurationDialog.h" ActivityReportConfigurationDialog::ActivityReportConfigurationDialog( QWidget* parent ) : ReportConfigurationDialog( parent ) , m_ui( new Ui::ActivityReportConfigurationDialog ) , m_rootTask( 0 ) { 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 ); connect( m_ui->buttonBox, SIGNAL(accepted()), this, SLOT(accept()) ); connect( m_ui->buttonBox, SIGNAL(rejected()), this, SLOT(reject()) ); connect( m_ui->comboBox, SIGNAL(currentIndexChanged(int)), SLOT(slotTimeSpanSelected(int)) ); connect( m_ui->checkBoxSubTasksOnly, SIGNAL(toggled(bool)), SLOT(slotCheckboxSubtasksOnlyChecked(bool)) ); connect( m_ui->checkBoxExcludeTasks, SIGNAL(toggled(bool)), SLOT(slotCheckBoxExcludeTasksChecked(bool)) ); connect( m_ui->toolButtonSelectTask, SIGNAL(clicked()), SLOT(slotSelectTask()) ); connect( m_ui->toolButtonExcludeTask, SIGNAL(clicked()), SLOT(slotExcludeTask()) ); slotCheckboxSubtasksOnlyChecked( m_ui->checkBoxSubTasksOnly->isChecked() ); slotCheckBoxExcludeTasksChecked( m_ui->checkBoxExcludeTasks->isChecked() ); 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(), SIGNAL(dateChanged()), SLOT(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::slotCheckboxSubtasksOnlyChecked( bool checked ) { if ( checked && m_rootTask == 0 ) { slotSelectTask(); } if ( ! checked ) { m_rootTask = 0; m_ui->labelTaskName->setText( tr( "(All Tasks)" ) ); } } void ActivityReportConfigurationDialog::slotCheckBoxExcludeTasksChecked( bool checked ) { if ( checked && m_rootExcludeTask == 0 ) { slotExcludeTask(); } if ( ! checked ) { m_rootExcludeTask = 0; m_ui->labelExcludeTaskName->setText( tr( "(No Tasks)" ) ); } } void ActivityReportConfigurationDialog::slotSelectTask() { if ( selectTask( m_rootTask ) ) { 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 ); } } void ActivityReportConfigurationDialog::slotExcludeTask() { if ( selectTask( m_rootExcludeTask ) ) { const TaskTreeItem& item = DATAMODEL->taskTreeItem( m_rootExcludeTask ); m_ui->labelExcludeTaskName->setText( DATAMODEL->fullTaskName( item.task() ) ); } else { if ( m_rootExcludeTask == 0 ) m_ui->checkBoxExcludeTasks->setChecked( false ); } } bool ActivityReportConfigurationDialog::selectTask(TaskId& task) { SelectTaskDialog dialog( this ); dialog.setNonTrackableSelectable(); const bool taskSelected = dialog.exec(); if ( taskSelected ) task = dialog.selectedTask(); return taskSelected; } void ActivityReportConfigurationDialog::accept() { // FIXME save settings QDialog::accept(); } void ActivityReportConfigurationDialog::showReportPreviewDialog( QWidget* parent ) { QDate start, end; const int index = m_ui->comboBox->currentIndex(); if ( index == m_timespans.size() - 1 ) { //Range start = m_ui->dateEditStart->date(); end = m_ui->dateEditEnd->date().addDays( 1 ); } else { start = m_timespans[index].timespan.first; end = m_timespans[index].timespan.second; } auto report = new ActivityReport( parent ); report->timeSpanSelection( m_timespans[index] ); report->setReportProperties( start, end, m_rootTask, m_rootExcludeTask ); report->show(); } ActivityReport::ActivityReport( QWidget* parent ) : ReportPreviewWindow( parent ) , m_rootTask( 0 ) , m_rootExcludeTask( 0 ) { saveToXmlButton()->hide(); saveToTextButton()->hide(); uploadButton()->hide(); connect( this, SIGNAL(anchorClicked(QUrl)), SLOT(slotLinkClicked(QUrl)) ); } ActivityReport::~ActivityReport() { } void ActivityReport::setReportProperties( const QDate& start, const QDate& end, TaskId rootTask, TaskId rootExcludeTask ) { m_start = start; m_end = end; m_rootTask = rootTask; m_rootExcludeTask = rootExcludeTask; slotUpdate(); } void ActivityReport::timeSpanSelection( NamedTimeSpan timeSpanSelection ) { m_timeSpanSelection = timeSpanSelection; } void ActivityReport::slotUpdate() { const QString DateFormat( "yyyy/MM/dd" ); const QString TimeFormat( "HH:mm" ); const QString DateTimeFormat( "yyyy/MM/dd HH:mm" ); // retrieve matching events: EventIdList matchingEvents = DATAMODEL->eventsThatStartInTimeFrame( m_start, m_end ); matchingEvents = Charm::eventIdsSortedByStartTime( matchingEvents ); if ( m_rootTask != 0 ) { matchingEvents = Charm::filteredBySubtree( matchingEvents, m_rootTask ); } // filter unproductive events: if ( m_rootExcludeTask != 0 ) { matchingEvents = Charm::filteredBySubtree( matchingEvents, m_rootExcludeTask, 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_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( "body" ); // create the caption: { QDomElement headline = doc.createElement( "h1" ); QDomText text = doc.createTextNode( tr( "Activity Report" ) ); headline.appendChild( text ); body.appendChild( headline ); } { QDomElement headline = doc.createElement( "h3" ); QString content = tr( "Report for %1, from %2 to %3" ) .arg( CONFIGURATION.user.name() ) .arg( m_start.toString( Qt::TextDate ) ) .arg( m_end.toString( Qt::TextDate ) ); QDomText text = doc.createTextNode( content ); headline.appendChild( text ); body.appendChild( headline ); QDomElement previousLink = doc.createElement( "a" ); previousLink.setAttribute( "href" , "Previous" ); QDomText previousLinkText = doc.createTextNode( tr( "" ).arg( timeSpanTypeName ) ); previousLink.appendChild( previousLinkText ); body.appendChild( previousLink ); QDomElement nextLink = doc.createElement( "a" ); nextLink.setAttribute( "href" , "Next" ); QDomText nextLinkText = doc.createTextNode( tr( "" ).arg( timeSpanTypeName ) ); nextLink.appendChild( nextLinkText ); body.appendChild( nextLink ); { QDomElement paragraph = doc.createElement( "h4" ); QString totalsText = tr( "Total: %1" ).arg( hoursAndMinutes( totalSeconds ) ); QDomText totalsElement = doc.createTextNode( totalsText ); paragraph.appendChild( totalsElement ); body.appendChild( paragraph ); } if ( m_rootTask != 0 ) { QDomElement paragraph = doc.createElement( "p" ); const Task& task = DATAMODEL->getTask( m_rootTask ); QString rootTaskText = tr( "Activity under task %1" ).arg( DATAMODEL->fullTaskName( task ) ); QDomText rootText = doc.createTextNode( rootTaskText ); paragraph.appendChild( rootText ); body.appendChild( paragraph ); } QDomElement paragraph = doc.createElement( "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( "table" ); table.setAttribute( "width", "100%" ); table.setAttribute( "align", "left" ); table.setAttribute( "cellpadding", "3" ); table.setAttribute( "cellspacing", "0" ); body.appendChild( table ); // table header QDomElement tableHead = doc.createElement( "thead" ); table.appendChild( tableHead ); QDomElement headerRow = doc.createElement( "tr" ); headerRow.setAttribute( "class", "header_row" ); tableHead.appendChild( headerRow ); // column headers for ( int i = 0; i < NumberOfColumns; ++i ) { QDomElement header = doc.createElement( "th" ); QDomText text = doc.createTextNode( Headlines[i] ); header.appendChild( text ); headerRow.appendChild( header ); } QDomElement tableBody = doc.createElement( "tbody" ); table.appendChild( tableBody ); // rows Q_FOREACH( EventId id, matchingEvents ) { const Event& event = DATAMODEL->eventForId( id ); Q_ASSERT( event.isValid() ); const TaskTreeItem& item = DATAMODEL->taskTreeItem( event.taskId() ); const Task& task = item.task(); Q_ASSERT( task.isValid() ); const QString row1Texts[] = { tr( "%1 %2-%3 (%4) -- [%5] %6" ) .arg( event.startDateTime().date().toString( Qt::SystemLocaleShortDate ).trimmed() ) .arg( event.startDateTime().time().toString( Qt::SystemLocaleShortDate ).trimmed() ) .arg( event.endDateTime().time().toString( Qt::SystemLocaleShortDate ).trimmed() ) .arg( hoursAndMinutes( event.duration() ) ) .arg( QString().setNum( task.id() ).trimmed(), Configuration::instance().taskPaddingLength, '0' ) .arg( task.name().trimmed() ) }; QDomElement row1 = doc.createElement( "tr" ); row1.setAttribute( "class", "event_attributes_row" ); QDomElement row2 = doc.createElement( "tr" ); for ( int index = 0; index < NumberOfColumns; ++index ) { QDomElement cell = doc.createElement( "td" ); cell.setAttribute( "class", "event_attributes" ); QDomText text = doc.createTextNode( row1Texts[index] ); cell.appendChild( text ); row1.appendChild( cell ); } QDomElement cell2 = doc.createElement( "td" ); cell2.setAttribute( "class", "event_description" ); cell2.setAttribute( "align", "left" ); QDomElement preElement = doc.createElement( "pre" ); QDomText preText = doc.createTextNode( event.comment() ); preElement.appendChild( preText ); cell2.appendChild( preElement ); row2.appendChild( cell2 ); tableBody.appendChild( row1 ); tableBody.appendChild( row2 ); } } // 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 ) { QDate start, end; switch( m_timeSpanSelection.timeSpanType ) { case Day: { start = which.toString() == "Previous" ? m_start.addDays( -1 ) : m_start.addDays( 1 ); end = which.toString() == "Previous" ? m_end.addDays( -1 ) : m_end.addDays( 1 ); } break; case Week: { start = which.toString() == "Previous" ? m_start.addDays( -7 ) : m_start.addDays( 7 ); end = which.toString() == "Previous" ? m_end.addDays( -7 ) : m_end.addDays( 7 ); } break; case Month: { start = which.toString() == "Previous" ? m_start.addMonths( -1 ) : m_start.addMonths( 1 ); end = which.toString() == "Previous" ? m_end.addMonths( -1 ) : m_end.addMonths( 1 ); } case Year: { start = which.toString() == "Previous" ? m_start.addYears( -1 ) : m_start.addYears( 1 ); end = which.toString() == "Previous" ? m_end.addYears( -1 ) : m_end.addYears( 1 ); } break; case Range: { int spanRange = m_start.daysTo(m_end); start = which.toString() == "Previous" ? m_start.addDays( -spanRange ) : m_start.addDays( spanRange ); end = which.toString() == "Previous" ? m_end.addDays( -spanRange ) : m_end.addDays( spanRange ); } break; default: Q_ASSERT( false ); // should not happen } setReportProperties( start, end, m_rootTask, m_rootExcludeTask ); } #include "moc_ActivityReport.cpp" Charm-1.10.0/Charm/Widgets/ActivityReport.h000066400000000000000000000047761260343353100204420ustar00rootroot00000000000000/* ActivityReport.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 namespace Ui { class ActivityReportConfigurationDialog; } class QUrl; class ActivityReportConfigurationDialog : public ReportConfigurationDialog { Q_OBJECT public: explicit ActivityReportConfigurationDialog( QWidget* parent ); ~ActivityReportConfigurationDialog(); void showReportPreviewDialog( QWidget* parent ) override; public Q_SLOTS: void accept() override; private slots: void slotDelayedInitialization(); void slotStandardTimeSpansChanged(); void slotTimeSpanSelected( int ); void slotCheckboxSubtasksOnlyChecked( bool ); void slotCheckBoxExcludeTasksChecked( bool ); void slotSelectTask(); void slotExcludeTask(); private: bool selectTask(TaskId& task); QScopedPointer m_ui; QList m_timespans; TaskId m_rootTask; TaskId m_rootExcludeTask; }; class ActivityReport : public ReportPreviewWindow { Q_OBJECT public: explicit ActivityReport( QWidget* parent = nullptr ); ~ActivityReport(); void setReportProperties( const QDate& start, const QDate& end, TaskId rootTask, TaskId rootExcludeTask ); void timeSpanSelection( NamedTimeSpan timeSpanSelection ); private slots: void slotLinkClicked( const QUrl& which ); private: void slotUpdate() override; private: QDate m_start; QDate m_end; TaskId m_rootTask; TaskId m_rootExcludeTask; NamedTimeSpan m_timeSpanSelection; }; #endif Charm-1.10.0/Charm/Widgets/ActivityReportConfigurationDialog.ui000066400000000000000000000266201260343353100244700ustar00rootroot00000000000000 ActivityReportConfigurationDialog 0 0 500 397 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 Included tasks Show... false 0 0 (task name) Qt::AlignCenter ... and subtasks false Select Task... (all tasks, otherwise). Qt::Horizontal 40 20 Exclude... false 0 0 (task name) Qt::AlignCenter ... and subtasks false Select Task... (no tasks, otherwise). Qt::Horizontal 40 20 Qt::Vertical 20 40 QDialogButtonBox::Cancel|QDialogButtonBox::Ok checkBoxSubTasksOnly toggled(bool) labelTaskName setEnabled(bool) 52 160 201 165 checkBoxSubTasksOnly toggled(bool) toolButtonSelectTask setEnabled(bool) 64 161 169 197 checkBoxExcludeTasks toggled(bool) labelExcludeTaskName setEnabled(bool) 69 225 262 225 checkBoxExcludeTasks toggled(bool) toolButtonExcludeTask setEnabled(bool) 69 225 177 260 Charm-1.10.0/Charm/Widgets/BillDialog.cpp000066400000000000000000000050631260343353100177750ustar00rootroot00000000000000/* BillDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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), m_year( 0 ), m_week( 0 ) { setResult(Later); QPalette p = palette(); QImage billImage(":/Charm/bill.jpg"); QBrush billBrush(billImage); p.setBrush(QPalette::Window, billBrush); setPalette(p); setAutoFillBackground(true); setMinimumSize(billImage.size()); setMaximumSize(billImage.size()); setWindowTitle("Yeah... about those timesheets..."); m_asYouWish = new QPushButton("As you wish"); connect(m_asYouWish, SIGNAL(clicked()), SLOT(slotAsYouWish())); m_alreadyDone = new QPushButton("Already done"); connect(m_alreadyDone, SIGNAL(clicked()), SLOT(slotAlreadyDone())); m_later = new QPushButton("Later"); connect(m_later, SIGNAL(clicked()), SLOT(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(QString("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); } #include "moc_BillDialog.cpp" Charm-1.10.0/Charm/Widgets/BillDialog.h000066400000000000000000000027021260343353100174370ustar00rootroot00000000000000/* BillDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 slots: void slotAsYouWish(); void slotAlreadyDone(); void slotLater(); private: QPushButton *m_asYouWish; QPushButton *m_alreadyDone; QPushButton *m_later; int m_year; int m_week; }; #endif Charm-1.10.0/Charm/Widgets/CharmAboutDialog.cpp000066400000000000000000000025201260343353100211330ustar00rootroot00000000000000/* CharmAboutDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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( "CHARM_VERSION", CHARM_VERSION ); m_ui->versionLabel->setText( versionText ); } CharmAboutDialog::~CharmAboutDialog() { } #include "moc_CharmAboutDialog.cpp" Charm-1.10.0/Charm/Widgets/CharmAboutDialog.h000066400000000000000000000023341260343353100206030ustar00rootroot00000000000000/* CharmAboutDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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(); private: QScopedPointer m_ui; }; #endif Charm-1.10.0/Charm/Widgets/CharmAboutDialog.ui000066400000000000000000000150541260343353100207740ustar00rootroot00000000000000 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-2015 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</span></p></body></html> Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse Charm-1.10.0/Charm/Widgets/CharmNewReleaseDialog.cpp000066400000000000000000000061551260343353100221230ustar00rootroot00000000000000/* CharmNewReleaseDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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, SIGNAL(clicked()), SLOT(slotSkipVersion()) ); m_remindMeLater = new QPushButton( tr( "Remind Me Later" ) ); connect( m_remindMeLater, SIGNAL(clicked()), SLOT(slotRemindMe()) ); m_update = new QPushButton( tr( "Update" ) ); connect( m_update, SIGNAL(clicked()), SLOT(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( "NEW", newVersion ); versionText.replace( "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( "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( QLatin1String( "UpdateChecker" ) ); settings.setValue( QLatin1String( "skip-version" ), m_version ); settings.endGroup(); accept(); } void CharmNewReleaseDialog::slotRemindMe() { reject(); } CharmNewReleaseDialog::~CharmNewReleaseDialog() { } #include "moc_CharmNewReleaseDialog.cpp" Charm-1.10.0/Charm/Widgets/CharmNewReleaseDialog.h000066400000000000000000000033241260343353100215630ustar00rootroot00000000000000/* CharmNewReleaseDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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(); void setVersion( const QString& newVersion , const QString& localVersion ); void setDownloadLink( const QUrl& link ); void setReleaseInformationLink( const QString& link ); private 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.10.0/Charm/Widgets/CharmNewReleaseDialog.ui000066400000000000000000000057551260343353100217630ustar00rootroot00000000000000 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.10.0/Charm/Widgets/CharmPreferences.cpp000066400000000000000000000162221260343353100212060ustar00rootroot00000000000000/* CharmPreferences.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "HttpClient/HttpJob.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 = HttpJob::credentialsAvailable(); 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->setEnabled( haveCommandInterface ); m_ui.cbEnableCommandInterface->setEnabled( haveCommandInterface ); m_ui.cbEnableCommandInterface->setChecked( haveCommandInterface && config.enableCommandInterface ); connect(m_ui.cbWarnUnuploadedTimesheets, SIGNAL(toggled(bool)), SLOT(slotWarnUnuploadedChanged(bool))); connect(m_ui.pbResetPassword, SIGNAL(clicked()), SLOT(slotResetPassword())); // 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; }; // 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(); } 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 (!HttpJob::credentialsAvailable()) 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); } } void CharmPreferences::slotResetPassword() { HttpJob* job = new HttpJob(this); bool ok; QPointer that( this ); //guard against destruction while dialog is open const QString newpass = QInputDialog::getText( this, tr("Password"), tr("Please enter your lotsofcake password"), QLineEdit::Password, "", &ok ); if ( !that ) return; if ( ok ) { job->provideRequestedPassword(newpass); } } #include "moc_CharmPreferences.cpp" Charm-1.10.0/Charm/Widgets/CharmPreferences.h000066400000000000000000000032441260343353100206530ustar00rootroot00000000000000/* CharmPreferences.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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(); Configuration::DurationFormat durationFormat() const; bool detectIdling() const; bool warnUnuploadedTimesheets() const; bool requestEventComment() const; bool enableCommandInterface() const; Qt::ToolButtonStyle toolButtonStyle() const; Configuration::TimeTrackerFontSize timeTrackerFontSize() const; private slots: void slotWarnUnuploadedChanged(bool); void slotResetPassword(); private: Ui::CharmPreferences m_ui; }; #endif Charm-1.10.0/Charm/Widgets/CharmPreferences.ui000066400000000000000000000222041260343353100210360ustar00rootroot00000000000000 CharmPreferences 0 0 382 254 Charm Configuration 0 0 Duration format (requires restart) false cbDurationFormat 0 0 Buttons show cbToolButtonStyle 0 0 Time tracker window font size cbTimeTrackerFontSize Minutes (hh:mm) Decimal (h.xx) true 0 0 Enable Bill Lumbergh cbWarnUnuploadedTimesheets Reset password Icons Text Text under icon Text beside icon System default true true 0 0 Comment finished events cbRequestEventComment 0 0 Enable idle detection cbIdleDetection 0 0 Enable command interface Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter cbEnableCommandInterface true Reset password Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Small Regular (Application Font) Large 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.10.0/Charm/Widgets/CharmWindow.cpp000066400000000000000000000164121260343353100202150ustar00rootroot00000000000000/* CharmWindow.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "Commands/CommandRelayCommand.h" #include "Core/CharmCommand.h" #include "Core/CharmConstants.h" #include #include #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 ) ) , m_showHideAction( new QAction( this ) ) , m_windowNumber( -1 ) , m_shortcut( 0 ) { setWindowName( name ); handleOpenCharm( false ); handleShow( false ); handleShowHide( false ); connect( m_openCharmAction, SIGNAL(triggered(bool)), SLOT(showView()) ); connect( m_showAction, SIGNAL(triggered(bool)), SLOT(showView()) ); connect( m_showHideAction, SIGNAL(triggered(bool)), SLOT(showHideView()) ); m_toolBar = addToolBar( "Toolbar" ); m_toolBar->setMovable( 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::windowIdentfier() 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_showHideAction->setShortcut( sequence ); 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; } QAction* CharmWindow::showHideAction() { return m_showHideAction; } void CharmWindow::restore() { show(); } void CharmWindow::showEvent( QShowEvent* e ) { handleOpenCharm( true ); handleShow( true ); handleShowHide( true ); QMainWindow::showEvent( e ); } void CharmWindow::hideEvent( QHideEvent* e ) { handleOpenCharm( false ); handleShow( false ); handleShowHide( false ); 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::handleOpenCharm( bool visible ) { m_openCharmAction->setEnabled( !visible ); } void CharmWindow::handleShow( bool visible ) { const QString text = tr( "Show %1" ).arg( m_windowName ); m_showAction->setText( text ); m_showAction->setEnabled( !visible ); } void CharmWindow::handleShowHide( bool visible ) { const QString text = visible ? tr( "Hide %1 Window" ).arg( m_windowName ) : tr( "Show %1 Window" ).arg( m_windowName ); m_showHideAction->setText( text ); emit visibilityChanged( 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->raise(); w->activateWindow(); } bool CharmWindow::showHideView( QWidget* w ) { // hide or restore the view if ( w->isVisible() ) { w->hide(); return false; } else { w->show(); w->raise(); w->activateWindow(); return true; } } void CharmWindow::showView() { showView( this ); } void CharmWindow::showHideView() { showHideView( this ); } void CharmWindow::configurationChanged() { const QList buttons = findChildren(); std::for_each( buttons.begin(), buttons.end(), std::bind2nd( std::mem_fun( &QToolButton::setToolButtonStyle ), CONFIGURATION.toolButtonStyle ) ); } void CharmWindow::saveGuiState() { Q_ASSERT( !windowIdentfier().isEmpty() ); QSettings settings; settings.beginGroup( windowIdentfier() ); // save geometry settings.setValue( MetaKey_MainWindowGeometry, saveGeometry() ); settings.setValue( MetaKey_MainWindowVisible, isVisible() ); } void CharmWindow::restoreGuiState() { Q_ASSERT( !windowIdentfier().isEmpty() ); // restore geometry QSettings settings; settings.beginGroup( windowIdentfier() ); if ( settings.contains( MetaKey_MainWindowGeometry ) ) { restoreGeometry( settings.value( MetaKey_MainWindowGeometry ).toByteArray() ); } // restore visibility if ( settings.contains( MetaKey_MainWindowVisible ) ) { const bool visible = settings.value( MetaKey_MainWindowVisible ).toBool(); setVisible(visible); } } #include "moc_CharmWindow.cpp" Charm-1.10.0/Charm/Widgets/CharmWindow.h000066400000000000000000000063661260343353100176710ustar00rootroot00000000000000/* CharmWindow.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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/ViewInterface.h" #include "Core/CommandEmitterInterface.h" class QAction; class QShortcut; class CharmWindow : public QMainWindow, public ViewInterface, public CommandEmitterInterface { Q_OBJECT public: explicit CharmWindow( const QString& name, QWidget* parent = nullptr ); QAction* showHideAction(); QAction* showAction(); QAction* openCharmAction(); QString windowName() const; QString windowIdentfier() 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() {} public: void stateChanged( State previous ) override; void showEvent( QShowEvent* ) override; void hideEvent( QHideEvent* ) override; void keyPressEvent( QKeyEvent* event ) override; virtual void saveGuiState(); virtual void restoreGuiState(); static void showView( QWidget* w ); static bool showHideView( QWidget* w ); signals: void visibilityChanged( bool ) override; void saveConfiguration() override; public slots: void sendCommandRollback( CharmCommand* ) override; void sendCommand( CharmCommand* ) override; void commitCommand( CharmCommand* ) override; void restore() override; void showView(); void showHideView(); void configurationChanged() override; private: void handleOpenCharm( bool visible ); void handleShow( bool visible ); void handleShowHide( bool visible ); QString m_windowName; QAction* m_openCharmAction; QAction* m_showAction; QAction* m_showHideAction; int m_windowNumber; // Mac numerical window number, used for shortcut etc QString m_windowIdentifier; QShortcut* m_shortcut; QToolBar* m_toolBar; }; #endif Charm-1.10.0/Charm/Widgets/CommentEditorPopup.cpp000066400000000000000000000044411260343353100215670ustar00rootroot00000000000000/* CommentEditorPopup.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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 ) , m_id() { 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.10.0/Charm/Widgets/CommentEditorPopup.h000066400000000000000000000025251260343353100212350ustar00rootroot00000000000000/* CommentEditorPopup.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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(); public Q_SLOTS: void loadEvent( EventId id ); void accept() override; private: Ui::CommentEditorPopup *ui; EventId m_id; }; #endif // COMMENTEDITORPOPUP_H Charm-1.10.0/Charm/Widgets/CommentEditorPopup.ui000066400000000000000000000034161260343353100214230ustar00rootroot00000000000000 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.10.0/Charm/Widgets/ConfigurationDialog.cpp000066400000000000000000000052631260343353100217240ustar00rootroot00000000000000/* ConfigurationDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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, SIGNAL(rejected()), SLOT(reject()) ); connect( m_ui.buttonBox, SIGNAL(accepted()), SLOT(accept()) ); #ifdef Q_OS_ANDROID setWindowState(windowState() | Qt::WindowMaximized); #endif } Configuration ConfigurationDialog::configuration() const { return m_config; } void ConfigurationDialog::on_databaseLocation_textChanged( const QString& text ) { checkInput(); } void ConfigurationDialog::accept() { m_config.installationId = 1; 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& text ) { 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 ); } #include "moc_ConfigurationDialog.cpp" Charm-1.10.0/Charm/Widgets/ConfigurationDialog.h000066400000000000000000000030401260343353100213600ustar00rootroot00000000000000/* ConfigurationDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 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.10.0/Charm/Widgets/ConfigurationDialog.ui000066400000000000000000000064161260343353100215600ustar00rootroot00000000000000 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.10.0/Charm/Widgets/DateEntrySyncer.cpp000066400000000000000000000056741260343353100210660ustar00rootroot00000000000000/* DateEntrySyncer.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 ); } } #include "moc_DateEntrySyncer.cpp" Charm-1.10.0/Charm/Widgets/DateEntrySyncer.h000066400000000000000000000025101260343353100205150ustar00rootroot00000000000000/* DateEntrySyncer.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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.10.0/Charm/Widgets/EnterVacationDialog.cpp000066400000000000000000000214061260343353100216540ustar00rootroot00000000000000/* EnterVacationDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 toHtmlEscaped( const QString& s ) { #if QT_VERSION < 0x050000 return Qt::escape( s ); #else return s.toHtmlEscaped(); #endif } 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 ); #if QT_VERSION >= 0x040700 events.reserve( days ); #endif 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 ) , m_selectedTaskId( -1 ) { 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, SIGNAL(dateChanged(QDate)), this, SLOT(updateButtonStates()) ); connect( m_ui->endDate, SIGNAL(dateChanged(QDate)), this, SLOT(updateButtonStates()) ); connect( m_ui->buttonBox, SIGNAL(accepted()), this, SLOT(okClicked()) ); connect( m_ui->buttonBox, SIGNAL(rejected()), this, SLOT(reject()) ); connect( m_ui->selectTaskButton, SIGNAL(clicked()), this, SLOT(selectTask()) ); QSettings settings; settings.beginGroup( QLatin1String("EnterVacation") ); m_ui->hoursSpinBox->setValue( settings.value( QLatin1String("workHours"), 8 ).toInt() ); m_ui->minutesSpinBox->setValue( settings.value( QLatin1String("workMinutes"), 0 ).toInt() ); m_selectedTaskId = settings.value( QLatin1String("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, SIGNAL(accepted()), &confirmationDialog, SLOT(accept()) ); connect( box, SIGNAL(rejected()), &confirmationDialog, SLOT(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 = toHtmlEscaped( startDate ); const QString htmlEndDate = toHtmlEscaped( endDate ); const QString htmlTaskName = toHtmlEscaped( task.name() ); QString html = ""; html += QString::fromLatin1("

    %1

    ").arg( tr("Vacation")); html += QString::fromLatin1("

    %1

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

    %1

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

    "; 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 = toHtmlEscaped( shortDate ); const QString htmlDuration = toHtmlEscaped( duration ); html += QString::fromLatin1("%1").arg( tr( "%1: %3", "short date, duration" ).arg( htmlShortDate, htmlDuration ) ); html += "

    "; } html += "

    "; html += ""; textBrowser->setHtml( html ); confirmationDialog.resize( 400, 600 ); if ( confirmationDialog.exec() == QDialog::Accepted ) m_events = events; } void EnterVacationDialog::okClicked() { QSettings settings; settings.beginGroup( QLatin1String("EnterVacation") ); settings.setValue( QLatin1String("workHours"), m_ui->hoursSpinBox->value() ); settings.setValue( QLatin1String("workMinutes"), m_ui->minutesSpinBox->value() ); settings.setValue( QLatin1String("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; } #include "moc_EnterVacationDialog.cpp" Charm-1.10.0/Charm/Widgets/EnterVacationDialog.h000066400000000000000000000030211260343353100213120ustar00rootroot00000000000000/* EnterVacationDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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(); EventList events() const; private: void updateTaskLabel(); void createEvents(); private slots: void selectTask(); void okClicked(); void updateButtonStates(); private: QScopedPointer m_ui; TaskId m_selectedTaskId; EventList m_events; }; #endif Charm-1.10.0/Charm/Widgets/EnterVacationDialog.ui000066400000000000000000000105411260343353100215050ustar00rootroot00000000000000 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.10.0/Charm/Widgets/EventEditor.cpp000066400000000000000000000210441260343353100202200ustar00rootroot00000000000000/* EventEditor.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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_updating( false ) , m_endDateChanged( true ) { 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, SIGNAL(dateChanged(QDate)), SLOT(startDateChanged(QDate)) ); connect( m_ui->timeEditStart, SIGNAL(timeChanged(QTime)), SLOT(startTimeChanged(QTime)) ); connect( m_ui->dateEditEnd, SIGNAL(dateChanged(QDate)), SLOT(endDateChanged(QDate)) ); connect( m_ui->timeEditEnd, SIGNAL(timeChanged(QTime)), SLOT(endTimeChanged(QTime)) ); connect( m_ui->pushButtonSelectTask, SIGNAL(clicked()), SLOT(selectTaskClicked()) ); connect( m_ui->textEditComment, SIGNAL(textChanged()), SLOT(commentChanged()) ); connect( m_ui->startToNowButton, SIGNAL(clicked()), SLOT(startToNowButtonClicked()) ); connect( m_ui->endToNowButton, SIGNAL(clicked()), SLOT(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( "ap" ) .remove( "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 value ) { updateEndTime(); updateValues(); } void EventEditor::durationMinutesEdited( int value ) { 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->textEditComment->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( "ap" ) .remove( "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(); } #include "moc_EventEditor.cpp" Charm-1.10.0/Charm/Widgets/EventEditor.h000066400000000000000000000037031260343353100176670ustar00rootroot00000000000000/* EventEditor.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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 ); virtual ~EventEditor(); // 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; bool m_endDateChanged; }; #endif /* EVENTEDITOR_H */ Charm-1.10.0/Charm/Widgets/EventEditor.ui000066400000000000000000000475651260343353100200730ustar00rootroot00000000000000 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.10.0/Charm/Widgets/EventEditorDelegate.cpp000066400000000000000000000201711260343353100216530ustar00rootroot00000000000000/* EventEditorDelegate.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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() ) { // make up event settings and calculate the space they need: QPixmap pixmap( 2000, 800 ); // temp QPainter painter( &pixmap ); QStyleOptionViewItem fakeOption ( option ); fakeOption.rect.setSize( pixmap.size() ); const QString task ( tr( "KDAB/Programming" ) ); QString dateAndDuration; QTextStream stream( &dateAndDuration ); QDate date = QDate::currentDate(); QTime time = QTime::currentTime(); stream << date.toString( Qt::SystemLocaleDate ) << " " << time.toString( Qt::SystemLocaleDate ) << " " << hoursAndMinutes( 3654 ); m_cachedSizeHint = paint( &painter, fakeOption, task, dateAndDuration, 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() ); 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( "h:mm" ) << " - " << endTime.toString( "h:mm" ) << " (" << hoursAndMinutes( event.duration() ) << ") Week " << date.weekNumber(); QString taskName; QTextStream taskStream( &taskName ); // print leading zeroes for the TaskId const int taskIdLength = CONFIGURATION.taskPaddingLength; taskStream << QString( "%1" ).arg( item.task().id(), taskIdLength, 10, QChar( '0' ) ) << " " << DATAMODEL->smartTaskName( item.task() ); paint( painter, option, taskName, dateAndDuration, logDuration( event.duration() ), locked ? EventState_Locked : EventState_Default ); } } QRect EventEditorDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QString& taskName, const QString& timespan, double logDuration, EventState state ) const { painter->save(); const QPalette& palette = option.palette; QFont mainFont = painter->font(); 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: 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->setBrush( brush ); painter->setPen( Qt::NoPen ); painter->drawRect( option.rect ); if ( state != EventState_Locked ) { foreground = palette.color( QPalette::Active, QPalette::HighlightedText ); } } painter->setPen( foreground ); // draw line 1 and decoration: painter->setFont( mainFont ); QRect taskRect; taskRect.setTopLeft( option.rect.topLeft() ); taskRect.setWidth( option.rect.width() - decoration.width() ); taskRect.setHeight( option.rect.height() ); QPoint decorationPoint ( option.rect.width() - decoration.width(), option.rect.top() + ( option.rect.height() - decoration.height() ) / 2 ); QRect boundingRect; QString elidedTask = Charm::elidedTaskName( taskName, mainFont, taskRect.width() ); painter->drawText( taskRect, Qt::AlignLeft | Qt::AlignTop, elidedTask, &boundingRect ); taskRect.setSize( boundingRect.size() ); 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; detailsRect.setTopLeft( QPoint( taskRect.topLeft().x(), taskRect.topLeft().y() + taskRect.height() ) ); detailsRect.setWidth( option.rect.width() ); detailsRect.setHeight( option.rect.height() - taskRect.height() ); painter->drawText( detailsRect, Qt::AlignLeft | Qt::AlignTop, timespan, &boundingRect ); detailsRect.setSize( boundingRect.size() ); // 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->setBrush( palette.dark() ); painter->setPen( Qt::NoPen ); painter->drawRect( durationRect ); painter->restore(); // return bounding rectangle 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 ); } } #include "moc_EventEditorDelegate.cpp" Charm-1.10.0/Charm/Widgets/EventEditorDelegate.h000066400000000000000000000042071260343353100213220ustar00rootroot00000000000000/* EventEditorDelegate.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 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; // paint the values into the painter at the given rectangle, return the // bounding rectangle // (factored out to use the same implementation for the size hint // and the painting during paintEvent) QRect paint( QPainter*, const QStyleOptionViewItem& option, const QString& taskName, const QString& timespan, double logDuration, EventState state ) const; // calculate the length for a visual representation of the event duration double logDuration( int seconds ) const; }; #endif Charm-1.10.0/Charm/Widgets/EventView.cpp000066400000000000000000000433661260343353100177170ustar00rootroot00000000000000/* EventView.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "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( QToolBar* toolBar, QWidget* (parent) ) : QWidget( parent ) , m_model( nullptr ) , 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 ) ) { auto layout = new QVBoxLayout( this ); layout->setContentsMargins( 0, 0, 0, 0 ); layout->addWidget( m_listView ); m_listView->setAlternatingRowColors( true ); m_listView->setContextMenuPolicy( Qt::CustomContextMenu ); connect( m_listView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotContextMenuRequested(QPoint)) ); connect( m_listView, SIGNAL(doubleClicked(QModelIndex)), SLOT(slotEventDoubleClicked(QModelIndex)) ); connect( &m_actionNewEvent, SIGNAL(triggered()), SLOT(slotNewEvent()) ); connect( &m_actionEditEvent, SIGNAL(triggered()), SLOT(slotEditEvent()) ); connect( &m_actionDeleteEvent, SIGNAL(triggered()), SLOT(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, SIGNAL(canUndoChanged(bool)), &m_actionUndo, SLOT(setEnabled(bool))); connect(m_undoStack, SIGNAL(undoTextChanged(QString)), this, SLOT(slotUndoTextChanged(QString))); connect(&m_actionUndo, SIGNAL(triggered()), m_undoStack, SLOT(undo())); connect(m_undoStack, SIGNAL(canRedoChanged(bool)), &m_actionRedo, SLOT(setEnabled(bool))); connect(m_undoStack, SIGNAL(redoTextChanged(QString)), this, SLOT(slotRedoTextChanged(QString))); connect(&m_actionRedo, SIGNAL(triggered()), m_undoStack, SLOT(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 ); toolBar->addAction( &m_actionNewEvent ); m_actionEditEvent.setText( tr( "Edit Event...") ); m_actionEditEvent.setShortcut( Qt::CTRL + Qt::Key_E ); m_actionEditEvent.setIcon( Data::editEventIcon() ); 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() ); toolBar->addAction( &m_actionFindAndReplace ); connect( &m_actionFindAndReplace, SIGNAL(triggered()), SLOT(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() ); 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 ); 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 ); toolBar->addWidget( spacer ); 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() { } void EventView::delayedInitialization() { timeSpansChanged(); connect( ApplicationCore::instance().dateChangeWatcher(), SIGNAL(dateChanged()), SLOT(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 ) ) }; 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::closeEvent( QCloseEvent* e ) { e->setAccepted( false ); reject(); } void EventView::reject() { emit visible( false ); } void EventView::commitCommand( CharmCommand* command ) { command->finalize(); } void EventView::slotCurrentItemChanged( const QModelIndex& start, const QModelIndex& end ) { 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, SIGNAL(emitExecute(CharmCommand*)), this, SIGNAL(emitCommand(CharmCommand*))); connect(command, SIGNAL(emitRollback(CharmCommand*)), this, SIGNAL(emitCommandRollback(CharmCommand*))); connect(command, SIGNAL(emitSlotEventIdChanged(int,int)), this, SLOT(slotEventIdChanged(int,int))); 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 date = m_event.startDateTime().date(); const QTime time = m_event.startDateTime().time(); const QString dateAndDuration = date.toString( Qt::SystemLocaleDate ) + ' ' + time.toString( Qt::SystemLocaleDate ) + ' ' + hoursAndMinutes( m_event.duration() ); const QString eventDescription = name + ' ' + dateAndDuration; if ( MessageBox::question( this, tr( "Delete Event?" ), tr( "Do you really want to delete the event %1?" ).arg(eventDescription), 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 ) ); } // FIXME obsolete Event EventView::newSettings() { Event event( m_event ); return event; } 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() ); } // ViewModeInterface: void EventView::saveGuiState() {} void EventView::restoreGuiState() {} void EventView::stateChanged( State previous ) {} void EventView::configurationChanged() { slotConfigureUi(); } 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(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(slotCurrentItemChanged(QModelIndex,QModelIndex)) ); connect( model, SIGNAL(eventActivationNotice(EventId)), SLOT(slotEventActivated(EventId)) ); connect( model, SIGNAL(eventDeactivationNotice(EventId)), SLOT(slotEventDeactivated(EventId)) ); connect( model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), SLOT(slotUpdateCurrent()) ); connect( model, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(slotUpdateTotal()) ); connect( model, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(slotUpdateTotal()) ); connect( model, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(slotConfigureUi()) ); connect( model, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(slotConfigureUi()) ); connect( model, SIGNAL(layoutChanged()), SLOT(slotUpdateCurrent()) ); connect( model, SIGNAL(modelReset()), SLOT(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, SIGNAL(finishedOk(Event)), this, SLOT(slotEventChangesCompleted(Event)), 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; } #include "moc_EventView.cpp" Charm-1.10.0/Charm/Widgets/EventView.h000066400000000000000000000066751260343353100173660ustar00rootroot00000000000000/* EventView.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "Core/Event.h" #include "Core/TimeSpans.h" #include "Core/CommandEmitterInterface.h" #include "ViewModeInterface.h" #include "UndoCharmCommandWrapper.h" class QModelIndex; class CharmCommand; class EventModelFilter; class QToolBar; class QComboBox; class QLabel; class QListView; class EventView : public QWidget, public ViewModeInterface, public CommandEmitterInterface { Q_OBJECT public: explicit EventView( QToolBar* toolBar, QWidget* parent ); ~EventView(); void closeEvent( QCloseEvent* ) override; void reject(); void makeVisibleAndCurrent( const Event& ); // implement ViewModeInterface: void saveGuiState() override; void restoreGuiState() override; void stateChanged( State previous ) override; void configurationChanged() override; void setModel( ModelConnector* ) override; void populateEditMenu( QMenu* ); signals: void visible( bool ); void emitCommand( CharmCommand* ); void emitCommandRollback( CharmCommand* ); public slots: void commitCommand( CharmCommand* ) override; void delayedInitialization(); void timeSpansChanged(); void timeFrameChanged(int ); void slotConfigureUi(); private 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: Event newSettings(); void setCurrentEvent( const Event& ); void stageCommand( CharmCommand* ); QUndoStack* m_undoStack; QList m_timeSpans; Event m_event; EventModelFilter* m_model; 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.10.0/Charm/Widgets/EventWindow.cpp000066400000000000000000000042651260343353100202470ustar00rootroot00000000000000/* EventWindow.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "EventWindow.h" #include "EventView.h" #include "ApplicationCore.h" #include EventWindow::EventWindow( QWidget* parent ) : CharmWindow( tr( "Events Editor" ), parent ) , m_eventView( new EventView( toolBar(), this ) ) { setWindowNumber( 2 ); setWindowIdentifier( QLatin1String( "window_events" ) ); setCentralWidget( m_eventView ); setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Expanding ); connect( m_eventView, SIGNAL(emitCommand(CharmCommand*)), SIGNAL(emitCommand(CharmCommand*)) ); connect( m_eventView, SIGNAL(emitCommandRollback(CharmCommand*)), SIGNAL(emitCommandRollback(CharmCommand*)) ); } EventWindow::~EventWindow() { } void EventWindow::insertEditMenu() { QMenu* editMenu = menuBar()->addMenu( tr( "Edit" ) ); m_eventView->populateEditMenu( editMenu ); } void EventWindow::configurationChanged() { CharmWindow::configurationChanged(); m_eventView->configurationChanged(); } void EventWindow::stateChanged( State previous ) { CharmWindow::stateChanged( previous ); m_eventView->stateChanged( previous ); if ( ApplicationCore::instance().state() == Connecting ) { m_eventView->setModel( & ApplicationCore::instance().model() ); } } void EventWindow::restore() { show(); } #include "moc_EventWindow.cpp" Charm-1.10.0/Charm/Widgets/EventWindow.h000066400000000000000000000027021260343353100177060ustar00rootroot00000000000000/* EventWindow.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 EVENTWINDOW_H #define EVENTWINDOW_H #include "CharmWindow.h" class EventView; class EventWindow : public CharmWindow { Q_OBJECT public: explicit EventWindow( QWidget* parent = nullptr ); ~EventWindow(); void stateChanged( State previous ) override; // restore the view void restore() override; public slots: void configurationChanged() override; protected: void insertEditMenu() override; signals: void emitCommand( CharmCommand* ); void emitCommandRollback( CharmCommand* ); void quit(); private: EventView* m_eventView; }; #endif Charm-1.10.0/Charm/Widgets/ExpandStatesHelper.cpp000066400000000000000000000055431260343353100215410ustar00rootroot00000000000000/* ExpandStatesHelper.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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.10.0/Charm/Widgets/ExpandStatesHelper.h000066400000000000000000000023421260343353100212000ustar00rootroot00000000000000/* ExpandStatesHelper.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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.10.0/Charm/Widgets/FindAndReplaceEventsDialog.cpp000066400000000000000000000126561260343353100231050ustar00rootroot00000000000000/* FindAndReplaceEventsDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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_taskToSearch( 0 ) ,m_taskToReplaceWith( 0 ) ,m_ui( new Ui::FindAndReplaceEventsDialog ) { m_ui->setupUi(this); m_replace = new QPushButton( tr( "Replace" ) ); m_replace->setEnabled( false ); connect( m_replace, SIGNAL(clicked()), SLOT(slotReplaceProjectCode()) ); m_cancel = new QPushButton( tr( "Cancel") ); connect( m_cancel, SIGNAL(clicked()), SLOT(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(), SIGNAL(selectionChanged()), SLOT(slotTimeSpansChanged()) ); connect( m_ui->dateEditEnd->calendarWidget(), SIGNAL(selectionChanged()), SLOT(slotTimeSpansChanged()) ); connect( ApplicationCore::instance().dateChangeWatcher(), SIGNAL(dateChanged()), SLOT(slotTimeSpansChanged()) ); m_ui->selectReplaceWithTaskPB->setEnabled( false ); connect ( m_ui->selectSearchTaskPB, SIGNAL(clicked()), SLOT(slotSelectTaskToSearch()) ); connect ( m_ui->selectReplaceWithTaskPB, SIGNAL(clicked()), SLOT(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(); if ( m_foundEvents.count() > 0 ) m_ui->selectReplaceWithTaskPB->setEnabled( true ); } 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( !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(); } #include "moc_FindAndReplaceEventsDialog.cpp" Charm-1.10.0/Charm/Widgets/FindAndReplaceEventsDialog.h000066400000000000000000000041251260343353100225420ustar00rootroot00000000000000/* FindAndReplaceEventsDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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(); QList modifiedEvents() const; private 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.10.0/Charm/Widgets/FindAndReplaceEventsDialog.ui000066400000000000000000000125061260343353100227320ustar00rootroot00000000000000 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.10.0/Charm/Widgets/HttpJobProgressDialog.cpp000066400000000000000000000037641260343353100222200ustar00rootroot00000000000000/* HttpJobProgressDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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, SIGNAL(finished(HttpJob*)), this, SLOT(jobFinished(HttpJob*)) ); connect( job, SIGNAL(transferStarted()), this, SLOT(jobTransferStarted()) ); connect( job, SIGNAL(passwordRequested()), this, SLOT(jobPasswordRequested()) ); } void HttpJobProgressDialog::jobTransferStarted() { show(); } void HttpJobProgressDialog::jobFinished( HttpJob* ) { deleteLater(); } void HttpJobProgressDialog::jobPasswordRequested() { bool ok; QPointer that( this ); //guard against destruction while dialog is open const QString newpass = QInputDialog::getText( parentWidget(), tr("Password"), tr("Please enter your lotsofcake password"), QLineEdit::Password, m_job->password(), &ok ); if ( !that ) return; if ( ok ) { m_job->provideRequestedPassword( newpass ); } else { m_job->passwordRequestCanceled(); } } Charm-1.10.0/Charm/Widgets/HttpJobProgressDialog.h000066400000000000000000000025071260343353100216570ustar00rootroot00000000000000/* HttpJobProgressDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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(); private: QPointer m_job; }; #endif Charm-1.10.0/Charm/Widgets/IdleCorrectionDialog.cpp000066400000000000000000000027621260343353100220230ustar00rootroot00000000000000/* IdleCorrectionDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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" IdleCorrectionDialog::IdleCorrectionDialog( QWidget* parent ) : QDialog( parent ) , m_ui( new Ui::IdleCorrectionDialog ) { m_ui->setupUi( this ); } 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 { Q_ASSERT( false ); // unhandled whatever? } return Idle_NoResult; } #include "moc_IdleCorrectionDialog.cpp" Charm-1.10.0/Charm/Widgets/IdleCorrectionDialog.h000066400000000000000000000025641260343353100214700ustar00rootroot00000000000000/* IdleCorrectionDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 #include namespace Ui { class IdleCorrectionDialog; } class IdleCorrectionDialog : public QDialog { Q_OBJECT public: enum Result { Idle_NoResult, Idle_Ignore, Idle_EndEvent }; explicit IdleCorrectionDialog( QWidget* parent = nullptr ); ~IdleCorrectionDialog(); Result result() const; private: QScopedPointer m_ui; }; #endif Charm-1.10.0/Charm/Widgets/IdleCorrectionDialog.ui000066400000000000000000000047341260343353100216570ustar00rootroot00000000000000 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 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.10.0/Charm/Widgets/MakeTemporarilyVisible.h000066400000000000000000000026451260343353100220660ustar00rootroot00000000000000/* MakeTemporarilyVisible.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 ) , m_wasVisible( false ) { 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; }; #endif Charm-1.10.0/Charm/Widgets/MessageBox.cpp000066400000000000000000000050221260343353100200230ustar00rootroot00000000000000/* MessageBox.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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.10.0/Charm/Widgets/MessageBox.h000066400000000000000000000027421260343353100174760ustar00rootroot00000000000000/* MessageBox.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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.10.0/Charm/Widgets/MonthlyTimesheet.cpp000066400000000000000000000303221260343353100212710ustar00rootroot00000000000000/* MonthlyTimesheet.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 ) , m_numberOfWeeks( 0 ) , m_monthNumber( 0 ) , m_yearOfMonth( 0 ) { QSettings settings; settings.beginGroup("users"); m_weeklyhours = settings.value("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, SIGNAL(anchorClicked(QUrl)), SLOT(slotLinkClicked(QUrl)) ); } 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, QChar('0') ); } QByteArray MonthlyTimeSheetReport::saveToText() { QByteArray output; QTextStream stream( &output ); QString content = tr( "Report for %1, %2 %3 (%4 to %5)" ) .arg( CONFIGURATION.user.name() ) .arg( QDate::longMonthName( m_monthNumber ) ) .arg( startDate().year() ) .arg( startDate().toString( Qt::TextDate ) ) .arg( 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() { try { MonthlyTimesheetXmlWriter timesheet; timesheet.setDataModel( DATAMODEL ); timesheet.setMonthNumber( m_monthNumber ); timesheet.setYearOfMonth( m_yearOfMonth ); timesheet.setNumberOfWeeks( m_numberOfWeeks ); timesheet.setRootTask( rootTask() ); 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( "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( "td" ); cell.setAttribute( "align", "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( "body" ); // QTextCursor cursor( m_report ); // create the caption: { QDomElement headline = doc.createElement( "h1" ); QDomText text = doc.createTextNode( tr( "Monthly Time Sheet" ) ); headline.appendChild( text ); body.appendChild( headline ); } { QDomElement headline = doc.createElement( "h3" ); QString content = tr( "Report for %1, %2 %3 (%4 to %5)" ) .arg( CONFIGURATION.user.name() ) .arg( QDate::longMonthName( m_monthNumber ) ) .arg( startDate().year() ) .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( "a" ); previousLink.setAttribute( "href" , "Previous" ); QDomText previousLinkText = doc.createTextNode( tr( "" ) ); previousLink.appendChild( previousLinkText ); body.appendChild( previousLink ); QDomElement nextLink = doc.createElement( "a" ); nextLink.setAttribute( "href" , "Next" ); QDomText nextLinkText = doc.createTextNode( tr( "" ) ); nextLink.appendChild( nextLinkText ); body.appendChild( nextLink ); QDomElement paragraph = doc.createElement( "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( "table" ); table.setAttribute( "width", "100%" ); table.setAttribute( "align", "left" ); table.setAttribute( "cellpadding", "3" ); table.setAttribute( "cellspacing", "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( "tr" ); headerRow.setAttribute( "class", "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( "tr" ); headerDayRow.setAttribute( "class", "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( "tr" ); if (i % 2) row.setAttribute( "class", "alternate_row" ); table.appendChild( row ); QDomElement taskCell = addTblCell( row, timeSheetInfo[i].formattedTaskIdAndName( CONFIGURATION.taskPaddingLength ) ); taskCell.setAttribute( "align", "left" ); taskCell.setAttribute( "style", QString( "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( "tr" ); totals.setAttribute( "class", "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() == "Previous" ? startDate().addMonths( -1 ) : startDate().addMonths( 1 ); QDate end = which.toString() == "Previous" ? endDate().addMonths( -1 ) : endDate().addMonths( 1 ); setReportProperties( start, end, rootTask(), activeTasksOnly() ); } Charm-1.10.0/Charm/Widgets/MonthlyTimesheet.h000066400000000000000000000033761260343353100207470ustar00rootroot00000000000000/* MonthlyTimesheet.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 ); virtual ~MonthlyTimeSheetReport(); void setReportProperties( const QDate& start, const QDate& end, TaskId rootTask, bool activeTasksOnly ) override; private slots: void slotLinkClicked( const QUrl& which ); private: QString suggestedFileName() const override; void update() override; QByteArray saveToText() override; QByteArray saveToXml() override; private: // properties of the report: int m_numberOfWeeks; int m_monthNumber; int m_yearOfMonth; QString m_weeklyhours; float m_dailyhours; }; #endif Charm-1.10.0/Charm/Widgets/MonthlyTimesheetConfigurationDialog.cpp000066400000000000000000000144241260343353100251460ustar00rootroot00000000000000/* MonthlyTimesheetConfigurationDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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, SIGNAL(accepted()), this, SLOT(accept()) ); connect( m_ui->buttonBox, SIGNAL(rejected()), this, SLOT(reject()) ); connect( m_ui->comboBoxMonth, SIGNAL(currentIndexChanged(int)), SLOT(slotMonthComboItemSelected(int)) ); connect( m_ui->toolButtonSelectTask, SIGNAL(clicked()), SLOT(slotSelectTask()) ); connect( m_ui->checkBoxSubTasksOnly, SIGNAL(toggled(bool)), SLOT(slotCheckboxSubtasksOnlyChecked(bool)) ); m_ui->comboBoxMonth->setCurrentIndex( 1 ); slotCheckboxSubtasksOnlyChecked( m_ui->checkBoxSubTasksOnly->isChecked() ); slotStandardTimeSpansChanged(); connect( ApplicationCore::instance().dateChangeWatcher(), SIGNAL(dateChanged()), SLOT(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( QWidget* parent ) { 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( parent ); 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 }; 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.10.0/Charm/Widgets/MonthlyTimesheetConfigurationDialog.h000066400000000000000000000036101260343353100246060ustar00rootroot00000000000000/* MonthlyTimesheetConfigurationDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 ); virtual ~MonthlyTimesheetConfigurationDialog(); void showReportPreviewDialog( QWidget* parent ) override; void showEvent( QShowEvent* ) override; void setDefaultMonth( int yearOfMonth, int month ); public Q_SLOTS: void accept() override; private slots: void slotCheckboxSubtasksOnlyChecked( bool ); void slotStandardTimeSpansChanged(); void slotMonthComboItemSelected( int ); void slotSelectTask(); private: QScopedPointer m_ui; QList m_monthInfo; TaskId m_rootTask; }; #endif Charm-1.10.0/Charm/Widgets/MonthlyTimesheetConfigurationDialog.ui000066400000000000000000000166671260343353100250140ustar00rootroot00000000000000 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.10.0/Charm/Widgets/NotificationPopup.cpp000066400000000000000000000040351260343353100214430ustar00rootroot00000000000000/* NotificationPopup.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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 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( "TITLE", title ) ); QString messageText = m_ui->messageLB->text(); m_ui->messageLB->setText( messageText.replace( "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(); } #include "moc_NotificationPopup.cpp" Charm-1.10.0/Charm/Widgets/NotificationPopup.h000066400000000000000000000026761260343353100211210ustar00rootroot00000000000000/* NotificationPopup.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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(); void showNotification( const QString& title, const QString& message ); private slots: void slotCloseNotification(); private: void mousePressEvent( QMouseEvent* event ) override; QScopedPointer m_ui; }; #endif // NOTIFICATIONPOPUP_H Charm-1.10.0/Charm/Widgets/NotificationPopup.ui000066400000000000000000000033421260343353100212760ustar00rootroot00000000000000 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.10.0/Charm/Widgets/ReportConfigurationDialog.cpp000066400000000000000000000020651260343353100231150ustar00rootroot00000000000000/* ReportConfigurationDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 ) { } #include "moc_ReportConfigurationDialog.cpp" Charm-1.10.0/Charm/Widgets/ReportConfigurationDialog.h000066400000000000000000000027011260343353100225570ustar00rootroot00000000000000/* ReportConfigurationDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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( QWidget* parent ) = 0; }; #endif Charm-1.10.0/Charm/Widgets/ReportPreviewWindow.cpp000066400000000000000000000073141260343353100220010ustar00rootroot00000000000000/* ReportPreviewWindow.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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_document() { m_ui->setupUi( this ); setAttribute( Qt::WA_DeleteOnClose ); connect( m_ui->pushButtonClose, SIGNAL(clicked()), SLOT(slotClose()) ); connect( m_ui->pushButtonUpdate, SIGNAL(clicked()), SLOT(slotUpdate()) ); connect( m_ui->pushButtonSave, SIGNAL(clicked()), SLOT(slotSaveToXml()) ); connect( m_ui->pushButtonSaveTotals, SIGNAL(clicked()), SLOT(slotSaveToText()) ); connect( m_ui->textBrowser, SIGNAL(anchorClicked(QUrl)), SIGNAL(anchorClicked(QUrl)) ); #ifndef QT_NO_PRINTER connect( m_ui->pushButtonPrint, SIGNAL(clicked()), SLOT(slotPrint()) ); #else m_ui->pushButtonPrint->setEnabled(false); #endif 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( "html" ); // FIXME this is only a rudimentary subset of a valid xhtml 1 document // html element QDomElement html = doc.createElement( "html" ); html.setAttribute( "xmlns", "http://www.w3.org/1999/xhtml" ); doc.appendChild( html ); // head and body, children of html QDomElement head = doc.createElement( "head" ); html.appendChild( head ); QDomElement body = doc.createElement( "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(); } #include "moc_ReportPreviewWindow.cpp" Charm-1.10.0/Charm/Widgets/ReportPreviewWindow.h000066400000000000000000000035021260343353100214410ustar00rootroot00000000000000/* ReportPreviewWindow.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 namespace Ui { class ReportPreviewWindow; } class QPushButton; class ReportPreviewWindow : public QDialog { Q_OBJECT public: explicit ReportPreviewWindow( QWidget* parent = nullptr ); virtual ~ReportPreviewWindow(); signals: void anchorClicked(const QUrl& which); protected: void setDocument( const QTextDocument* document ); QDomDocument createReportTemplate() const; QPushButton* saveToXmlButton() const; QPushButton* saveToTextButton() const; QPushButton* uploadButton() const; private 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.10.0/Charm/Widgets/ReportPreviewWindow.ui000066400000000000000000000041021260343353100216240ustar00rootroot00000000000000 ReportPreviewWindow 0 0 593 258 Report Preview false &Close Qt::Horizontal 40 20 &Update Upload Save As &XML... Save &Totals... &Print... Charm-1.10.0/Charm/Widgets/SelectTaskDialog.cpp000066400000000000000000000212361260343353100211550ustar00rootroot00000000000000/* SelectTaskDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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& parent ) 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_selectedTask( 0 ) , m_proxy( MODEL.charmDataModel() ) , m_nonTrackableSelectable( false ) { 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(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(slotCurrentItemChanged(QModelIndex,QModelIndex)) ); connect( m_ui->treeView, SIGNAL(doubleClicked(QModelIndex)), SLOT(slotDoubleClicked(QModelIndex)) ); connect( m_ui->filter, SIGNAL(textChanged(QString)), SLOT(slotFilterTextChanged(QString)) ); connect( m_ui->showExpired, SIGNAL(toggled(bool)), SLOT(slotPrefilteringChanged()) ); connect( m_ui->showSelected, SIGNAL(toggled(bool)), SLOT(slotPrefilteringChanged()) ); connect( this, SIGNAL(accepted()), SLOT(slotAccepted()) ); connect( MODEL.charmDataModel(), SIGNAL(resetGUIState()), SLOT(slotResetState()) ); QSettings settings; settings.beginGroup( 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( 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( staticMetaObject.className() ); settings.setValue( MetaKey_MainWindowGeometry, size() ); QDialog::hideEvent( event ); } TaskId SelectTaskDialog::selectedTask() const { return m_selectedTask; } 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 = 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( ' ', '*' ); Charm::saveExpandStates( m_ui->treeView, &m_expansionStates ); m_proxy.setFilterWildcard( filtertext ); 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( 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( 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; } #include "moc_SelectTaskDialog.cpp" Charm-1.10.0/Charm/Widgets/SelectTaskDialog.h000066400000000000000000000046341260343353100206250ustar00rootroot00000000000000/* SelectTaskDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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(); TaskId selectedTask() const; void setNonTrackableSelectable(); signals: void saveConfiguration(); protected: void showEvent( QShowEvent * event ) override; void hideEvent( QHideEvent* event ) override; private slots: void slotCurrentItemChanged( const QModelIndex&, const QModelIndex& ); void slotDoubleClicked ( const QModelIndex & ); void slotFilterTextChanged( const QString& ); void slotAccepted(); void slotPrefilteringChanged(); void slotResetState(); private: bool isValidAndTrackable( const QModelIndex& index ) const; private: QScopedPointer m_ui; TaskId m_selectedTask; SelectTaskDialogProxy m_proxy; QHash m_expansionStates; bool m_nonTrackableSelectable; }; #endif Charm-1.10.0/Charm/Widgets/SelectTaskDialog.ui000066400000000000000000000057561260343353100210210ustar00rootroot00000000000000 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.10.0/Charm/Widgets/TaskEditor.cpp000066400000000000000000000153151260343353100200450ustar00rootroot00000000000000/* TaskEditor.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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, SIGNAL(clicked()), SLOT(slotSelectParent()) ); connect( m_ui->dateEditFrom, SIGNAL(dateChanged(QDate)), SLOT(slotDateChanged(QDate)) ); connect( m_ui->dateEditTo, SIGNAL(dateChanged(QDate)), SLOT(slotDateChanged(QDate)) ); connect( m_ui->checkBoxFrom, SIGNAL(clicked(bool)), SLOT(slotCheckBoxChecked(bool)) ); connect( m_ui->checkBoxUntil, SIGNAL(clicked(bool)), SLOT(slotCheckBoxChecked(bool)) ); } 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() ); } 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() ); } 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 ).arg( name ).arg( parent ).arg( 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 ); } #include "moc_TaskEditor.cpp" Charm-1.10.0/Charm/Widgets/TaskEditor.h000066400000000000000000000030011260343353100174770ustar00rootroot00000000000000/* TaskEditor.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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 ); virtual ~TaskEditor(); 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.10.0/Charm/Widgets/TaskEditor.ui000066400000000000000000000443441260343353100177040ustar00rootroot00000000000000 TaskEditor 0 0 483 267 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 Name: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Parent task: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Choose... No parent true Valid from: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter true No from date Valid until: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter No until date 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.10.0/Charm/Widgets/TaskIdDialog.cpp000066400000000000000000000036671260343353100203020ustar00rootroot00000000000000/* TaskIdDialog.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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, SIGNAL(accepted()), this, SLOT(accept()) ); connect( m_ui.buttonBox, SIGNAL(rejected()), this, SLOT(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(); } #include "moc_TaskIdDialog.cpp" Charm-1.10.0/Charm/Widgets/TaskIdDialog.h000066400000000000000000000026701260343353100177400ustar00rootroot00000000000000/* TaskIdDialog.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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(); void setSuggestedId( int ); int selectedId() const; QString taskName() const; private slots: void on_spinBox_valueChanged( int ); private: Ui::TaskIdDialog m_ui; TaskModelInterface* m_model; }; #endif Charm-1.10.0/Charm/Widgets/TaskIdDialog.ui000066400000000000000000000054571260343353100201340ustar00rootroot00000000000000 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.10.0/Charm/Widgets/TasksView.cpp000066400000000000000000000356141260343353100177200ustar00rootroot00000000000000/* TasksView.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "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( QToolBar* toolBar, QWidget* parent ) : QWidget( parent ) // , ViewInterface() , 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( toolBar ) ) , m_showSubscribedOnly( new QAction( toolBar ) ) , m_treeView( new QTreeView( this ) ) { auto layout = new QVBoxLayout( this ); layout->setContentsMargins( 0, 0, 0, 0 ); layout->addWidget( m_treeView ); m_treeView->setItemDelegate( m_delegate ); connect( m_delegate, SIGNAL(editingStateChanged()), SLOT(configureUi()) ); // set up actions m_actionNewTask.setText( tr( "New &Task" ) ); m_actionNewTask.setShortcut( QKeySequence::New ); m_actionNewTask.setIcon( Data::newTaskIcon() ); toolBar->addAction( &m_actionNewTask ); connect( &m_actionNewTask, SIGNAL(triggered(bool)), SLOT(actionNewTask()) ); m_actionNewSubTask.setText( tr( "New &Subtask" ) ); m_actionNewSubTask.setShortcut( Qt::META + Qt::Key_N ); m_actionNewSubTask.setIcon( Data::newSubtaskIcon() ); toolBar->addAction( &m_actionNewSubTask ); connect( &m_actionNewSubTask, SIGNAL(triggered(bool)), SLOT(actionNewSubTask()) ); m_actionEditTask.setText( tr( "Edit Task" ) ); m_actionEditTask.setShortcut( Qt::CTRL + Qt::Key_E ); m_actionEditTask.setIcon( Data::editTaskIcon() ); toolBar->addAction( &m_actionEditTask ); connect( &m_actionEditTask, SIGNAL(triggered(bool)), SLOT(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() ); toolBar->addAction( &m_actionDeleteTask ); connect( &m_actionDeleteTask, SIGNAL(triggered(bool)), SLOT(actionDeleteTask()) ); m_actionExpandTree.setText( tr( "Expand All" ) ); connect( &m_actionExpandTree, SIGNAL(triggered(bool)), m_treeView, SLOT(expandAll()) ); m_actionCollapseTree.setText( tr( "Collapse All" ) ); connect( &m_actionCollapseTree, SIGNAL(triggered(bool)), m_treeView, SLOT(collapseAll()) ); // filter setup m_showCurrentOnly->setText( tr( "Current" ) ); m_showCurrentOnly->setCheckable( true ); toolBar->addAction( m_showCurrentOnly ); connect( m_showCurrentOnly, SIGNAL(triggered(bool)), SLOT(taskPrefilteringChanged()) ); m_showSubscribedOnly->setText( tr( "Selected" ) ); m_showSubscribedOnly->setCheckable( true ); toolBar->addAction( m_showSubscribedOnly ); connect( m_showSubscribedOnly, SIGNAL(triggered(bool)), SLOT(taskPrefilteringChanged()) ); auto searchField = new QLineEdit( this ); connect( searchField, SIGNAL(textChanged(QString)), SLOT(slotFiltertextChanged(QString)) ); 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, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotContextMenuRequested(QPoint)) ); // I hate doing this but the stupid default view sizeHints suck badly. setMinimumHeight( 200 ); // 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 previous ) { 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, SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(configureUi()) ); connect( smodel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)), SLOT(configureUi()) ); connect( smodel, SIGNAL(currentColumnChanged(QModelIndex,QModelIndex)), SLOT(configureUi()) ); connect( filter, SIGNAL(eventActivationNotice(EventId)), SLOT(slotEventActivated(EventId)) ); connect( filter, SIGNAL(eventDeactivationNotice(EventId)), SLOT(slotEventDeactivated(EventId)) ); } break; case Connected: //the model is populated when entering Connected, so delay state restore QMetaObject::invokeMethod( this, "restoreGuiState", Qt::QueuedConnection ); break; case Disconnecting: saveGuiState(); break; case ShuttingDown: case Dead: default: break; }; } void TasksView::saveGuiState() { 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() { 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(); } void TasksView::setModel( ModelConnector* connector ) { m_treeView->setModel( connector->taskModel() ); restoreGuiState(); } void TasksView::slotFiltertextChanged( const QString& filtertextRaw ) { ViewFilter* filter = ApplicationCore::instance().model().taskModel(); QString filtertext = filtertextRaw.simplified(); filtertext.replace( ' ', '*' ); saveGuiState(); filter->setFilterWildcard( filtertext ); 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(); } } #include "moc_TasksView.cpp" Charm-1.10.0/Charm/Widgets/TasksView.h000066400000000000000000000054401260343353100173570ustar00rootroot00000000000000/* TasksView.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "ViewModeInterface.h" class QMenu; class TasksViewDelegate; class QToolBar; class QTreeView; class TasksView : public QWidget, public ViewModeInterface, public CommandEmitterInterface { Q_OBJECT public: explicit TasksView ( QToolBar* toolBar, QWidget* parent = nullptr ); ~TasksView(); // implement ViewModeInterface: void stateChanged( State previous ) override; void configurationChanged() override; void setModel( ModelConnector* ) override; void populateEditMenu( QMenu* ); public Q_SLOTS: void commitCommand( CharmCommand* ) override; signals: // FIXME connect to MainWindow void saveConfiguration(); void emitCommand( CharmCommand* ); void emitCommandRollback( CharmCommand* ); private 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(); void restoreGuiState() override; private: void saveGuiState() override; // helper to retrieve selected task: Task selectedTask(); void addTaskHelper( const Task& parent ); 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.10.0/Charm/Widgets/TasksViewDelegate.cpp000066400000000000000000000164441260343353100213530ustar00rootroot00000000000000/* TasksViewDelegate.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 ) , m_editing( false ) { connect( this, SIGNAL(closeEditor(QWidget*,QAbstractItemDelegate::EndEditHint)), SLOT(slotCloseEditor(QWidget*,QAbstractItemDelegate::EndEditHint)) ); } 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 #if QT_VERSION < QT_VERSION_CHECK(5,0,0) const QRect cbRect = check(option, bounding, variant); #else const QRect cbRect = doCheck(option, bounding, variant); #endif // 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; } #include "moc_TasksViewDelegate.cpp" Charm-1.10.0/Charm/Widgets/TasksViewDelegate.h000066400000000000000000000050401260343353100210060ustar00rootroot00000000000000/* TasksViewDelegate.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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; signals: void editingStateChanged() const; private 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; }; #endif Charm-1.10.0/Charm/Widgets/TasksWindow.cpp000066400000000000000000000050311260343353100202430ustar00rootroot00000000000000/* TasksWindow.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "TasksWindow.h" #include "ApplicationCore.h" #include "TasksView.h" #include TasksWindow::TasksWindow( QWidget* parent ) : CharmWindow( tr( "Tasks Editor" ), parent ) , m_tasksView( new TasksView( toolBar(), this ) ) { setWindowNumber( 1 ); setWindowIdentifier( QLatin1String( "window_tasks" ) ); setCentralWidget( m_tasksView ); setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Expanding ); connect( m_tasksView, SIGNAL(emitCommand(CharmCommand*)), SIGNAL(emitCommand(CharmCommand*)) ); connect( m_tasksView, SIGNAL(emitCommandRollback(CharmCommand*)), SIGNAL(emitCommandRollback(CharmCommand*)) ); connect( m_tasksView, SIGNAL(saveConfiguration()), SIGNAL(saveConfiguration()) ); } TasksWindow::~TasksWindow() { } void TasksWindow::stateChanged( State previous ) { CharmWindow::stateChanged( previous ); m_tasksView->stateChanged( previous ); if ( ApplicationCore::instance().state() == Connecting ) { m_tasksView->setModel( & ApplicationCore::instance().model() ); } } void TasksWindow::restore() { show(); } void TasksWindow::configurationChanged() { CharmWindow::configurationChanged(); m_tasksView->configurationChanged(); } void TasksWindow::insertEditMenu() { QMenu* editMenu = menuBar()->addMenu( tr( "Edit" ) ); m_tasksView->populateEditMenu( editMenu); } void TasksWindow::sendCommand( CharmCommand* ) { Q_ASSERT( false ); // should not be called } void TasksWindow::sendCommandRollback( CharmCommand* ) { Q_ASSERT( false ); // should not be called } void TasksWindow::commitCommand( CharmCommand* ) { } #include "moc_TasksWindow.cpp" Charm-1.10.0/Charm/Widgets/TasksWindow.h000066400000000000000000000031671260343353100177200ustar00rootroot00000000000000/* TasksWindow.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 TASKSWINDOW_H #define TASKSWINDOW_H #include "CharmWindow.h" class TasksView; class TasksWindow : public CharmWindow { Q_OBJECT public: explicit TasksWindow( QWidget* parent = nullptr ); ~TasksWindow(); void stateChanged( State previous ) override; void sendCommand( CharmCommand* ) override; void sendCommandRollback( CharmCommand* ) override; void commitCommand( CharmCommand* ) override; // restore the view void restore() override; public slots: void configurationChanged() override; protected: void insertEditMenu() override; signals: void emitCommand( CharmCommand* ) override; void emitCommandRollback( CharmCommand* ) override; void quit() override; private: TasksView* m_tasksView; }; #endif Charm-1.10.0/Charm/Widgets/TimeTrackingTaskSelector.cpp000066400000000000000000000257571260343353100227140ustar00rootroot00000000000000/* TimeTrackingTaskSelector.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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/Event.h" #include "Core/Task.h" #include #include #include #include #include #include #include #include #include #include #define CUSTOM_TASK_PROPERTY_NAME "CUSTOM_TASK_PROPERTY" 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 ) ) , m_selectedTask( 0 ) , m_manuallySelectedTask( 0 ) , m_taskManuallySelected( false ) { connect( m_menu, SIGNAL(triggered(QAction*)), SLOT(slotActionSelected(QAction*)) ); 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, SIGNAL(triggered(bool)), SLOT(slotGoStopToggled(bool)) ); 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, SIGNAL(triggered(bool)), SLOT(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, SIGNAL(triggered()), SLOT(slotManuallySelectTask()) ); } void TimeTrackingTaskSelector::populateEditMenu( QMenu* menu ) { menu->addAction( m_stopGoAction ); menu->addAction( m_editCommentAction ); menu->addAction( m_startOtherTaskAction ); } QSize TimeTrackingTaskSelector::sizeHint() const { const QSize stopGoButtonSizeHint = m_stopGoButton->sizeHint(); return QSize( 200, stopGoButtonSizeHint.height() ); // width is ignored anyway } void TimeTrackingTaskSelector::resizeEvent( QResizeEvent* ) { m_stopGoButton->resize( m_stopGoButton->sizeHint() ); m_stopGoButton->move( 0, 0 ); m_editCommentButton->resize( m_editCommentButton->sizeHint() ); m_editCommentButton->move( m_stopGoButton->width(), 0 ); const QSize space( width() - m_stopGoButton->width() - m_editCommentButton->width(), height() ); m_taskSelectorButton->resize( space ); m_taskSelectorButton->move( m_stopGoButton->width() + m_editCommentButton->width(), 0 ); } QMenu* TimeTrackingTaskSelector::menu() const { return m_menu; } static QString escapeAmpersands( QString text ) { text.replace( QLatin1String("&"), QLatin1String("&&") ); return text; } void TimeTrackingTaskSelector::populate( const QVector& summaries ) { // Don't repopulate while the menu is displayed; very ugly and it can wait. if (m_menu->isActiveWindow()) return; m_menu->clear(); QMap addedTasks; bool addedAction = false; Q_FOREACH( const WeeklySummary& s, summaries ) { auto action = new QAction( escapeAmpersands( DATAMODEL->taskIdAndSmartNameString( s.task ) ), m_menu ); addedTasks.insert( s.task, action ); action->setProperty( CUSTOM_TASK_PROPERTY_NAME, QVariant::fromValue( s.task ) ); Q_ASSERT( action->property( CUSTOM_TASK_PROPERTY_NAME ).value() == s.task ); m_menu->addAction( action ); addedAction = true; } // insert the manually selected task, if one is set: if ( addedAction ) { m_menu->addSeparator(); addedAction = false; } if( m_manuallySelectedTask > 0 && ! addedTasks.contains( m_manuallySelectedTask )) { const Task& task = DATAMODEL->getTask( m_manuallySelectedTask ); auto action = new QAction( DATAMODEL->taskIdAndSmartNameString( task.id() ), m_menu ); addedTasks.insert( m_manuallySelectedTask, action ); action->setProperty( CUSTOM_TASK_PROPERTY_NAME, QVariant::fromValue( m_manuallySelectedTask ) ); m_menu->addAction( action ); } // ... add action to select a task: m_menu->addAction( m_startOtherTaskAction ); TaskIdList interestingTasks; interestingTasks += DATAMODEL->mostRecentlyUsedTasks(); interestingTasks += DATAMODEL->mostFrequentlyUsedTasks(); TaskIdList interestingTasksToAdd; while( interestingTasksToAdd.count() < 10 ) { // arbitrary hardcoded number warning if( interestingTasks.isEmpty() ) break; TaskId id = interestingTasks.takeFirst(); if( !addedTasks.contains( id ) && DATAMODEL->getTask(id).isCurrentlyValid() ) interestingTasksToAdd.append( id ); } qSort( interestingTasksToAdd.begin(), interestingTasksToAdd.end() ); foreach( TaskId id, interestingTasksToAdd ) { if( addedTasks.contains( id ) ) continue; if( !addedAction ) { m_menu->addSeparator(); addedAction = true; } auto action = new QAction( DATAMODEL->taskIdAndSmartNameString( id ), m_menu ); action->setProperty( CUSTOM_TASK_PROPERTY_NAME, QVariant::fromValue( id ) ); m_menu->addAction( action ); addedTasks.insert( id, action ); } // finally, select the task that the user has just selected if( m_taskManuallySelected ) { m_taskManuallySelected = false; auto action = addedTasks.value( m_manuallySelectedTask ); Q_ASSERT_X( action != 0, Q_FUNC_INFO, "the manually selected task should always be in the menu" ); // this sets the correct text on the button slotActionSelected( action ); } // enable the selector button if the menu is not empty 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 ); } 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 ); } } void TimeTrackingTaskSelector::slotActionSelected( QAction* action ) { TaskId taskId = action->property( CUSTOM_TASK_PROPERTY_NAME ).value(); const Task& task = DATAMODEL->getTask( taskId ); if ( task.isValid() ) { bool expired = !task.isCurrentlyValid(); bool trackable = task.trackable(); bool notTrackableAndExpired = ( !trackable && expired ); int id = task.id(); const QString name = task.name(); const 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 ).arg( name ).arg( expirationDate ) : expired ? tr( "The task %1 (%2) is expired since %3").arg( id ).arg( name ).arg( expirationDate ) : tr( "The task %1 (%2) is not trackable").arg( id ).arg( name ); QMessageBox::information( this, tr( "Please choose another task" ), message ); return; } } if( taskId > 0 ) { taskSelected( action->text(), taskId ); handleActiveEvents(); if ( !DATAMODEL->isTaskActive( taskId ) ) { if ( !DATAMODEL->activeEvents().isEmpty() ) emit stopEvents(); emit startEvent( taskId ); } } } void TimeTrackingTaskSelector::taskSelected( const QString& taskname, TaskId id ) { m_selectedTask = id; m_stopGoAction->setEnabled( true ); 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.taskname, 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(); } #include "moc_TimeTrackingTaskSelector.cpp" Charm-1.10.0/Charm/Widgets/TimeTrackingTaskSelector.h000066400000000000000000000050621260343353100223440ustar00rootroot00000000000000/* TimeTrackingTaskSelector.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 TimeTrackingTaskSelector : public QWidget { Q_OBJECT public: explicit TimeTrackingTaskSelector(QWidget *parent = nullptr); void populate( const QVector& summaries ); void handleActiveEvents(); void taskSelected( const WeeklySummary& ); void resizeEvent( QResizeEvent* ) override; QSize sizeHint() const override; QMenu* menu() const; void populateEditMenu( QMenu* ); signals: void startEvent( TaskId ); void stopEvents(); void updateSummariesPlease(); private slots: void slotActionSelected( QAction* ); void slotGoStopToggled( bool ); void slotEditCommentClicked(); void slotManuallySelectTask(); private: void taskSelected( const QString& taskname, 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; }; #endif // TIMETRACKINGTASKSELECTOR_H Charm-1.10.0/Charm/Widgets/TimeTrackingTaskSelector.ui000066400000000000000000000033201260343353100225250ustar00rootroot00000000000000 TaskSelector 0 0 400 36 0 0 Form background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0.553, y2:0.312909, stop:0 rgba(0, 0, 0, 255), stop:1 rgba(255, 255, 255, 255)); 1 0 0 0 12 ... 0 0 ... Charm-1.10.0/Charm/Widgets/TimeTrackingView.cpp000066400000000000000000000402211260343353100212020ustar00rootroot00000000000000/* TimeTrackingView.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 ) ) , m_dayOfWeek( 0 ) { 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, SIGNAL(startEvent(TaskId)), SIGNAL(startEvent(TaskId)) ); connect( m_taskSelector, SIGNAL(stopEvents()), SIGNAL(stopEvents()) ); connect( m_taskSelector, SIGNAL(updateSummariesPlease()), SLOT(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( "100:00" ) .adjusted( 0, 0, 2 * Margin, 2 * Margin ) ); const int dayWidth = fixedFontMetrics.width( "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(); } bool TimeTrackingView::isTracking() const { return DATAMODEL->activeEventCount() > 0; } void TimeTrackingView::configurationChanged() { m_fixedFont = font(); #ifdef Q_OS_OSX m_fixedFont.setFamily( "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; }; 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 ); int id = 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 ).arg( name ).arg( expirationDate ) : expired ? tr( "The task %1 (%2) is expired since %3").arg( id ).arg( name ).arg( expirationDate ) : tr( "The task %1 (%2) is not trackable").arg( id ).arg( name ); QMessageBox::information( this, tr( "Please choose another task" ), message ); return false; } return true; } #include "moc_TimeTrackingView.cpp" Charm-1.10.0/Charm/Widgets/TimeTrackingView.h000066400000000000000000000074431260343353100206600ustar00rootroot00000000000000/* TimeTrackingView.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 { DataField() : hasHighlight( false ), storeAsActive( false ) {} QString text; QBrush background; bool hasHighlight; // QBrush does not have isValid() bool storeAsActive; 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(); signals: void maybeShrink(); void startEvent( TaskId ); void stopEvents(); private 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; /** 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.10.0/Charm/Widgets/TimeTrackingWindow.cpp000066400000000000000000000633551260343353100215540ustar00rootroot00000000000000/* TimeTrackingWindow.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "TimeTrackingView.h" #include "Uniquifier.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/GetUserInfoJob.h" #include "Idle/IdleDetector.h" #include "Widgets/HttpJobProgressDialog.h" #include #include #include #include #include #include #include #include #include #include TimeTrackingWindow::TimeTrackingWindow( QWidget* parent ) : CharmWindow( tr( "Time Tracker" ), parent ) , m_weeklyTimesheetDialog( nullptr ) , m_monthlyTimesheetDialog( nullptr ) , m_activityReportDialog( nullptr ) , m_summaryWidget( new TimeTrackingView( this ) ) , m_billDialog( new BillDialog( this ) ) { setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed ); setWindowNumber( 3 ); setWindowIdentifier( QLatin1String( "window_tracking" ) ); setCentralWidget( m_summaryWidget ); connect( m_summaryWidget, SIGNAL(startEvent(TaskId)), SLOT(slotStartEvent(TaskId)) ); connect( m_summaryWidget, SIGNAL(stopEvents()), SLOT(slotStopEvent()) ); connect( &m_checkUploadedSheetsTimer, SIGNAL(timeout()), SLOT(slotCheckUploadedTimesheets()) ); connect( m_billDialog, SIGNAL(finished(int)), SLOT(slotBillGone(int)) ); connect( &m_checkCharmReleaseVersionTimer, SIGNAL(timeout()), SLOT(slotCheckForUpdatesAutomatic()) ); connect( &m_updateTasksDefinitionsTimer, SIGNAL(timeout()), SLOT(slotSyncTasksAutomatic()) ); //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 ( !QString::fromLatin1(UPDATE_CHECK_URL).isEmpty() ) { QTimer::singleShot( 1000, this, SLOT(slotCheckForUpdatesAutomatic()) ); m_checkCharmReleaseVersionTimer.start(); } #endif //Update tasks definitions once every 24h m_updateTasksDefinitionsTimer.setInterval(24 * 60 * 60 * 1000); QTimer::singleShot( 1000, this, SLOT(slotSyncTasksAutomatic()) ); m_updateTasksDefinitionsTimer.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(), SIGNAL(dateChanged()), SLOT(slotSelectTasksToShow()) ); DATAMODEL->registerAdapter( this ); m_summaryWidget->setSummaries( QVector() ); m_summaryWidget->handleActiveEvents(); break; } case Disconnecting: case ShuttingDown: default: break; } } void TimeTrackingWindow::restore() { show(); } void TimeTrackingWindow::quit() { } // model adapter: void TimeTrackingWindow::resetTasks() { slotSelectTasksToShow(); } void TimeTrackingWindow::taskAboutToBeAdded( TaskId parent, int pos ) { } void TimeTrackingWindow::taskAdded( TaskId id ) { slotSelectTasksToShow(); } void TimeTrackingWindow::taskModified( TaskId id ) { slotSelectTasksToShow(); } void TimeTrackingWindow::taskParentChanged( TaskId task, TaskId oldParent, TaskId newParent ) { slotSelectTasksToShow(); } void TimeTrackingWindow::taskAboutToBeDeleted( TaskId ) { } void TimeTrackingWindow::taskDeleted( TaskId id ) { slotSelectTasksToShow(); } void TimeTrackingWindow::resetEvents() { slotSelectTasksToShow(); } void TimeTrackingWindow::eventAboutToBeAdded( EventId id ) { } void TimeTrackingWindow::eventAdded( EventId id ) { slotSelectTasksToShow(); } void TimeTrackingWindow::eventModified( EventId id, Event discardedEvent ) { slotSelectTasksToShow(); } void TimeTrackingWindow::eventAboutToBeDeleted( EventId id ) { } void TimeTrackingWindow::eventDeleted( EventId id ) { slotSelectTasksToShow(); } void TimeTrackingWindow::eventActivated( EventId id ) { 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 = item.task().name(); QMessageBox::critical( this, tr( "Invalid task" ), tr( "Task '%1' is no longer valid, so can't be started" ).arg( nm ) ); } } 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(); 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, SIGNAL(finished(int)), this, SLOT(slotActivityReportPreview(int)) ); m_activityReportDialog->show(); } void TimeTrackingWindow::resetWeeklyTimesheetDialog() { delete m_weeklyTimesheetDialog; m_weeklyTimesheetDialog = new WeeklyTimesheetConfigurationDialog( this ); m_weeklyTimesheetDialog->setAttribute( Qt::WA_DeleteOnClose ); connect( m_weeklyTimesheetDialog, SIGNAL(finished(int)), this, SLOT(slotWeeklyTimesheetPreview(int)) ); } void TimeTrackingWindow::slotWeeklyTimesheetReport() { resetWeeklyTimesheetDialog(); m_weeklyTimesheetDialog->show(); } void TimeTrackingWindow::resetMonthlyTimesheetDialog() { delete m_monthlyTimesheetDialog; m_monthlyTimesheetDialog = new MonthlyTimesheetConfigurationDialog( this ); m_monthlyTimesheetDialog->setAttribute( Qt::WA_DeleteOnClose ); connect( m_monthlyTimesheetDialog, SIGNAL(finished(int)), this, SLOT(slotMonthlyTimesheetPreview(int)) ); } void TimeTrackingWindow::slotMonthlyTimesheetReport() { resetMonthlyTimesheetDialog(); m_monthlyTimesheetDialog->show(); } 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( this ); } 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+=".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 ) { GetProjectCodesJob* client = new GetProjectCodesJob( this ); if ( mode == Verbose ) { HttpJobProgressDialog* dialog = new HttpJobProgressDialog( client, this ); dialog->setWindowTitle( tr("Downloading") ); } else client->setVerbose( false ); connect( client, SIGNAL(finished(HttpJob*)), this, SLOT(slotTasksDownloaded(HttpJob*)) ); client->start(); } void TimeTrackingWindow::slotSyncTasksAutomatic() { // check if HttpJob is possible if ( HttpJob::credentialsAvailable() && !HttpJob::lastAuthenticationFailed() ) { 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 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" ), "", 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" ), "", 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->show(); 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, SIGNAL(finished(CheckForUpdatesJob::JobData)), this, SLOT(slotCheckForUpdates(CheckForUpdatesJob::JobData)) ); const QString urlString = UPDATE_CHECK_URL; checkForUpdates->setUrl( QUrl( urlString ) ); 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( QLatin1String( "UpdateChecker" ) ); const QString skipVersion = settings.value( QLatin1String( "skip-version" ) ).toString(); if ( ( skipVersion == releaseVersion ) && !data.verbose ) return; if ( Charm::versionLessThan( CHARM_VERSION, 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 = CHARM_VERSION; localVersion.truncate( releaseVersion.length() ); CharmNewReleaseDialog dialog( this ); dialog.setVersion( releaseVersion, localVersion ); dialog.setDownloadLink( link ); dialog.setReleaseInformationLink( releaseInfoLink ); dialog.exec(); } void TimeTrackingWindow::maybeIdle( IdleDetector* detector ) { Q_ASSERT( detector ); static bool inProgress = false; if ( inProgress == true ) return; Uniquifier u( &inProgress ); Q_FOREACH( const IdleDetector::IdlePeriod& p, detector->idlePeriods() ) { qDebug() << "ApplicationCore::slotMaybeIdle: computer might be have been idle from" << p.first << "to" << p.second; } // handle idle merging: IdleCorrectionDialog dialog( this ); MakeTemporarilyVisible m( this ); dialog.exec(); switch( dialog.result() ) { case IdleCorrectionDialog::Idle_Ignore: break; case IdleCorrectionDialog::Idle_EndEvent: { 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: Q_ASSERT( !detector->idlePeriods().isEmpty() ); const IdleDetector::IdlePeriod period = detector->idlePeriods().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 ); } } break; } default: break; // should not happen } detector->clear(); } static void setValueIfNotNull(QSettings* s, const QString& key, const QString& value ) { if ( !value.isNull() ) s->setValue( key, value ); else s->remove( key ); } 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 emit showNotification( title, detailsText ); getUserInfo(); } QSettings settings; settings.beginGroup( "httpconfig" ); setValueIfNotNull( &settings, QLatin1String("username"), exporter.metadata( QLatin1String("username") ) ); setValueIfNotNull( &settings, QLatin1String("portalUrl"), exporter.metadata( QLatin1String("portal-url") ) ); setValueIfNotNull( &settings, QLatin1String("loginUrl"), exporter.metadata( QLatin1String("login-url") ) ); setValueIfNotNull( &settings, QLatin1String("timesheetUploadUrl"), exporter.metadata( QLatin1String("timesheet-upload-url") ) ); setValueIfNotNull( &settings, QLatin1String("projectCodeDownloadUrl"), exporter.metadata( QLatin1String("project-code-download-url") ) ); settings.endGroup(); settings.beginGroup( "users" ); settings.setValue( QLatin1String("portalUrl"), QLatin1String("https://lotsofcake.kdab.com:443/KdabHome/apps/portal/")); settings.setValue( QLatin1String("loginUrl"), QLatin1String("https://lotsofcake.kdab.com:443/KdabHome/apps/portal/j_security_check")); settings.setValue( QLatin1String("userInfoDownloadUrl"), QLatin1String("https://lotsofcake.kdab.com/KdabHome/rest/user")); settings.endGroup(); ApplicationCore::instance().setHttpActionsVisible( true ); } 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::getUserInfo() { if (!HttpJob::credentialsAvailable()) return; QSettings settings; settings.beginGroup( "httpconfig" ); m_user = settings.value("username").toString(); settings.endGroup(); settings.beginGroup( "users" ); settings.setValue( QLatin1String("userInfoDownloadUrl"), QLatin1String("https://lotsofcake.kdab.com/KdabHome/rest/user?user=") + m_user ); settings.endGroup(); GetUserInfoJob *client = new GetUserInfoJob(this,"users"); client->schema() = m_user; HttpJobProgressDialog* dialog = new HttpJobProgressDialog( client, this ); dialog->setWindowTitle( tr("Downloading weekly hours") ); connect( client, SIGNAL(finished(HttpJob*)), this, SLOT(slotUserInfoDownloaded(HttpJob*)) ); client->start(); } void TimeTrackingWindow::slotUserInfoDownloaded( HttpJob* job_ ) { GetUserInfoJob * 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; } QByteArray readData = job->userInfo(); int index = readData.indexOf("weeklyHours"); index += 13; QString weeklyH = readData.mid(index,2).trimmed(); if (weeklyH.length() != 2 ) weeklyH = "40"; QSettings settings; settings.beginGroup( "users" ); settings.setValue( QLatin1String("weeklyhours"), weeklyH); settings.endGroup(); } #include "moc_TimeTrackingWindow.cpp" Charm-1.10.0/Charm/Widgets/TimeTrackingWindow.h000066400000000000000000000117521260343353100212130ustar00rootroot00000000000000/* TimeTrackingWindow.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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/ViewInterface.h" #include "Core/CharmDataModelAdapterInterface.h" #include "HttpClient/CheckForUpdatesJob.h" #include "CharmWindow.h" #include "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(); enum VerboseMode { Verbose = 0, Silent }; // application: void stateChanged( State previous ) override; void restore() override; void quit() 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 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 slotImportTasks(); void slotExportTasks(); void maybeIdle( IdleDetector* idleDetector ); void slotTasksDownloaded( HttpJob* ); void slotUserInfoDownloaded( HttpJob* ); void slotCheckForUpdatesManual(); protected: void insertEditMenu() override; private slots: void slotStartEvent( TaskId ); 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 configurationChanged() override; signals: void emitCommand( CharmCommand* ); void emitCommandRollback( CharmCommand* ); void showNotification( const QString& title, const QString& message ); private: void resetWeeklyTimesheetDialog(); void resetMonthlyTimesheetDialog(); void showPreview( ReportConfigurationDialog*, int result ); //ugly but private: void importTasksFromDeviceOrFile( QIODevice* device, const QString& filename, bool verbose = true ); void getUserInfo(); void startCheckForUpdates( VerboseMode mode = Silent ); void informUserAboutNewRelease( const QString& releaseVersion, const QUrl& link , const QString& releaseInfoLink ); WeeklyTimesheetConfigurationDialog* m_weeklyTimesheetDialog; MonthlyTimesheetConfigurationDialog* m_monthlyTimesheetDialog; ActivityReportConfigurationDialog *m_activityReportDialog; TimeTrackingView* m_summaryWidget; QVector m_summaries; QTimer m_checkUploadedSheetsTimer; QTimer m_checkCharmReleaseVersionTimer; QTimer m_updateTasksDefinitionsTimer; BillDialog *m_billDialog; CheckForUpdatesJob* m_checkForUpdatesJob; QString m_user; }; #endif Charm-1.10.0/Charm/Widgets/Timesheet.cpp000066400000000000000000000072411260343353100177220ustar00rootroot00000000000000/* Timesheet.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 ) , m_rootTask( 0 ) , m_activeTasksOnly( false ) { } 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() { qDebug() << "TimeSheet::slotSaveToXml: creating XML time sheet"; // 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(); 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() { qDebug() << "TimeSheet::slotSaveToText: creating text file with totals"; // first, ask for a file name: const QString filename = getFileName( "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.10.0/Charm/Widgets/Timesheet.h000066400000000000000000000043541260343353100173710ustar00rootroot00000000000000/* Timesheet.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 ); virtual ~TimeSheetReport(); virtual void setReportProperties( const QDate& start, const QDate& end, TaskId rootTask, bool activeTasksOnly ); protected: virtual QString suggestedFileName() const = 0; virtual void update() = 0; virtual QByteArray saveToText() = 0; virtual QByteArray saveToXml() = 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; }; #endif Charm-1.10.0/Charm/Widgets/TrayIcon.cpp000066400000000000000000000033761260343353100175300ustar00rootroot00000000000000/* TrayIcon.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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) { connect(this, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), SLOT(slotActivated(QSystemTrayIcon::ActivationReason))); } 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().toggleShowHide(); #endif break; case QSystemTrayIcon::MiddleClick: // TODO: Start task? ApplicationCore::instance().slotStopAllTasks(); break; case QSystemTrayIcon::Unknown: default: break; } } #include "moc_TrayIcon.cpp" Charm-1.10.0/Charm/Widgets/TrayIcon.h000066400000000000000000000022351260343353100171660ustar00rootroot00000000000000/* TrayIcon.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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); virtual ~TrayIcon(); private Q_SLOTS: void slotActivated(QSystemTrayIcon::ActivationReason); }; #endif // TRAYICON_H Charm-1.10.0/Charm/Widgets/WeeklyTimesheet.cpp000066400000000000000000000535531260343353100211120ustar00rootroot00000000000000/* WeeklyTimesheet.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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 "Widgets/HttpJobProgressDialog.h" #include "SelectTaskDialog.h" #include "ViewHelpers.h" #include "ui_WeeklyTimesheetConfigurationDialog.h" namespace { static const char * SETTING_GRP_TIMESHEETS = "timesheets"; static const char * SETTING_VAL_FIRSTYEAR = "firstYear"; static const char * SETTING_VAL_FIRSTWEEK = "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, SIGNAL(accepted()), this, SLOT(accept()) ); connect( m_ui->buttonBox, SIGNAL(rejected()), this, SLOT(reject()) ); connect( m_ui->comboBoxWeek, SIGNAL(currentIndexChanged(int)), SLOT(slotWeekComboItemSelected(int)) ); connect( m_ui->toolButtonSelectTask, SIGNAL(clicked()), SLOT(slotSelectTask()) ); connect( m_ui->checkBoxSubTasksOnly, SIGNAL(toggled(bool)), SLOT(slotCheckboxSubtasksOnlyChecked(bool)) ); 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(), SIGNAL(dateChanged()), SLOT(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( QWidget* parent ) { 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( parent ); 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 }; 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 ) , m_weekNumber( 0 ) , m_yearOfWeek( 0 ) { QPushButton* upload = uploadButton(); connect( upload, SIGNAL(clicked()), SLOT(slotUploadTimesheet()) ); connect( this, SIGNAL(anchorClicked(QUrl)), SLOT(slotLinkClicked(QUrl)) ); if (!HttpJob::credentialsAvailable()) 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() { auto client = new UploadTimesheetJob( this ); auto dialog = new HttpJobProgressDialog( client, this ); dialog->setWindowTitle( tr("Uploading") ); connect( client, SIGNAL(finished(HttpJob*)), this, SLOT(slotTimesheetUploaded(HttpJob*)) ); client->setFileName( suggestedFileName() ); client->setPayload( saveToXml() ); client->start(); uploadButton()->setEnabled(false); } void WeeklyTimeSheetReport::slotTimesheetUploaded(HttpJob* client) { if ( client->error() == HttpJob::Canceled ) { uploadButton()->setEnabled(true); return; } if ( client->error() ) { uploadButton()->setEnabled(true); QMessageBox::critical(this, tr("Error"), tr("Could not upload timesheet: %1").arg( client->errorString() ) ); } else { addUploadedTimesheet(m_yearOfWeek, m_weekNumber); QMessageBox::information(this, tr("Timesheet Uploaded"), tr("Your timesheet was successfully uploaded.")); } } QString WeeklyTimeSheetReport::suggestedFileName() const { return tr( "WeeklyTimeSheet-%1-%2" ).arg( m_yearOfWeek ).arg( m_weekNumber, 2, 10, QChar('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( "body" ); // create the caption: { QDomElement headline = doc.createElement( "h1" ); QDomText text = doc.createTextNode( tr( "Weekly Time Sheet" ) ); headline.appendChild( text ); body.appendChild( headline ); } { QDomElement headline = doc.createElement( "h3" ); QString content = tr( "Report for %1, Week %2 (%3 to %4)" ) .arg( CONFIGURATION.user.name() ) .arg( m_weekNumber, 2, 10, QChar('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( "a" ); previousLink.setAttribute( "href" , "Previous" ); QDomText previousLinkText = doc.createTextNode( tr( "" ) ); previousLink.appendChild( previousLinkText ); body.appendChild( previousLink ); QDomElement nextLink = doc.createElement( "a" ); nextLink.setAttribute( "href" , "Next" ); QDomText nextLinkText = doc.createTextNode( tr( "" ) ); nextLink.appendChild( nextLinkText ); body.appendChild( nextLink ); QDomElement paragraph = doc.createElement( "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( "table" ); table.setAttribute( "width", "100%" ); table.setAttribute( "align", "left" ); table.setAttribute( "cellpadding", "3" ); table.setAttribute( "cellspacing", "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( "tr" ); headerRow.setAttribute( "class", "header_row" ); table.appendChild( headerRow ); QDomElement headerDayRow = doc.createElement( "tr" ); headerDayRow.setAttribute( "class", "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( "th" ); QDomText text = doc.createTextNode( Headlines[i] ); header.appendChild( text ); headerRow.appendChild( header ); QDomElement dayHeader = doc.createElement( "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( "tr" ); if (i % 2) row.setAttribute( "class", "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( "td" ); cell.setAttribute( "align", column == Column_Task ? "left" : "center" ); if ( column == Column_Task ) { QString style = QString( "text-indent: %1px;" ) .arg( 9 * timeSheetInfo[i].indentation ); cell.setAttribute( "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( "tr" ); totals.setAttribute( "class", "header_row" ); table.appendChild( totals ); for ( int i = 0; i < NumberOfColumns; ++i ) { QDomElement cell = doc.createElement( "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() { try { WeeklyTimesheetXmlWriter timesheet; timesheet.setDataModel( DATAMODEL ); timesheet.setYear( m_yearOfWeek ); timesheet.setWeekNumber( m_weekNumber ); timesheet.setRootTask( rootTask() ); 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, QChar('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() == "Previous" ? startDate().addDays( -7 ) : startDate().addDays( 7 ); QDate end = which.toString() == "Previous" ? endDate().addDays( -7 ) : endDate().addDays( 7 ); setReportProperties( start, end, rootTask(), activeTasksOnly() ); } Charm-1.10.0/Charm/Widgets/WeeklyTimesheet.h000066400000000000000000000055611260343353100205530ustar00rootroot00000000000000/* WeeklyTimesheet.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2014-2015 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(); void showReportPreviewDialog( QWidget* parent ) override; void showEvent( QShowEvent* ) override; void setDefaultWeek( int yearOfWeek, int week ); public Q_SLOTS: void accept() override; private 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 ); virtual ~WeeklyTimeSheetReport(); void setReportProperties( const QDate& start, const QDate& end, TaskId rootTask, bool activeTasksOnly ) override; private slots: void slotUploadTimesheet(); void slotTimesheetUploaded( HttpJob* ); void slotLinkClicked( const QUrl& which ); private: QString suggestedFileName() const override; void update() override; QByteArray saveToXml() override; QByteArray saveToText() override; private: // properties of the report: int m_weekNumber; int m_yearOfWeek; }; #endif Charm-1.10.0/Charm/Widgets/WeeklyTimesheetConfigurationDialog.ui000066400000000000000000000175421260343353100246130ustar00rootroot00000000000000 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.10.0/Charm/Widgets/report_stylesheet.sty000066400000000000000000000011611260343353100216070ustar00rootroot00000000000000h1 { 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.10.0/Charm/bill.jpg000066400000000000000000001221211260343353100153000ustar00rootroot00000000000000JFIFHHC     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.10.0/Charm/charmtimetracker.desktop000066400000000000000000000022601260343353100205750ustar00rootroot00000000000000[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-128x128 Type=Application Categories=Qt;KDE;Utility; X-KDE-StartupNotify=true Charm-1.10.0/CharmCMake.h.cmake000066400000000000000000000013341260343353100160070ustar00rootroot00000000000000/* Define to the version from CMake */ #define CHARM_VERSION "@Charm_VERSION@" /* Define if you have enabled the idle detection */ #cmakedefine CHARM_IDLE_DETECTION /* Defined if idle detection is available on X11 */ #cmakedefine CHARM_IDLE_DETECTION_AVAILABLE_X11 1 /* Delay for idle detection, default is 360 */ #define CHARM_IDLE_TIME @CHARM_IDLE_TIME@ /* Define the url where to check for updates */ #define UPDATE_CHECK_URL "@UPDATE_CHECK_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 Charm-1.10.0/Core/000077500000000000000000000000001260343353100135135ustar00rootroot00000000000000Charm-1.10.0/Core/CMakeLists.txt000066400000000000000000000011001260343353100162430ustar00rootroot00000000000000if(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 ) ADD_LIBRARY( CharmCore STATIC ${CharmCore_SRCS} ) TARGET_LINK_LIBRARIES( CharmCore ${QT_LIBRARIES} ) Charm-1.10.0/Core/CharmCommand.cpp000066400000000000000000000045421260343353100165550ustar00rootroot00000000000000/* CharmCommand.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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_owner(0), 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; } CommandEmitterInterface* CharmCommand::owner() const { return m_owner; } void CharmCommand::requestExecute() { emit emitExecute(this); } void CharmCommand::requestRollback() { emit emitRollback(this); } void CharmCommand::requestSlotEventIdChanged(int oldId, int newId) { 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 ); } #include "moc_CharmCommand.cpp" Charm-1.10.0/Core/CharmCommand.h000066400000000000000000000065601260343353100162240ustar00rootroot00000000000000/* CharmCommand.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 ControllerInterface; 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 ); virtual ~CharmCommand(); QString description() const; virtual bool prepare() = 0; virtual bool execute( ControllerInterface* controller ) = 0; virtual bool rollback( ControllerInterface* controller ) { return false; } 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){} 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; const QString m_description; }; #endif Charm-1.10.0/Core/CharmConstants.cpp000066400000000000000000000125121260343353100171470ustar00rootroot00000000000000/* CharmConstants.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 = "EventsInLeafsOnly"; const QString MetaKey_OneEventAtATime = "OneEventAtATime"; const QString MetaKey_MainWindowGeometry = "MainWindowGeometry"; const QString MetaKey_MainWindowVisible = "MainWindowVisible"; const QString MetaKey_MainWindowGUIStateSelectedTask = "MainWindowGUIStateSelectedTask"; const QString MetaKey_MainWindowGUIStateExpandedTasks = "MainWindowGUIStateExpandedTasks"; const QString MetaKey_MainWindowGUIStateShowExpiredTasks = "MainWindowGUIStateShowExpiredTasks"; const QString MetaKey_MainWindowGUIStateShowCurrentTasks = "MainWindowGUIStateShowCurrentTasks"; const QString MetaKey_TimeTrackerGeometry = "TimeTrackerGeometry"; const QString MetaKey_TimeTrackerVisible = "TimeTrackerVisible"; const QString MetaKey_ReportsRecentSavePath = "ReportsRecentSavePath"; const QString MetaKey_ExportToXmlRecentSavePath = "ExportToXmlSavePath"; const QString MetaKey_TimesheetSubscribedOnly = "TimesheetSubscribedOnly"; const QString MetaKey_TimesheetActiveOnly = "TimesheetActiveOnly"; const QString MetaKey_TimesheetRootTask = "TimesheetRootTask"; const QString MetaKey_LastEventEditorDateTime= "LastEventEditorDateTime"; const QString MetaKey_Key_InstallationId = "InstallationId"; const QString MetaKey_Key_UserName = "UserName"; const QString MetaKey_Key_UserId = "UserId"; const QString MetaKey_Key_LocalStorageDatabase = "LocalStorageDatabase"; const QString MetaKey_Key_LocalStorageType = "LocalStorageType"; const QString MetaKey_Key_SubscribedTasksOnly = "SubscribedTasksOnly"; const QString MetaKey_Key_TimeTrackerFontSize = "TimeTrackerFontSize"; const QString MetaKey_Key_24hEditing = "Key24hEditing"; const QString MetaKey_Key_DurationFormat = "DurationFormat"; const QString MetaKey_Key_IdleDetection = "IdleDetection"; const QString MetaKey_Key_WarnUnuploadedTimesheets = "WarnUnuploadedTimesheets"; const QString MetaKey_Key_RequestEventComment = "RequestEventComment"; const QString MetaKey_Key_ToolButtonStyle = "ToolButtonStyle"; const QString MetaKey_Key_ShowStatusBar = "ShowStatusBar"; const QString MetaKey_Key_EnableCommandInterface = "EnableCommandInterface"; const QString TrueString( "true" ); const QString FalseString( "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 QLatin1String("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( QChar( '0' ) ) << hours << qSetFieldWidth( 0 ) << ":" << qSetFieldWidth( 2 ) << minutes; return text; } else { //Decimal return formatDecimal(hours + minutes / 60.0 ); } } Charm-1.10.0/Core/CharmConstants.h000066400000000000000000000102061260343353100166120ustar00rootroot00000000000000/* CharmConstants.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 "CharmDatabaseSchemaVersion" #define CHARM_DATABASE_VERSION_BEFORE_TASK_EXPIRY 2 #define CHARM_DATABASE_VERSION_BEFORE_TRACKABLE 3 #define CHARM_DATABASE_VERSION 4 #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 "sqlite" #define CHARM_MYSQL_BACKEND_DESCRIPTOR "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_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 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.10.0/Core/CharmDataModel.cpp000066400000000000000000000517471260343353100170420ustar00rootroot00000000000000/* CharmDataModel.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 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 { qDebug() << "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 { qDebug() << "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 ) + '/' + 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 << tr( "%1 - %2 %3" ) .arg( hoursAndMinutes( event.duration() ) ) .arg( task.id(), taskIdLength, 10, QChar( '0' ) ) .arg( fullTaskName( task ) ); } } return eStrList.join( "\n" ); } QString CharmDataModel::taskIdAndFullNameString(TaskId id) const { return QString("%1 %2") .arg( id, CONFIGURATION.taskPaddingLength, 10, QChar( '0' ) ) .arg( fullTaskName( getTask( id ) ) ); } QString CharmDataModel::taskIdAndSmartNameString(TaskId id) const { return QString("%1 %2") .arg( id, CONFIGURATION.taskPaddingLength, 10, QChar( '0' ) ) .arg( smartTaskName( getTask( id ) ) ); } QString CharmDataModel::taskIdAndNameString(TaskId id) const { return QString("%1 %2") .arg( id, CONFIGURATION.taskPaddingLength, 10, QChar( '0' ) ) .arg( getTask( id ).name() ); } int CharmDataModel::totalDuration() const { int totalDuration = 0; Q_FOREACH ( EventId eventId, activeEvents() ) { Event event = eventForId( eventId ); if ( event.isValid() ) { totalDuration += event.duration(); } } return totalDuration; } QString CharmDataModel::totalDurationString() const { return hoursAndMinutes( totalDuration() ); } void CharmDataModel::updateToolTip() { QString toolTip; int numEvents = activeEvents().count(); switch( numEvents ) { case 0: toolTip = tr( "No active events" ); break; case 1: toolTip = eventsString(); break; default: toolTip = tr( "%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 { EventIdList events; EventMap::const_iterator it; for ( it = m_events.begin(); it != m_events.end(); ++it ) { const Event& event( it->second ); if ( event.startDateTime().date() >= start && event.startDateTime().date() < end ) { 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; } struct TaskWithCount { TaskId id; unsigned int count; bool operator<( const TaskWithCount& other ) const { return count < other.count; } }; struct TaskWithLastUseDate { TaskId id; QDateTime lastUse; bool operator<( const TaskWithLastUseDate& other ) const { return lastUse < other.lastUse; } }; TaskIdList CharmDataModel::mostFrequentlyUsedTasks() const { QMap mfuMap; const EventMap& events = eventMap(); for( EventMap::const_iterator it = events.begin(); it != events.end(); ++it ) { const TaskId id = it->second.taskId(); // process use count const unsigned count = mfuMap[id] + 1; mfuMap[id] = count; } std::priority_queue mfuTasks; for( QMap::const_iterator it = mfuMap.constBegin(); it != mfuMap.constEnd(); ++it ) { TaskWithCount t; t.id = it.key(); t.count = it.value(); mfuTasks.push( t ); } TaskIdList mfu; while( ! mfuTasks.empty() ) { const TaskWithCount t = mfuTasks.top(); mfuTasks.pop(); mfu.append( t.id ); } return mfu; } TaskIdList CharmDataModel::mostRecentlyUsedTasks() const { QMap mruMap; const EventMap& events = eventMap(); for( EventMap::const_iterator it = events.begin(); it != events.end(); ++it ) { const TaskId id = it->second.taskId(); // process use date const QDateTime date = it->second.startDateTime(); mruMap[id]= qMax( mruMap[id], date ); } std::priority_queue mruTasks; for( QMap::const_iterator it = mruMap.constBegin(); it != mruMap.constEnd(); ++it ) { TaskWithLastUseDate t; t.id = it.key(); t.lastUse = it.value(); if( t.id != 0 ) mruTasks.push( t ); } TaskIdList mru; while( ! mruTasks.empty() ) { const TaskWithLastUseDate t = mruTasks.top(); mruTasks.pop(); Q_ASSERT( t.id != 0 ); mru.append( t.id ); } return mru; } 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; } #include "moc_CharmDataModel.cpp" Charm-1.10.0/Core/CharmDataModel.h000066400000000000000000000150211260343353100164700ustar00rootroot00000000000000/* CharmDataModel.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); 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; 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 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 slots: void eventUpdateTimerEvent(); private: // functions only used for testing: CharmDataModel* clone() const; }; #endif Charm-1.10.0/Core/CharmDataModelAdapterInterface.h000066400000000000000000000040741260343353100216200ustar00rootroot00000000000000/* CharmDataModelAdapterInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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.10.0/Core/CharmExceptions.cpp000066400000000000000000000032351260343353100173160ustar00rootroot00000000000000/* CharmExceptions.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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.10.0/Core/CharmExceptions.h000066400000000000000000000035271260343353100167670ustar00rootroot00000000000000/* CharmExceptions.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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.10.0/Core/CommandEmitterInterface.h000066400000000000000000000021331260343353100204140ustar00rootroot00000000000000/* CommandEmitterInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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.10.0/Core/Configuration.cpp000066400000000000000000000142461260343353100170350ustar00rootroot00000000000000/* Configuration.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 "default" #else #define DEFAULT_CONFIG_GROUP "debug" #endif Configuration& Configuration::instance() { static Configuration configuration; return configuration; } Configuration::Configuration() : taskPrefilteringMode( TaskPrefilter_ShowAll ) , timeTrackerFontSize( TimeTrackerFont_Regular ) , durationFormat( Minutes ) , toolButtonStyle( Qt::ToolButtonFollowStyle ) , showStatusBar( true ) , detectIdling( true ) , warnUnuploadedTimesheets( true ) , requestEventComment( false ) , enableCommandInterface( false ) , configurationName( DEFAULT_CONFIG_GROUP ) , installationId( 0 ) , newDatabase( false ) , failure( false ) , taskPaddingLength( 6 ) // arbitrary { } Configuration::Configuration( bool _eventsInLeafsOnly, bool _oneEventAtATime, User _user, TaskPrefilteringMode _taskPrefilteringMode, TimeTrackerFontSize _timeTrackerFontSize, DurationFormat _durationFormat, bool _detectIdling, Qt::ToolButtonStyle _buttonstyle, bool _showStatusBar, bool _warnUnuploadedTimesheets, bool _requestEventComment, bool _enableCommandInterface ) : taskPrefilteringMode( _taskPrefilteringMode ) , timeTrackerFontSize( _timeTrackerFontSize ) , durationFormat( _durationFormat ) , toolButtonStyle( _buttonstyle ) , showStatusBar( _showStatusBar ) , detectIdling ( _detectIdling ) , warnUnuploadedTimesheets( _warnUnuploadedTimesheets ) , requestEventComment( _requestEventComment ) , enableCommandInterface( _enableCommandInterface ) , configurationName( DEFAULT_CONFIG_GROUP ) , installationId( 0 ) , newDatabase( false ) , failure( false ) , taskPaddingLength( 6 ) // arbitrary { } 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; } 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( "(Configuration::writeTo stored configuration)" ); } bool Configuration::readFrom( QSettings& settings ) { bool complete = true; if ( settings.contains( MetaKey_Key_InstallationId ) ) { installationId = settings.value( MetaKey_Key_InstallationId ).toInt(); } 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( "(Configuration::readFrom loaded configuration)" ); 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; } Charm-1.10.0/Core/Configuration.h000066400000000000000000000072121260343353100164750ustar00rootroot00000000000000/* Configuration.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 ); User user; // this user's id TaskPrefilteringMode taskPrefilteringMode; TimeTrackerFontSize timeTrackerFontSize; DurationFormat durationFormat; Qt::ToolButtonStyle toolButtonStyle; bool showStatusBar; bool detectIdling; bool warnUnuploadedTimesheets; bool requestEventComment; bool enableCommandInterface; // these are stored in QSettings, since we need this information to locate and open the database: QString configurationName; int installationId; QString localStorageType; // SqLite, MySql, ... QString localStorageDatabase; // database name (path, with sqlite) bool newDatabase; // true if the configuration has just been created bool failure; // used to reconfigure on failures QString failureMessage; // a message to show the user if something is wrong with the configuration // appearance properties int taskPaddingLength; // auto-determined 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( bool eventsInLeafsOnly, bool oneEventAtATime, User user, TaskPrefilteringMode taskPrefilteringMode, TimeTrackerFontSize, DurationFormat durationFormat, bool detectIdling, Qt::ToolButtonStyle buttonstyle, bool showStatusBar, bool warnUnuploadedTimesheets, bool _requestEventComment, bool enableCommandInterface ); Configuration(); }; #endif Charm-1.10.0/Core/Controller.cpp000066400000000000000000000355701260343353100163540ustar00rootroot00000000000000/* Controller.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 "StorageInterface.h" #include "Task.h" #include Controller::Controller( QObject* parent_ ) : QObject( parent_ ) , ControllerInterface() , m_storage( nullptr ) { } 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 ) { qDebug() << Q_FUNC_INFO << "adding task" << task.id() << "to parent" << task.parent(); 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 ) { // find it qDebug() << Q_FUNC_INFO << "committing changes to task" << task.id(); // modify the task itself: bool result = m_storage->modifyTask( task ); Q_ASSERT( result ); if ( ! result ) { qDebug() << Q_FUNC_INFO << "modifyTask failed!"; return result; } updateSubscriptionForTask( task ); emit( taskUpdated( task ) ); return true; } bool Controller::deleteTask( const Task& task ) { qDebug() << Q_FUNC_INFO << "deleting task" << task.id(); 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 ) { qDebug() << Q_FUNC_INFO << "replacing all 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 // qDebug() << "Application::enterConnectingState: closing existing storage interface"; 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 ) } }; 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 ); 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 ); } StorageInterface* Controller::storage() { return m_storage; } const QString MetaDataElement ( "metadata" ); const QString ExportRootElement( "charmdatabase" ); const QString VersionElement( "version" ); const QString TasksElement( "tasks" ); const QString EventsElement( "events" ); QDomDocument Controller::exportDatabasetoXml() const { QDomDocument document( "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( 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( Event event, events ) { QDomElement element = event.toXml( document ); eventsElement.appendChild( element ); } root.appendChild( eventsElement ); // qDebug() << document.toString( 4 ); 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( "version" ).toInt( &ok ); if ( !ok ) throw XmlSerializationException( QObject::tr( "Syntax error, no version attribute found." ) ); QDomElement metadataElement = rootElement.firstChildElement( MetaDataElement ); 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 ); } #include "moc_Controller.cpp" Charm-1.10.0/Core/Controller.h000066400000000000000000000057441260343353100160210ustar00rootroot00000000000000/* Controller.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 "Task.h" #include "Event.h" #include "ControllerInterface.h" class StorageInterface; class Controller : public QObject, public ControllerInterface { Q_OBJECT public: explicit Controller( QObject* parent = nullptr ); ~Controller(); void stateChanged( State previous, State next ) override; void persistMetaData( Configuration& ) override; void provideMetaData( Configuration& ) override; bool initializeBackEnd( const QString& name ) override; bool connectToBackend() override; bool disconnectFromBackend() override; StorageInterface* storage() override; // FIXME add the add/modify/delete functions will not be slots anymore Event makeEvent( const Task& ) override; Event cloneEvent( const Event& ) override; bool modifyEvent( const Event& ) override; bool deleteEvent( const Event& ) override; bool addTask( const Task& parent ) override; bool modifyTask( const Task& ) override; bool deleteTask( const Task& ) override; bool setAllTasks( const TaskList& ) override; QDomDocument exportDatabasetoXml() const override; QString importDatabaseFromXml( const QDomDocument& ) override; void updateModelEventsAndTasks(); public slots: void executeCommand( CharmCommand* ) override; void rollbackCommand ( CharmCommand* ) override; signals: void eventAdded( const Event& event ); void eventModified( const Event& event ); void eventDeleted( const Event& event ); void allEvents( const EventList& ); void definedTasks( const TaskList& ); void taskAdded( const Task& ); void taskUpdated( const Task& ); void taskDeleted( const Task& ); void readyToQuit(); void currentBackendStatus( const QString& text ); void commandCompleted( CharmCommand* ); private: void updateSubscriptionForTask( const Task& ); template void loadConfigValue( const QString &key, T &configValue ) const; StorageInterface* m_storage; }; #endif Charm-1.10.0/Core/ControllerInterface.h000066400000000000000000000107571260343353100176420ustar00rootroot00000000000000/* ControllerInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 CONTROLLERINTERFACE_H #define CONTROLLERINTERFACE_H #include #include "Task.h" #include "Event.h" #include "State.h" class CharmCommand; class Configuration; class StorageInterface; class ControllerInterface { public: virtual ~ControllerInterface() {} // keep compiler happy // application: // react on application state changes virtual void stateChanged( State previous, State next ) = 0; // persist meta data portions of Configuration virtual void persistMetaData( Configuration& ) = 0; // load meta data and store appropriate portions in configuration virtual void provideMetaData( Configuration& ) = 0; /** Add an event. Return a valid event if successful. */ virtual Event makeEvent( const Task& ) = 0; /** Add an event, copying data from another event. */ virtual Event cloneEvent( const Event& ) = 0; /** Modify an event. */ virtual bool modifyEvent( const Event& ) = 0; /** Delete an event. */ virtual bool deleteEvent( const Event& ) = 0; /** Add a task, and send the result to the view as a signal. */ virtual bool addTask( const Task& task ) = 0; /** Modify the task, the user has changed it in the view. */ virtual bool modifyTask( const Task& ) = 0; /** Delete the task. Send a signal to the view confirming it. */ virtual bool deleteTask( const Task& ) = 0; /** Set all tasks. Updates the view, after. */ virtual bool setAllTasks( const TaskList& tasks ) = 0; /** Receive a command from the view. */ virtual void executeCommand( CharmCommand* ) = 0; /** Receive an undo command from the view. */ virtual void rollbackCommand( CharmCommand* ) = 0; /** Export the database contents into a XML document. */ virtual QDomDocument exportDatabasetoXml() const = 0 ; /** 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. */ virtual QString importDatabaseFromXml( const QDomDocument& ) = 0; // supposed to be implemented as signals: /** Added an event. */ virtual void eventAdded( const Event& event ) = 0; /** Modified an event. */ virtual void eventModified( const Event& event ) = 0; /** Deleted an event. */ virtual void eventDeleted( const Event& event ) = 0; /** This sends out the current task list. */ virtual void definedTasks( const TaskList& ) = 0; /** Add a task. */ virtual void taskAdded( const Task& ) = 0; /** Update a task in the view. */ virtual void taskUpdated( const Task& ) = 0; /** Delete a task from the view completely. */ virtual void taskDeleted( const Task& ) = 0; /** A command has been completed from the controller's point of view. */ virtual void commandCompleted( CharmCommand* ) = 0; /** 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. */ virtual void readyToQuit() = 0; /** The currently used backend. */ virtual StorageInterface* storage() = 0; /** Create the backend. */ virtual bool initializeBackEnd( const QString& name ) = 0; /** Connect to the backend (make it available). */ virtual bool connectToBackend() = 0; /** Disconnect from the backend (shut it down). */ virtual bool disconnectFromBackend() = 0; }; #endif Charm-1.10.0/Core/Dates.cpp000066400000000000000000000047371260343353100152720ustar00rootroot00000000000000/* Dates.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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.10.0/Core/Dates.h000066400000000000000000000025401260343353100147250ustar00rootroot00000000000000/* Dates.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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.10.0/Core/Event.cpp000066400000000000000000000164251260343353100153100ustar00rootroot00000000000000/* Event.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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() : m_userid() , m_reportid() , m_installationId() , m_id() , m_taskId() { } bool Event::operator == ( const Event& other ) const { return ( other.id() == id() && other.installationId() == installationId() && 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; } void Event::setInstallationId( int instId ) { m_installationId = instId; } int Event::installationId() const { return m_installationId; } bool Event::isValid() const { // negative values are allowed and indicate calculated values return id() != 0 && m_installationId != 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( "event" ); const QString EventIdAttribute( "eventid" ); const QString EventInstallationIdAttribute( "installationid" ); const QString EventTaskIdAttribute( "taskid" ); const QString EventUserIdAttribute( "userid" ); const QString EventReportIdAttribute( "reportid" ); const QString EventStartAttribute( "start" ); const QString EventEndAttribute( "end" ); QDomElement Event::toXml( QDomDocument document ) const { QDomElement element = document.createElement( EventElement ); element.setAttribute( EventIdAttribute, QString().setNum( id() ) ); element.setAttribute( EventInstallationIdAttribute, QString().setNum( installationId() ) ); 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( QString::fromLatin1( "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.setInstallationId( element.attribute( EventInstallationIdAttribute ).toInt( &ok ) ); if ( !ok ) throw XmlSerializationException( QObject::tr( "Event::fromXml: invalid installation 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.10.0/Core/Event.h000066400000000000000000000061561260343353100147550ustar00rootroot00000000000000/* Event.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 ); void setInstallationId( int instId ); int installationId() const; 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; /** The installation-unique id of the event. */ int m_installationId; 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.10.0/Core/EventModelInterface.h000066400000000000000000000022511260343353100175470ustar00rootroot00000000000000/* EventModelInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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.10.0/Core/Installation.h000066400000000000000000000030051260343353100163230ustar00rootroot00000000000000/* Installation.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 INSTALLATION_H #define INSTALLATION_H #include class Installation { public: Installation() : m_id(), m_userId() {} bool isValid() const { return m_id != 0; } int id() const { return m_id; } void setId( int newid ) { m_id = newid; } int userId() const { return m_userId; } void setUserId( int userId ) { m_userId = userId; } QString name() const { return m_name; } void setName( const QString& newname ) { m_name = newname; } private: int m_id; int m_userId; QString m_name; }; #endif Charm-1.10.0/Core/MySqlStorage.cpp000066400000000000000000000155021260343353100166140ustar00rootroot00000000000000/* MySqlStorage.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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[] = { "MetaData", "Installations", "Tasks", "Events", "Subscriptions", "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[] = { { "id", "INTEGER AUTO_INCREMENT PRIMARY KEY" }, { "key", "VARCHAR( 128 ) NOT NULL" }, { "value", "VARCHAR( 128 )" }, LastField }; static const Fields Installations_Fields[] = { { "id", "INTEGER AUTO_INCREMENT PRIMARY KEY" }, { "inst_id", "INTEGER" }, { "user_id", "INTEGER" }, { "name", "varchar(256)" }, LastField }; static const Fields Tasks_Fields[] = { { "id", "INTEGER AUTO_INCREMENT PRIMARY KEY" }, { "task_id", "INTEGER UNIQUE" }, { "parent", "INTEGER" }, { "validfrom", "timestamp" }, { "validuntil", "timestamp" }, { "trackable", "INTEGER" }, { "name", "varchar(256)" }, LastField }; static const Fields Event_Fields[] = { { "id", "INTEGER AUTO_INCREMENT PRIMARY KEY" }, { "user_id", "INTEGER" }, { "event_id", "INTEGER" }, { "installation_id", "INTEGER" }, { "report_id", "INTEGER NULL" }, { "task", "INTEGER" }, { "comment", "varchar(256)" }, { "start", "timestamp" }, { "end", "timestamp" }, LastField }; static const Fields Subscriptions_Fields[] = { { "id", "INTEGER AUTO_INCREMENT PRIMARY KEY" }, { "user_id", "INTEGER" }, { "task", "INTEGER" }, LastField }; static const Fields Users_Fields[] = { { "id", "INTEGER AUTO_INCREMENT PRIMARY KEY" }, { "user_id", "INTEGER UNIQUE" }, { "name", "varchar(256)" }, LastField }; static const Fields* Database_Fields[NumberOfTables] = { MetaData_Fields, Installations_Fields, Tasks_Fields, Event_Fields, Subscriptions_Fields, Users_Fields }; const char DatabaseName[] = "mysql.charm.kdab.com"; MySqlStorage::MySqlStorage() : SqlStorage(), m_database(QSqlDatabase::addDatabase("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 } int MySqlStorage::installationId() const { return -1; // not implemented } bool MySqlStorage::createDatabase(Configuration& conf) { 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( ';' ); 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& parameters ) { 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.10.0/Core/MySqlStorage.h000066400000000000000000000034121260343353100162560ustar00rootroot00000000000000/* MySqlStorage.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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( "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; int installationId() const 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.10.0/Core/SmartNameCache.cpp000066400000000000000000000114271260343353100170370ustar00rootroot00000000000000/* SmartNameCache.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2015 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 TaskList::Iterator 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 TaskList::ConstIterator 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 QVector& 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.10.0/Core/SmartNameCache.h000066400000000000000000000026731260343353100165070ustar00rootroot00000000000000/* SmartNameCache.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2015 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.10.0/Core/SqLiteStorage.cpp000066400000000000000000000226551260343353100167570ustar00rootroot00000000000000/* SqLiteStorage.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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[] = { "MetaData", "Installations", "Tasks", "Events", "Subscriptions", "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[] = { { "id", "INTEGER PRIMARY KEY" }, { "key", "VARCHAR( 128 ) NOT NULL" }, { "value", "VARCHAR( 128 )" }, LastField }; static const Fields Installations_Fields[] = { { "id", "INTEGER PRIMARY KEY" }, { "inst_id", "INTEGER" }, { "user_id", "INTEGER" }, { "name", "varchar(256)" }, LastField }; static const Fields Tasks_Fields[] = { { "id", "INTEGER PRIMARY KEY" }, { "task_id", "INTEGER UNIQUE" }, { "parent", "INTEGER" }, { "validfrom", "timestamp" }, { "validuntil", "timestamp" }, { "trackable", "INTEGER" }, { "name", "varchar(256)" }, LastField }; static const Fields Event_Fields[] = { { "id", "INTEGER PRIMARY KEY" }, { "user_id", "INTEGER" }, { "event_id", "INTEGER" }, { "installation_id", "INTEGER" }, { "report_id", "INTEGER NULL" }, { "task", "INTEGER" }, { "comment", "varchar(256)" }, { "start", "date" }, { "end", "date" }, LastField }; static const Fields Subscriptions_Fields[] = { { "id", "INTEGER PRIMARY KEY" }, { "user_id", "INTEGER" }, { "task", "INTEGER" }, LastField }; static const Fields Users_Fields[] = { { "id", "INTEGER PRIMARY KEY" }, { "user_id", "INTEGER UNIQUE" }, { "name", "varchar(256)" }, LastField }; static const Fields* Database_Fields[NumberOfTables] = { MetaData_Fields, Installations_Fields, Tasks_Fields, Event_Fields, Subscriptions_Fields, Users_Fields }; const char DatabaseName[] = "charm.kdab.com"; const char DriverName[] = "QSQLITE"; SqLiteStorage::SqLiteStorage() : SqlStorage() , m_database( QSqlDatabase::addDatabase( DriverName, DatabaseName ) ) , m_installationId( 0 ) { if ( ! QSqlDatabase::isDriverAvailable( DriverName ) ) { throw CharmException( QObject::tr( "QSQLITE driver not available" ) ); } } SqLiteStorage::~SqLiteStorage() { } QString SqLiteStorage::lastInsertRowFunction() const { return QString::fromLocal8Bit("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: m_installationId = configuration.installationId; 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; } // connect: // qDebug() << "SqLiteStorage::connect: creating or opening local sqlite database at " // << fileInfo.absoluteFilePath(); const QDir oldDatabaseDirectory( QDir::homePath() + QDir::separator() + ".Charm" ); if ( oldDatabaseDirectory.exists() ) migrateDatabaseDirectory( oldDatabaseDirectory, fileInfo.dir() ); m_database.setHostName( "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; } // qDebug() << "SqLiteStorage::connect: SUCCESS - connected to database"; if ( ! verifyDatabase() ) { // qDebug() << "SqLiteStorage::connect: empty database, filling in the blanks"; 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 ); // qDebug() << "SqLiteStorage::connect: found user" << user.name() // << "for id" << userid << ( user.isValid() ? "(valid)" : "(invalid)"); 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 } int SqLiteStorage::installationId() const { return m_installationId; } 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; } // make an installation: // FIXME make a useful name for it QString installationName = "Unnamed Installation"; Installation installation = createInstallation( installationName ); if ( ! installation.isValid() ) { qDebug() << "SqLiteStorage::createDatabase: cannot create default installation id"; return false; } else { configuration.installationId = installation.id(); } return true; } Charm-1.10.0/Core/SqLiteStorage.h000066400000000000000000000031531260343353100164140ustar00rootroot00000000000000/* SqLiteStorage.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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; int installationId() const override; protected: bool createDatabase( Configuration& ) override; bool createDatabaseTables() override; bool migrateDatabaseDirectory(QDir, const QDir & ) const; QString lastInsertRowFunction() const override; private: QSqlDatabase m_database; int m_installationId; }; #endif Charm-1.10.0/Core/SqlRaiiTransactor.cpp000066400000000000000000000041041260343353100176230ustar00rootroot00000000000000/* SqlRaiiTransactor.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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_active( false ), 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.10.0/Core/SqlRaiiTransactor.h000066400000000000000000000022421260343353100172710ustar00rootroot00000000000000/* SqlRaiiTransactor.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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; QSqlDatabase& m_database; }; #endif Charm-1.10.0/Core/SqlStorage.cpp000066400000000000000000000644761260343353100163240ustar00rootroot00000000000000/* SqlStorage.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 // SqlStorage class SqlStorage::SqlStorage() : StorageInterface() { } 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 ) { SqlRaiiTransactor transactor( database() ); QSqlQuery query( database() ); query.prepare( QLatin1String("ALTER TABLE Tasks ADD trackable INTEGER") ); if ( !runQuery( query ) ) throw UnsupportedDatabaseVersionException( QObject::tr("Could not upgrade database from version %1 to version %2: %3").arg( QString::number( CHARM_DATABASE_VERSION_BEFORE_TRACKABLE ), QString::number( CHARM_DATABASE_VERSION ), query.lastError().text() ) ); version = CHARM_DATABASE_VERSION; setMetaData( CHARM_DATABASE_VERSION_DESCRIPTOR, QString::number ( version ), transactor ); transactor.commit(); return true; } throw UnsupportedDatabaseVersionException( QObject::tr( "Database version is not supported." ) ); return true; } TaskList SqlStorage::getAllTasks() { TaskList tasks; QSqlQuery query(database()); const char statement[] = "select * from Tasks left join Subscriptions " "on Tasks.task_id = Subscriptions.task;"; query.prepare(statement); // 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("INSERT into Tasks (task_id, name, parent, validfrom, validuntil, trackable) " "values ( :task_id, :name, :parent, :validfrom, :validuntil, :trackable);"); query.bindValue(":task_id", task.id()); query.bindValue(":name", task.name()); query.bindValue(":parent", task.parent()); query.bindValue(":validfrom", task.validFrom() ); query.bindValue(":validuntil", task.validUntil() ); query.bindValue(":trackable", task.trackable() ? 1 : 0 ); return runQuery(query); } Task SqlStorage::getTask( int taskid ) { QSqlQuery query(database()); const char statement[] = "SELECT * FROM Tasks LEFT JOIN Subscriptions ON Tasks.task_id = Subscriptions.task WHERE task_id = :id;"; query.prepare(statement); query.bindValue(":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("UPDATE Tasks set name = :name, parent = :parent, " "validfrom = :validfrom, validuntil = :validuntil, trackable = :trackable " "where task_id = :task_id;"); query.bindValue(":task_id", task.id()); query.bindValue(":name", task.name()); query.bindValue(":parent", task.parent()); query.bindValue(":validfrom", task.validFrom() ); query.bindValue(":validuntil", task.validUntil() ); query.bindValue(":trackable", task.trackable() ? 1 : 0 ); return runQuery(query); } bool SqlStorage::deleteTask(const Task& task) { SqlRaiiTransactor transactor(database()); QSqlQuery query(database()); query.prepare("DELETE from Tasks where task_id = :task_id;"); query.bindValue(":task_id", task.id()); bool rc = runQuery(query); QSqlQuery query2( database() ); query2.prepare( "DELETE from Events where task = :task_id;" ); query2.bindValue( ":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("DELETE from Tasks;"); return runQuery(query); } Event SqlStorage::makeEventFromRecord(const QSqlRecord& record) { Event event; int idField = record.indexOf("event_id"); int instIdField = record.indexOf("installation_id"); int userIdField = record.indexOf("user_id"); int reportIdField = record.indexOf("report_id"); int taskField = record.indexOf("task"); int commentField = record.indexOf("comment"); int startField = record.indexOf("start"); int endField = record.indexOf("end"); event.setId(record.field( idField ).value().toInt() ); event.setUserId( record.field( userIdField ).value().toInt() ); event.setReportId( record.field( reportIdField ).value().toInt() ); event.setInstallationId( record.field ( instIdField ).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().value() ); } if ( !record.field( endField ).isNull() ) { event.setEndDateTime ( record.field( endField ).value().value() ); } return event; } EventList SqlStorage::getAllEvents() { EventList events; QSqlQuery query(database()); query.prepare("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 const char* statement = "INSERT into Events values " "( NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL );"; QSqlQuery query(database()); query.prepare(statement); 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("id"); event.setId(query.value(indexField).toInt()); event.setInstallationId(installationId()); 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: const char* statement = "UPDATE Events SET event_id = :event_id, " "installation_id = :installation_id, report_id = :report_id WHERE id = :id;"; QSqlQuery query(database()); query.prepare(statement); query.bindValue(":event_id", event.id()); query.bindValue(":installation_id", event.installationId()); query.bindValue(":report_id", event.reportId()); query.bindValue(":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()); const char statement[] = "SELECT * FROM Events WHERE event_id = :id;"; query.prepare(statement); query.bindValue(":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("UPDATE Events set task = :task, comment = :comment, " "start = :start, end = :end, user_id = :user, report_id = :report " "where event_id = :id;"); query.bindValue(":id", event.id()); query.bindValue(":user", event.userId()); query.bindValue(":task", event.taskId()); query.bindValue(":report", event.reportId()); query.bindValue(":comment", event.comment()); query.bindValue(":start", event.startDateTime()); query.bindValue(":end", event.endDateTime() ); return runQuery( query ); } bool SqlStorage::deleteEvent(const Event& event) { QSqlQuery query(database()); query.prepare("DELETE from Events where event_id = :id;"); query.bindValue(":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("DELETE from Events;"); return runQuery(query); } #define MARKER "============================================================" bool SqlStorage::runQuery(QSqlQuery& query) { static const bool DoChitChat = false; if (DoChitChat) qDebug() << MARKER << endl << "SqlStorage::runQuery: executing query:" << endl << query.executedQuery(); bool result = query.exec(); if ( DoChitChat ) { 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; } void SqlStorage::stateChanged(State previous) { Q_UNUSED(previous); // atm, SqlStorage does not care about state // qDebug() << "SqlStorage::stateChanged: NOT IMPLEMENTED" } User SqlStorage::getUser(int userid) { User user; QSqlQuery query(database()); const char* statement = "SELECT * from Users WHERE user_id = :user_id;"; query.prepare(statement); query.bindValue(":user_id", userid); if (runQuery(query)) { if (query.next()) { int userIdPosition = query.record().indexOf("user_id"); int namePosition = query.record().indexOf("name"); Q_ASSERT(userIdPosition != -1 && namePosition != -1); user.setId(query.value(userIdPosition).toInt()); user.setName(query.value(namePosition).toString()); Q_ASSERT(user.isValid()); } else { qDebug() << "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()); const char* statement = "INSERT into Users ( id, user_id, name ) VALUES (NULL, NULL, :name);"; query.prepare(statement); query.bindValue(":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("id"); user.setId(query.value(idField).toInt()); Q_ASSERT(user.id() != 0); } else { qDebug() << "SqlStorage::makeUser: FAILED to find newly created user"; return user; } } if (result) { // make a unique user id: QSqlQuery query(database()); const char* statement = "UPDATE Users SET user_id = :id WHERE id = :idx;"; query.prepare(statement); query.bindValue(":id", user.id()); query.bindValue(":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()); const char statement[] = "UPDATE Users SET name = :name WHERE user_id = :id;"; query.prepare(statement); query.bindValue(":name", user.name()); query.bindValue(":id", user.id()); return runQuery(query); } bool SqlStorage::deleteUser(const User& user) { QSqlQuery query(database()); const char statement[] = "DELETE from Users WHERE user_id = :id;"; query.prepare(statement); query.bindValue(":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()); const char statement[] = "INSERT into Subscriptions VALUES (NULL, :user_id, :task);"; query.prepare(statement); query.bindValue(":user_id", user.id()); query.bindValue(":task", task.id()); return runQuery(query); } else { return true; } } bool SqlStorage::deleteSubscription(User user, Task task) { QSqlQuery query(database()); const char statement[] = "DELETE from Subscriptions WHERE user_id = :user_id AND task = :task;"; query.prepare(statement); query.bindValue(":user_id", user.id()); query.bindValue(":task", task.id()); return runQuery(query); } Installation SqlStorage::createInstallation(const QString& name) { SqlRaiiTransactor transactor(database()); bool result; Installation installation; { // insert a new record in the database const char* statement = "INSERT into Installations values ( NULL, NULL, NULL, :name );"; QSqlQuery query(database()); query.prepare(statement); query.bindValue(":name", name); result = runQuery(query); Q_ASSERT(result); Q_UNUSED(result); // this has to succeed } if (result) { // retrieve the AUTOINCREMENT id value of it const QString statement = QString::fromLocal8Bit( "SELECT * from Installations WHERE id = %1();") .arg( lastInsertRowFunction()); QSqlQuery query(database()); query.prepare(statement); result = runQuery(query); if (result && query.next()) { int indexField = query.record().indexOf("id"); int nameField = query.record().indexOf("name"); installation.setId(query.value(indexField).toInt()); installation.setName(query.value(nameField).toString()); Q_ASSERT(installation.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: const char* statement = "UPDATE Installations SET inst_id = :inst_id WHERE id = :id;"; QSqlQuery query(database()); query.prepare(statement); query.bindValue(":inst_id", installation.id()); query.bindValue(":id", installation.id()); result = runQuery(query); Q_ASSERT_X(result, Q_FUNC_INFO, "database implementation error (UPDATE)"); Q_UNUSED(result); } if (result) transactor.commit(); return installation; } Installation SqlStorage::getInstallation(int installationId) { QSqlQuery query(database()); const char statement[] = "SELECT * FROM Installations WHERE inst_id = :id;"; query.prepare(statement); query.bindValue(":id", installationId); if (runQuery(query) && query.next()) { Installation installation; int idField = query.record().indexOf("inst_id"); int nameField = query.record().indexOf("name"); int userIdField = query.record().indexOf("user_id"); installation.setId(query.value(idField).toInt()); installation.setName(query.value(nameField).toString()); installation.setUserId(query.value(userIdField).toInt()); Q_ASSERT(installation.isValid()); return installation; } else { return Installation(); } } bool SqlStorage::modifyInstallation(const Installation& installation) { QSqlQuery query(database()); const char statement[] = "UPDATE Installations SET name = :name, user_id = :user WHERE inst_id = :id;"; query.prepare(statement); query.bindValue(":name", installation.name()); query.bindValue(":user", installation.userId()); query.bindValue(":id", installation.id()); return runQuery(query); } bool SqlStorage::deleteInstallation(const Installation& installation) { QSqlQuery query(database()); const char statement[] = "DELETE from Installations WHERE inst_id = :id;"; query.prepare(statement); query.bindValue(":id", installation.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()); const char statement[] = "SELECT * FROM MetaData WHERE MetaData.key = :key;"; query.prepare(statement); query.bindValue(":key", key); if (runQuery(query) && query.next()) { result = true; } else { result = false; } } if (result) { // key exists, let's update: QSqlQuery query(database()); const char statement[] = "UPDATE MetaData SET value = :value WHERE key = :key;"; query.prepare(statement); query.bindValue(":value", value); query.bindValue(":key", key); return runQuery(query); } else { // key does not exist, let's insert: QSqlQuery query(database()); const char statement[] = "INSERT INTO MetaData VALUES ( NULL, :key, :value );"; query.prepare(statement); query.bindValue(":key", key); query.bindValue(":value", value); return runQuery(query); } return false; // never reached } QString SqlStorage::getMetaData(const QString& key) { QSqlQuery query(database()); const char statement[] = "SELECT * FROM MetaData WHERE key = :key;"; query.prepare(statement); query.bindValue(":key", key); if (runQuery(query) && query.next()) { int valueField = query.record().indexOf("value"); return query.value(valueField).toString(); } else { return QString::null; } } Task SqlStorage::makeTaskFromRecord( const QSqlRecord& record ) { Task task; int idField = record.indexOf("task_id"); int nameField = record.indexOf("name"); int parentField = record.indexOf("parent"); int useridField = record.indexOf("user_id"); int validfromField = record.indexOf("validfrom"); int validuntilField = record.indexOf("validuntil"); int trackableField = record.indexOf("trackable"); 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().value() ); } QString until = record.field(validuntilField).value().toString(); if ( ! until.isEmpty() ) { task.setValidUntil ( record.field( validuntilField ).value().value() ); } const QVariant trackableValue = record.field( trackableField ).value(); if ( !trackableValue.isNull() && trackableValue.isValid() ) { task.setTrackable( trackableValue.toInt() == 1 ); } 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( 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( 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.10.0/Core/SqlStorage.h000066400000000000000000000071721260343353100157570ustar00rootroot00000000000000/* SqlStorage.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 "StorageInterface.h" class QSqlDatabase; class QSqlQuery; class QSqlRecord; class SqlStorage : public StorageInterface { public: SqlStorage(); ~SqlStorage(); void stateChanged( State previous ) override; virtual QSqlDatabase& database() = 0; // installation id handling Installation createInstallation( const QString& name ) override; Installation getInstallation( int installationId ) override; bool modifyInstallation( const Installation& ) override; bool deleteInstallation( const Installation& ) override; // implement user database functions: User getUser ( int userid ) override; User makeUser( const QString& name ) override; bool modifyUser ( const User& user ) override; bool deleteUser ( const User& user ) override; // implement task database functions: TaskList getAllTasks() override; bool setAllTasks( const User& user, const TaskList& tasks ) override; bool addTask( const Task& task ) override; bool addTask( const Task& task, const SqlRaiiTransactor& ) override; Task getTask( int taskid ) override; bool modifyTask( const Task& task ) override; bool deleteTask( const Task& task ) override; bool deleteAllTasks() override; bool deleteAllTasks( const SqlRaiiTransactor& ) override; // implement event database functions: EventList getAllEvents() override; Event makeEvent() override; Event makeEvent( const SqlRaiiTransactor& ) override; Event getEvent( int eventid ) override; bool modifyEvent( const Event& event ) override; bool modifyEvent( const Event& event, const SqlRaiiTransactor& ) override; bool deleteEvent( const Event& event ) override; bool deleteAllEvents() override; bool deleteAllEvents( const SqlRaiiTransactor& ) override; // implement subscription management functions: bool addSubscription( User, Task ) override; bool deleteSubscription( User, Task ) override; // implement metadata management functions: bool setMetaData( const QString&, const QString& ) override; bool setMetaData( const QString&, const QString&, const SqlRaiiTransactor& ); QString getMetaData( const QString& ) override; // implement import functions: QString setAllTasksAndEvents( const User&, const TaskList&, const EventList& ) override; /** * @throws UnsupportedDatabaseVersionException */ bool verifyDatabase() override; virtual bool createDatabaseTables() = 0; // run the query and process possible errors static bool runQuery( QSqlQuery& ); protected: virtual QString lastInsertRowFunction() const = 0; private: Event makeEventFromRecord( const QSqlRecord& ); Task makeTaskFromRecord( const QSqlRecord& ); }; #endif Charm-1.10.0/Core/State.cpp000066400000000000000000000020571260343353100153030ustar00rootroot00000000000000/* State.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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.10.0/Core/State.h000066400000000000000000000022701260343353100147450ustar00rootroot00000000000000/* State.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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.10.0/Core/StorageInterface.h000066400000000000000000000110711260343353100171110ustar00rootroot00000000000000/* StorageInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 STORAGEINTERFACE_H #define STORAGEINTERFACE_H #include #include "Task.h" #include "User.h" #include "State.h" #include "Event.h" #include "Installation.h" #include "CharmExceptions.h" class Event; class Configuration; class SqlRaiiTransactor; class StorageInterface { public: virtual ~StorageInterface() { } // a readable description for the user virtual QString description() const = 0; // application: virtual void stateChanged(State previous) = 0; // backend availability virtual bool connect(Configuration&) = 0; virtual bool disconnect() = 0; // installation id table: // get the id of this installation virtual int installationId() const = 0; // create an installation id virtual Installation getInstallation(int installationId) = 0; virtual Installation createInstallation(const QString& name) = 0; virtual bool modifyInstallation(const Installation&) = 0; virtual bool deleteInstallation(const Installation&) = 0; // user database functions: virtual User getUser(int userid) = 0; virtual User makeUser(const QString& name) = 0; virtual bool modifyUser(const User& user) = 0; virtual bool deleteUser(const User& user) = 0; // task database functions: virtual TaskList getAllTasks() = 0; virtual bool setAllTasks( const User& user, const TaskList& tasks ) = 0; virtual bool addTask(const Task& task) = 0; virtual bool addTask( const Task& task, const SqlRaiiTransactor& ) = 0; virtual Task getTask(int taskId) = 0; virtual bool modifyTask(const Task& task) = 0; virtual bool deleteTask(const Task& task) = 0; virtual bool deleteAllTasks() = 0; virtual bool deleteAllTasks( const SqlRaiiTransactor& ) = 0; // event database functions: virtual EventList getAllEvents() = 0; // all events are created by the storage interface virtual Event makeEvent() = 0; virtual Event makeEvent( const SqlRaiiTransactor& ) = 0; virtual Event getEvent(int eventId)= 0; virtual bool modifyEvent( const Event& event ) = 0; virtual bool modifyEvent( const Event& event, const SqlRaiiTransactor& ) = 0; virtual bool deleteEvent(const Event& event) = 0; virtual bool deleteAllEvents() = 0; virtual bool deleteAllEvents( const SqlRaiiTransactor& ) = 0; // subscription management functions // (subscriptions cannot be modified, they are just boolean flags) // (subscription status is retrieved with the tasks) virtual bool addSubscription(User, Task) = 0; virtual bool deleteSubscription(User, Task) = 0; // database metadata management functions virtual bool setMetaData(const QString& key, const QString& value) = 0; virtual QString getMetaData(const QString& key) = 0; /*! @brief update all tasks and events in a single-transaction during imports @return an empty String on success, an error message otherwise */ virtual QString setAllTasksAndEvents( const User&, const TaskList&, const EventList& ) = 0; 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 bool verifyDatabase() = 0; }; #endif Charm-1.10.0/Core/Task.cpp000066400000000000000000000256341260343353100151330ustar00rootroot00000000000000/* Task.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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() : m_id(0) , m_parent(0) , m_subscribed(false) , m_trackable(true) { } Task::Task(TaskId id, const QString& name, TaskId parent, bool subscribed) : m_id(id) , m_parent(parent) , m_name(name) , m_subscribed(subscribed) , m_trackable(true) { } bool Task::isValid() const { return id() != 0; } QString Task::tagName() { static const QString tag( QString::fromLatin1( "task" ) ); return tag; } QString Task::taskListTagName() { static const QString tag( QString::fromLatin1( "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("taskid"); const QString TaskParentId("parentid"); const QString TaskSubscribed("subscribed"); const QString TaskTrackable("trackable"); const QString TaskValidFrom("validfrom"); const QString TaskValidUntil("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, QLatin1String("1")).toInt(&ok) == 1); if (!ok) throw XmlSerializationException( QObject::tr( "Task::fromXml: invalid trackable settings") ); } 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(); } 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.10.0/Core/Task.h000066400000000000000000000062311260343353100145700ustar00rootroot00000000000000/* Task.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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; 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; int m_parent; QString m_name; bool m_subscribed; bool m_trackable; /** The timestamp from which the task is valid. */ QDateTime m_validFrom; /** The timestamp after which the task becomes invalid. */ QDateTime m_validUntil; }; Q_DECLARE_METATYPE( TaskIdList ) Q_DECLARE_METATYPE( TaskList ) Q_DECLARE_METATYPE( Task ) void dumpTaskList( const TaskList& tasks ); #endif Charm-1.10.0/Core/TaskListMerger.cpp000066400000000000000000000104421260343353100171200ustar00rootroot00000000000000/* TaskListMerger.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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() : m_resultsValid( false ) { } 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.10.0/Core/TaskListMerger.h000066400000000000000000000044101260343353100165630ustar00rootroot00000000000000/* TaskListMerger.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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; TaskList m_oldTasks; TaskList m_newTasks; mutable TaskList m_results; mutable TaskList m_addedTasks; mutable TaskList m_modifiedTasks; }; #endif Charm-1.10.0/Core/TaskModelInterface.h000066400000000000000000000031371260343353100173740ustar00rootroot00000000000000/* TaskModelInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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.10.0/Core/TaskTreeItem.cpp000066400000000000000000000073561260343353100165730ustar00rootroot00000000000000/* TaskTreeItem.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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() : m_parent( 0 ) { } 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.10.0/Core/TaskTreeItem.h000066400000000000000000000045711260343353100162340ustar00rootroot00000000000000/* TaskTreeItem.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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; ConstPointerList m_children; Task m_task; }; #endif Charm-1.10.0/Core/TimeSpans.cpp000066400000000000000000000132031260343353100161210ustar00rootroot00000000000000/* TimeSpans.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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(); } #include "moc_TimeSpans.cpp" Charm-1.10.0/Core/TimeSpans.h000066400000000000000000000062331260343353100155730ustar00rootroot00000000000000/* TimeSpans.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 ); signals: void dateChanged(); private slots: void slotTimeout(); private: QTimer m_timer; QDate m_today; }; #endif Charm-1.10.0/Core/User.h000066400000000000000000000030161260343353100146020ustar00rootroot00000000000000/* User.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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() : m_id( 0 ) {} 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; }; #endif Charm-1.10.0/Core/ViewInterface.h000066400000000000000000000033401260343353100164170ustar00rootroot00000000000000/* ViewInterface.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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_VIEWINTERFACE_H #define CHARM_VIEWINTERFACE_H #include "Task.h" #include "State.h" #include "Event.h" class CharmCommand; class ViewInterface { public: virtual ~ViewInterface() {} // keep compiler happy // application: virtual void stateChanged( State previous ) = 0; virtual void visibilityChanged( bool ) = 0; // implement as signal and emit from show and hide events virtual void configurationChanged() = 0; virtual void saveConfiguration() = 0; virtual void emitCommand( CharmCommand* ) = 0; virtual void emitCommandRollback( CharmCommand* ) = 0; virtual void sendCommand( CharmCommand* ) = 0; virtual void sendCommandRollback( CharmCommand* ) = 0; virtual void commitCommand( CharmCommand* ) = 0; // restore the view virtual void restore() = 0; // quit the application virtual void quit() = 0; }; #endif Charm-1.10.0/Core/XmlSerialization.cpp000066400000000000000000000150341260343353100175200ustar00rootroot00000000000000/* XmlSerialization.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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 "charmreport"; } QString reportTypeAttribute() { return "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( "metadata" ); root.appendChild( metadata ); QDomElement username = doc.createElement( "username" ); metadata.appendChild( username ); QDomText text = doc.createTextNode( Configuration::instance().user.name() ); username.appendChild( text ); QDomElement creationTime = doc.createElement( "creation-time" ); metadata.appendChild( creationTime ); QDomText time = doc.createTextNode( QDateTime::currentDateTime().toUTC().toString( Qt::ISODate ) ); creationTime.appendChild( time ); // FIXME installation id and stuff are probably necessary } QDomElement report = doc.createElement( "report" ); root.appendChild( report ); return doc; } QDomElement reportElement( const QDomDocument& document ) { QDomElement root = document.documentElement(); return root.firstChildElement( "report" ); } QDomElement metadataElement( const QDomDocument& document ) { QDomElement root = document.documentElement(); return root.firstChildElement( "metadata" ); } QDateTime creationTime( const QDomElement& metaDataElement ) { QDomElement creationTimeElement = metaDataElement.firstChildElement( "creation-time" ); if ( ! creationTimeElement.isNull() ) { return QDateTime::fromString( creationTimeElement.text(), Qt::ISODate ); } else { return QDateTime(); } } QString userName( const QDomElement& metaDataElement ) { QDomElement usernameElement = metaDataElement.firstChildElement( "username" ); return usernameElement.text(); } } QString TaskExport::reportType() { return "taskdefinitions"; } void TaskExport::writeTo( const QString& filename, const TaskList& tasks ) { QDomDocument document = XmlSerialization::createXmlTemplate( reportType() ); QDomElement metadata = XmlSerialization::metadataElement( document ); 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.10.0/Core/XmlSerialization.h000066400000000000000000000035651260343353100171730ustar00rootroot00000000000000/* XmlSerialization.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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.10.0/License.txt000066400000000000000000000432541260343353100147560ustar00rootroot00000000000000 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.10.0/ReadMe.markdown000077700000000000000000000000001260343353100174152ReadMe.txtustar00rootroot00000000000000Charm-1.10.0/ReadMe.txt000066400000000000000000000056411260343353100145270ustar00rootroot00000000000000Charm - the Cross-Platform Time Tracker ====================================== 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.10.0/Tests/000077500000000000000000000000001260343353100137255ustar00rootroot00000000000000Charm-1.10.0/Tests/BackendIntegrationTests.cpp000066400000000000000000000133241260343353100212120ustar00rootroot00000000000000/* BackendIntegrationTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/StorageInterface.h" #include #include #include #include BackendIntegrationTests::BackendIntegrationTests() : TestApplication("./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() ); QVERIFY( controller()->storage()->getInstallation( testInstallationId() ).isValid() ); // model: QVERIFY( model()->taskTreeItem( 0 ).childCount() == 0 ); } void BackendIntegrationTests::simpleCreateModifyDeleteTaskTest() { Task task1( 1000, "Task 1" ); Task task1b( task1 ); task1b.setName( "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() + " - 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, "Task 1" ); Task task1_1( 1001, "Task 1-1", task1.id() ); Task task1_2( 1002, "Task 1-2", task1.id() ); Task task1_3( 1003, "Task 1-3", task1.id() ); Task task2( 2000, "Task 2" ); Task task2_1( 2100, "Task 2-1", task2.id() ); Task task2_1_1( 2110, "Task 2-1-1", task2_1.id() ); Task task2_1_2( 2120, "Task 2-1-2", task2_1.id() ); Task task2_2( 2200, "Task 2-2", task2.id() ); Task task2_2_1( 2210, "Task 2-2-1", task2_2.id() ); Task task2_2_2( 2220, "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 ) #include "moc_BackendIntegrationTests.cpp" Charm-1.10.0/Tests/BackendIntegrationTests.h000066400000000000000000000027751260343353100206670ustar00rootroot00000000000000/* BackendIntegrationTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 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.10.0/Tests/CMakeLists.txt000066400000000000000000000101261260343353100164650ustar00rootroot00000000000000INCLUDE_DIRECTORIES( ${Charm_SOURCE_DIR} ) if(POLICY CMP0020) CMAKE_POLICY(SET CMP0020 NEW) endif() SET( TestApplication_SRCS TestApplication.cpp ) SET( TEST_LIBRARIES CharmCore ${QT_QTTEST_LIBRARY} ${QT_LIBRARIES} ) QT_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( 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.10.0/Tests/CharmDataModelTests.cpp000066400000000000000000000166231260343353100202710ustar00rootroot00000000000000/* CharmDataModelTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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() , m_referenceModel( nullptr ) { } void CharmDataModelTests::initTestCase () { // set up a model that the other tests can clone to use: m_referenceModel = new CharmDataModel; Task task1( 1000, "Task 1" ); Task task1_1( 1001, "Task 1-1", task1.id() ); Task task1_2( 1002, "Task 1-2", task1.id() ); Task task1_3( 1003, "Task 1-3", task1.id() ); Task task2( 2000, "Task 2" ); Task task2_1( 2100, "Task 2-1", task2.id() ); Task task2_1_1( 2110, "Task 2-1-1", task2_1.id() ); Task task2_1_2( 2120, "Task 2-1-2", task2_1.id() ); Task task2_2( 2200, "Task 2-2", task2.id() ); Task task2_2_1( 2210, "Task 2-2-1", task2_2.id() ); Task task2_2_2( 2220, "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, "Task 1" ); Task task1_1( 1001, "Task 1-1", task1.id() ); Task task1_2( 1002, "Task 1-2", task1.id() ); Task task1_3( 1003, "Task 1-3", task1.id() ); Task task2( 2000, "Task 2" ); Task task2_1( 2100, "Task 2-1", task2.id() ); Task task2_1_1( 2110, "Task 2-1-1", task2_1.id() ); Task task2_1_2( 2120, "Task 2-1-2", task2_1.id() ); Task task2_2( 2200, "Task 2-2", task2.id() ); Task task2_2_1( 2210, "Task 2-2-1", task2_2.id() ); Task task2_2_2( 2220, "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, "Task 1" ); Task task1_1( 1001, "Task 1-1", task1.id() ); Task task1_2( 1002, "Task 1-2", task1.id() ); Task task1_3( 1003, "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( "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 ) #include "moc_CharmDataModelTests.cpp" Charm-1.10.0/Tests/CharmDataModelTests.h000066400000000000000000000024171260343353100177320ustar00rootroot00000000000000/* CharmDataModelTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 slots: void initTestCase(); void createAndDestroyTest(); void addAndRemoveTasksTest(); void modifyTaskTest(); void cleanupTestCase(); private: CharmDataModel* m_referenceModel; }; #endif Charm-1.10.0/Tests/ControllerTests.cpp000066400000000000000000000244171260343353100176070ustar00rootroot00000000000000/* ControllerTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/StorageInterface.h" #include "Core/CharmConstants.h" #include "Core/Controller.h" #include #include #include #include ControllerTests::ControllerTests() : QObject() , m_controller( nullptr ) , m_configuration( Configuration::instance() ) , m_localPath( "./ControllerTestDatabase.db" ) , m_eventListReceived( false ) , m_taskListReceived( false ) { } 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() { // stick with user id 0, it is not persisted in the DB, and 0 is the default Configuration configs[] = { Configuration( true, true, User( "bla", 0 ), Configuration::TaskPrefilter_ShowAll, Configuration::TimeTrackerFont_Small, Configuration::Minutes, true, Qt::ToolButtonIconOnly, true, true, true, false ), Configuration( true, false, User( "blub", 0 ), Configuration::TaskPrefilter_CurrentOnly, Configuration::TimeTrackerFont_Regular, Configuration::Minutes, false, Qt::ToolButtonTextOnly, false, false, false, false ), Configuration( false, true, User(), Configuration::TaskPrefilter_SubscribedAndCurrentOnly, Configuration::TimeTrackerFont_Large, Configuration::Minutes, true, Qt::ToolButtonTextBesideIcon, true, true, true, false ), }; 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( "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( "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 ( "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( "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( "Event-1-Comment" ); e1.setStartDateTime(); m_controller->modifyEvent( e1 ); Event e2 = m_controller->storage()->makeEvent(); e2.setTaskId( tasks.last().id() ); e2.setComment( "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( Task task, tasksBefore ) { task.dump(); } Q_FOREACH( 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 ) #include "moc_ControllerTests.cpp" Charm-1.10.0/Tests/ControllerTests.h000066400000000000000000000036531260343353100172530ustar00rootroot00000000000000/* ControllerTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/ControllerInterface.h" class ControllerTests : public QObject { Q_OBJECT public: ControllerTests(); public 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 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: ControllerInterface* m_controller; Configuration& m_configuration; QString m_localPath; EventList m_currentEvents; bool m_eventListReceived; TaskList m_definedTasks; bool m_taskListReceived; }; #endif Charm-1.10.0/Tests/Data/000077500000000000000000000000001260343353100145765ustar00rootroot00000000000000Charm-1.10.0/Tests/Data/simple-tasklists.xml000066400000000000000000000023761260343353100206400ustar00rootroot00000000000000 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.10.0/Tests/Data/tasklist-merges.xml000066400000000000000000000204631260343353100204430ustar00rootroot00000000000000 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.10.0/Tests/Data/tasklist-treeness.xml000066400000000000000000000016241260343353100210070ustar00rootroot00000000000000 task 1 task 2 task 3 task 4 task 5 task 3 Charm-1.10.0/Tests/Data/test-database-export.charmdatabaseexport000066400000000000000000022164121260343353100246110ustar00rootroot00000000000000 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.10.0/Tests/Data/test-tasklistexport.xml000066400000000000000000000013211260343353100213720ustar00rootroot00000000000000 Mirko Boehm 2008-09-20T23:53:12 top first-child project project task 1 project task 2 project task 3 Charm-1.10.0/Tests/Data/test-timesheet-report.charmreport000066400000000000000000000012511260343353100233220ustar00rootroot00000000000000 2015-01-08T11:32:52Z 1.8.0-136-gbe24 2015 1 National holiday Charm-1.10.0/Tests/DatesTests.cpp000066400000000000000000000120301260343353100165100ustar00rootroot00000000000000/* DatesTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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 ) #include "moc_DatesTests.cpp" Charm-1.10.0/Tests/DatesTests.h000066400000000000000000000024021260343353100161570ustar00rootroot00000000000000/* DatesTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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 slots: void testDateByWeekNumberAndWorkDay(); void testWeekDayInWeekOf(); void testNumberOfWeeksInYear_data(); void testNumberOfWeeksInYear(); void testWeekDifference_data(); void testWeekDifference(); }; #endif Charm-1.10.0/Tests/ImportExportTests.cpp000066400000000000000000000100071260343353100201260ustar00rootroot00000000000000/* ImportExportTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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("./ImportExportTestDatabase.db") { } void ImportExportTests::initTestCase() { initialize(); } void ImportExportTests::importExportTest() { const QString localFileName( "ImportExportTests-temp.charmdatabaseexport" ); const QString filename = ":/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 = ":/importExportTest/Data/test-database-export.charmdatabaseexport"; QBENCHMARK { importDatabase( filename ); } } void ImportExportTests::exportBenchmark() { const QString filename = ":/importExportTest/Data/test-database-export.charmdatabaseexport"; const QString localFileName( "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 ) #include "moc_ImportExportTests.cpp" Charm-1.10.0/Tests/ImportExportTests.h000066400000000000000000000024051260343353100175760ustar00rootroot00000000000000/* ImportExportTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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 slots: void initTestCase(); void importExportTest(); void importBenchmark(); void exportBenchmark(); void cleanupTestCase(); private: void importDatabase( const QString& filename ); }; #endif Charm-1.10.0/Tests/SmartNameCacheTests.cpp000066400000000000000000000042501260343353100202700ustar00rootroot00000000000000/* SmartNameCacheTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2015 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, QLatin1String("Projects") ); Task charm( 2, QLatin1String("Charm") ); charm.setParent( projects.id() ); Task charmDevelopment( 3, QLatin1String("Development") ); charmDevelopment.setParent( charm.id() ); Task charmOverhead( 4, QLatin1String("Overhead") ); charmOverhead.setParent( charm.id() ); Task lotsofcake( 5, QLatin1String("Lotsofcake") ); lotsofcake.setParent( projects.id() ); Task lotsofcakeDevelopment( 6, QLatin1String("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 ) #include "moc_SmartNameCacheTests.cpp" Charm-1.10.0/Tests/SmartNameCacheTests.h000066400000000000000000000020571260343353100177400ustar00rootroot00000000000000/* SmartNameCacheTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2015 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 slots: void testCache(); }; #endif Charm-1.10.0/Tests/SqLiteStorageTests.cpp000066400000000000000000000277051260343353100202150ustar00rootroot00000000000000/* SqLiteStorageTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/Installation.h" #include "Core/SqLiteStorage.h" #include #include #include #include SqLiteStorageTests::SqLiteStorageTests() : QObject() , m_storage( new SqLiteStorage ) , m_localPath( "./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::makeModifyDeleteInstallationTest() { int userId = 42; // make two installation ids: QString name1 = "Installation-1"; Installation installation1 = m_storage->createInstallation( name1 ); installation1.setUserId( userId ); QVERIFY( installation1.isValid() ); QVERIFY( installation1.name() == name1 ); QString name2 = "Installation-2"; Installation installation2 = m_storage->createInstallation( name2 ); installation1.setUserId( userId ); QVERIFY( installation2.isValid() ); QVERIFY( installation2.name() == name2 ); // modify installation 1: QString newName = "1-Installation"; installation1.setName( newName ); QVERIFY( m_storage->modifyInstallation( installation1 ) ); // verify installation 1 database entry: Installation installation1_1 = m_storage->getInstallation( installation1.id() ); QVERIFY( installation1.id() == installation1_1.id() ); QVERIFY( installation1.userId() == installation1_1.userId() ); QVERIFY( installation1_1.name() == newName ); // delete installation 1 QVERIFY( m_storage->deleteInstallation( installation1 ) ); // verify installation 1 is gone installation1 = m_storage->getInstallation( installation1.id() ); QVERIFY( ! installation1.isValid() ); // verify installation 2 is still there installation2 = m_storage->getInstallation( installation2.id() ); QVERIFY( installation2.isValid() ); } void SqLiteStorageTests::makeModifyDeleteUserTest() { // make two user accounts QString name1 = "Test-User-1"; User user1 = m_storage->makeUser( name1 ); QVERIFY( user1.name() == name1 ); QString name2 = "Test-User-2"; User user2 = m_storage->makeUser( name2 ); QVERIFY( user2.name() == name2 ); // modify the user QString newName = "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( "Task-1-Name" ); const int Task2Id = 2; const QString Task2Name( "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( "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( "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( "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( "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( "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( "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( "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( "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( "Key1" ); const QString Key2( "Key2" ); const QString Value1( "Value1" ); const QString Value2( "Value2" ); const QString Value1_1( "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 ) #include "moc_SqLiteStorageTests.cpp" Charm-1.10.0/Tests/SqLiteStorageTests.h000066400000000000000000000031451260343353100176520ustar00rootroot00000000000000/* SqLiteStorageTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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/StorageInterface.h" class SqLiteStorageTests : public QObject { Q_OBJECT public: SqLiteStorageTests(); ~SqLiteStorageTests(); private: StorageInterface* m_storage; Configuration m_configuration; QString m_localPath; private slots: void initTestCase (); void connectAndCreateDatabaseTest(); void makeModifyDeleteInstallationTest(); void makeModifyDeleteUserTest(); void makeModifyDeleteTasksTest(); void makeModifyDeleteEventsTest(); void addDeleteSubscriptionsTest(); void setGetMetaDataTest(); void deleteTaskWithEventsTest(); void cleanupTestCase(); }; #endif Charm-1.10.0/Tests/SqlTransactionTests.cpp000066400000000000000000000121251260343353100204220ustar00rootroot00000000000000/* SqlTransactionTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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 char DriverName[] = "QMYSQL"; QVERIFY( QSqlDatabase::isDriverAvailable( DriverName ) ); QSqlDatabase db = QSqlDatabase::addDatabase( DriverName, "test-mysql.charm.kdab.com" ); } void SqlTransactionTests::testSqLiteDriverRequirements() { const char DriverName[] = "QSQLITE"; QVERIFY( QSqlDatabase::isDriverAvailable( DriverName ) ); QSqlDatabase db = QSqlDatabase::addDatabase( DriverName, "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 ) #include "moc_SqlTransactionTests.cpp" Charm-1.10.0/Tests/SqlTransactionTests.h000066400000000000000000000025671260343353100201000ustar00rootroot00000000000000/* SqlTransactionTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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 slots: void testMySqlDriverRequirements(); void testSqLiteDriverRequirements(); #if 0 void testMySqlTransactionRollback(); void testMySqlTransactionCommit(); void testMySqlNestedTransactions(); #endif private: MySqlStorage prepareMySqlStorage(); }; #endif // SQLTRANSACTIONTESTS_H Charm-1.10.0/Tests/TaskStructureTests.cpp000066400000000000000000000126761260343353100203130ustar00rootroot00000000000000/* TaskStructureTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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( QDomElement testcase, TestHelpers::retrieveTestCases( ":/checkForUniqueTaskIdsTest/Data", "checkForUniqueTaskIdsTest" ) ) { QString name = testcase.attribute( "name" ); bool expectedResult = TestHelpers::attribute( "expectedResult", testcase ); QDomElement element = testcase.firstChildElement( Task::taskListTagName() ); QVERIFY( !element.isNull() ); TaskList tasks = Task::readTasksElement( element, CHARM_DATABASE_VERSION ); QTest::newRow( name.toLocal8Bit() ) << 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( ":/checkForTreenessTest/Data", "checkForTreenessTest" ) ) { QString name = testcase.attribute( "name" ); bool expectedResult = TestHelpers::attribute( "expectedResult", testcase ); QDomElement element = testcase.firstChildElement( Task::taskListTagName() ); QVERIFY( !element.isNull() ); TaskList tasks = Task::readTasksElement( element, CHARM_DATABASE_VERSION ); QTest::newRow( name.toLocal8Bit() ) << 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( ":/mergeTaskListsTest/Data", "mergeTaskListsTest" ) ) { QString name = testcase.attribute( "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( "arg" ); TaskList tasks = Task::readTasksElement( element, CHARM_DATABASE_VERSION ); if ( arg == "old" ) { old = tasks; oldFound = true; } else if ( arg == "new" ) { newTasks = tasks; newFound = true; } else if ( arg == "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() ) << 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 ) #include "moc_TaskStructureTests.cpp" Charm-1.10.0/Tests/TaskStructureTests.h000066400000000000000000000024661260343353100177540ustar00rootroot00000000000000/* TaskStructureTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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 slots: void checkForUniqueTaskIdsTest_data(); void checkForUniqueTaskIdsTest(); void checkForTreenessTest_data(); void checkForTreenessTest(); void mergeTaskListsTest_data(); void mergeTaskListsTest(); }; #endif Charm-1.10.0/Tests/TestApplication.cpp000066400000000000000000000070231260343353100175360ustar00rootroot00000000000000/* TestApplication.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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_controller( nullptr ) , m_model( nullptr ) , 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; } #include "moc_TestApplication.cpp" Charm-1.10.0/Tests/TestApplication.h000066400000000000000000000031441260343353100172030ustar00rootroot00000000000000/* TestApplication.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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; CharmDataModel* m_model; Configuration* m_configuration; QString m_localPath; }; #endif // TESTAPPLICATION_H Charm-1.10.0/Tests/TestData.qrc000066400000000000000000000013051260343353100161440ustar00rootroot00000000000000 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.10.0/Tests/TestHelpers.h000066400000000000000000000054321260343353100163440ustar00rootroot00000000000000/* TestHelpers.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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( QString path, QString type ) { const QString tagName( "testcase" ); QStringList filenamePatterns; filenamePatterns << "*.xml"; QDir dataDir( path ); if ( !dataDir.exists() ) { throw CharmException( "path to test case data does not exist" ); } QFileInfoList dataSets = dataDir.entryInfoList( filenamePatterns, QDir::Files, QDir::Name ); QList result; Q_FOREACH( QFileInfo fileinfo, dataSets ) { QDomDocument doc( "charmtests" ); QFile file( fileinfo.filePath() ); if ( ! file.open( QIODevice::ReadOnly ) ) { throw CharmException( "unable to open input file" ); } if ( !doc.setContent( &file ) ) { throw CharmException( "invalid DOM document, cannot load" ); } QDomElement root = doc.firstChildElement(); if ( root.tagName() != "testcases" ) { throw CharmException( "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( "type" ) == type ) { result << child; } } } return result; } bool attribute( const QString& name, const QDomElement& element ) { QString text = element.attribute( "expectedResult" ); if ( text != "false" && text != "true" ) { throw CharmException( "attribute does not represent a boolean" ); } return ( text == "true" ); } } #endif Charm-1.10.0/Tests/TimeSheetProcessorTests.cpp000066400000000000000000000066601260343353100212530ustar00rootroot00000000000000/* TimeSheetProcessorTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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(":/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("SELECT id, date_time_uploaded from timesheets where filename=:file AND userid=:user"); query.bindValue( "file", m_reportPath ); query.bindValue( "user", m_adminId ); QVERIFY( storage.runQuery( query ) ); QVERIFY( query.next() ); QSqlRecord record = query.record(); m_idTimeSheet = record.value( record.indexOf("id") ).toInt(); uint dateTimeUploaded = record.value( record.indexOf("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("SELECT id, date_time_uploaded from timesheets where filename=:file AND userid=:user"); queryRemove.bindValue( "file", m_reportPath ); queryRemove.bindValue( "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.10.0/Tests/TimeSheetProcessorTests.h000066400000000000000000000023111260343353100207050ustar00rootroot00000000000000/* TimeSheetProcessorTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2015-2015 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 slots: void testAddRemoveTimeSheet(); private: int m_idTimeSheet; int m_adminId; QString m_reportPath; }; #endif Charm-1.10.0/Tests/TimeSpanTests.cpp000066400000000000000000000051521260343353100171770ustar00rootroot00000000000000/* TimeSpanTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2015 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 #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 ) #include "moc_TimeSpanTests.cpp" Charm-1.10.0/Tests/TimeSpanTests.h000066400000000000000000000020331260343353100166370ustar00rootroot00000000000000/* TimeSpanTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2012-2015 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 slots: void testTimeSpans(); }; #endif Charm-1.10.0/Tests/UpdateCheckerTests.cpp000066400000000000000000000045501260343353100201670ustar00rootroot00000000000000/* UpdateCheckerTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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( QLatin1String("0.1"), QLatin1String("0.2") ) ); QVERIFY( Charm::versionLessThan( QLatin1String("1.9.0"), QLatin1String("1.10.0") ) ); QVERIFY( Charm::versionLessThan( QLatin1String("0.1"), QLatin1String("0.1.1") ) ); QVERIFY( ! Charm::versionLessThan( QLatin1String("1.9.0"), QLatin1String("1.9.0") ) ); QVERIFY( ! Charm::versionLessThan( QLatin1String("1.10.0"), QLatin1String("1.9.0") ) ); QVERIFY( Charm::versionLessThan( QLatin1String("1.9.0"), QLatin1String("1.9.0.1") ) ); QVERIFY( ! Charm::versionLessThan( QLatin1String("1.9.0"), QLatin1String("1.9.abc") ) ); QVERIFY( ! Charm::versionLessThan( QLatin1String("2.0.1"), QLatin1String("1.20.0") ) ); QVERIFY( ! Charm::versionLessThan( QLatin1String("1.9.0.1"), QLatin1String("1.9.0.0.1") ) ); QVERIFY( Charm::versionLessThan( QString(), QLatin1String("0.2") ) ); QVERIFY( ! Charm::versionLessThan( QLatin1String("0.2"), QString() ) ); QVERIFY( ! Charm::versionLessThan( QString(), QString() ) ); QVERIFY( ! Charm::versionLessThan( QLatin1String(" "), QLatin1String(".") ) ); QVERIFY( ! Charm::versionLessThan( QLatin1String(" ."), QLatin1String("....") ) ); QVERIFY( ! Charm::versionLessThan( QLatin1String(".1."), QLatin1String(" ") ) ); } QTEST_MAIN( UpdateCheckerTests ) #include "moc_UpdateCheckerTests.cpp" Charm-1.10.0/Tests/UpdateCheckerTests.h000066400000000000000000000020671260343353100176350ustar00rootroot00000000000000/* UpdateCheckerTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2010-2015 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 slots: void testVersionComparison(); }; #endif Charm-1.10.0/Tests/XmlSerializationTests.cpp000066400000000000000000000150321260343353100207530ustar00rootroot00000000000000/* XmlSerializationTests.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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( "A task" ); task.setId( 42 ); task.setParent( 4711 ); task.setSubscribed( true ); task.setValidFrom( QDateTime::currentDateTime() ); Task task2; task2.setName( "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( "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( "testdocument" ); Q_FOREACH( const Event& event, eventsToTest ) { QDomElement element = event.toXml( document ); // // temp: // document.appendChild( element ); // qDebug() << document.toString( 4 ); // // ^^^ 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( "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( "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 ) { qDebug() << "Failure reading tasks:" << e.what(); QFAIL( "Read tasks are not equal to the written ones" ); } } void XmlSerializationTests::testTaskExportImport() { TaskExport importer; importer.readFrom( ":/testTaskExportImport/Data/test-tasklistexport.xml" ); QVERIFY( !importer.tasks().isEmpty() ); QVERIFY( importer.exportTime().isValid() ); } QTEST_MAIN( XmlSerializationTests ) #include "moc_XmlSerializationTests.cpp" Charm-1.10.0/Tests/XmlSerializationTests.h000066400000000000000000000024651260343353100204260ustar00rootroot00000000000000/* XmlSerializationTests.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2007-2015 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 slots: void testEventSerialization(); void testTaskSerialization(); void testTaskListSerialization(); void testQDateTimeToFromString(); void testTaskExportImport(); private: TaskList tasksToTest() const; }; #endif Charm-1.10.0/Tools/000077500000000000000000000000001260343353100137235ustar00rootroot00000000000000Charm-1.10.0/Tools/Anonymizer/000077500000000000000000000000001260343353100160565ustar00rootroot00000000000000Charm-1.10.0/Tools/Anonymizer/Anonymizer.cpp000066400000000000000000000023521260343353100207170ustar00rootroot00000000000000/* Anonymizer.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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.10.0/Tools/TimesheetGenerator/000077500000000000000000000000001260343353100175215ustar00rootroot00000000000000Charm-1.10.0/Tools/TimesheetGenerator/CMakeLists.txt000066400000000000000000000005161260343353100222630ustar00rootroot00000000000000INCLUDE_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.10.0/Tools/TimesheetGenerator/Exceptions.h000066400000000000000000000027521260343353100220210ustar00rootroot00000000000000/* Exceptions.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2009-2015 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.10.0/Tools/TimesheetGenerator/Options.cpp000066400000000000000000000057371260343353100216740ustar00rootroot00000000000000/* Options.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2009-2015 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, "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.10.0/Tools/TimesheetGenerator/Options.h000066400000000000000000000022431260343353100213260ustar00rootroot00000000000000/* Options.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2009-2015 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.10.0/Tools/TimesheetGenerator/example-weekly-template.xml000066400000000000000000000007331260343353100250100ustar00rootroot00000000000000 auto-generated entry 1 auto-generated entry 2 Charm-1.10.0/Tools/TimesheetGenerator/main.cpp000066400000000000000000000151661260343353100211620ustar00rootroot00000000000000/* main.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2009-2015 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( "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( "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( "year" ); metadata.appendChild( yearElement ); QDomText text = document.createTextNode( QString::number( year ) ); yearElement.appendChild( text ); QDomElement weekElement = document.createElement( "serial-number" ); weekElement.setAttribute( "semantics", "week-number" ); metadata.appendChild( weekElement ); QDomText weektext = document.createTextNode( QString::number( week ) ); weekElement.appendChild( weektext ); } { // effort // make effort element: QDomElement effort = document.createElement( "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.10.0/Tools/TimesheetProcessor/000077500000000000000000000000001260343353100175525ustar00rootroot00000000000000Charm-1.10.0/Tools/TimesheetProcessor/CMakeLists.txt000066400000000000000000000005641260343353100223170ustar00rootroot00000000000000INCLUDE_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.10.0/Tools/TimesheetProcessor/CommandLine.cpp000066400000000000000000000260071260343353100224510ustar00rootroot00000000000000/* CommandLine.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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( 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.10.0/Tools/TimesheetProcessor/CommandLine.h000066400000000000000000000035071260343353100221160ustar00rootroot00000000000000/* CommandLine.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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.10.0/Tools/TimesheetProcessor/Database.cpp000066400000000000000000000133441260343353100217670ustar00rootroot00000000000000/* Database.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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( "No such user" ); } } User Database::getOrCreateUserByName( QString name ) throw (TimesheetProcessorException ) { User user; QSqlQuery query( database() ); const char* statement = "SELECT user_id from Users WHERE name = :user_name;"; query.prepare( statement ); query.bindValue( ":user_name", name ); bool result = query.exec(); if ( result ) { if ( query.next() ) { int userIdPosition = query.record().indexOf( "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( "Cannot create the new user" ); } } } else { throw TimesheetProcessorException( "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( "The database is not empty. Only " "empty databases can be automatically initialized." ); } if ( !m_storage.createDatabaseTables() ) { throw TimesheetProcessorException( "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( "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( ":index", index ); query.bindValue( ":userid", userid ); bool result = query.exec(); if ( !result ) { throw TimesheetProcessorException( "Failed to delete report" ); } } Charm-1.10.0/Tools/TimesheetProcessor/Database.h000066400000000000000000000033301260343353100214260ustar00rootroot00000000000000/* Database.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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.10.0/Tools/TimesheetProcessor/Exceptions.h000066400000000000000000000027371260343353100220550ustar00rootroot00000000000000/* Exceptions.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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.10.0/Tools/TimesheetProcessor/Operations.cpp000066400000000000000000000244631260343353100224120ustar00rootroot00000000000000/* Operations.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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("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("charmreport"); QDomElement metadataElement = charmReportElement.firstChildElement("metadata"); QDomElement yearElement = metadataElement.firstChildElement("year"); QString year = yearElement.text().simplified(); QDomElement weekElement = metadataElement.firstChildElement("serial-number"); QString week = weekElement.text().simplified(); QDomElement reportElement = charmReportElement.firstChildElement("report"); QDomElement effortElement = reportElement.firstChildElement("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( "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( ":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( "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( "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( QString("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( QString("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( "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.10.0/Tools/TimesheetProcessor/Operations.h000066400000000000000000000023751260343353100220550ustar00rootroot00000000000000/* Operations.h This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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.10.0/Tools/TimesheetProcessor/main.cpp000066400000000000000000000043501260343353100212040ustar00rootroot00000000000000/* main.cpp This file is part of Charm, a task-based time tracking application. Copyright (C) 2008-2015 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.10.0/android/000077500000000000000000000000001260343353100142435ustar00rootroot00000000000000Charm-1.10.0/android/AndroidManifest.xml000066400000000000000000000072441260343353100200430ustar00rootroot00000000000000 Charm-1.10.0/charmtimetracker.dsc000066400000000000000000000005011260343353100166370ustar00rootroot00000000000000Format: 1.0 Source: charmtimetracker Version: 1.9.0 Binary: charmtimetracker Maintainer: Frank Osterfeld Architecture: any Build-Depends: debhelper (>= 4.1.16), cdbs, cmake, libqt4-dev, libxss-dev, libqt4-sql-sqlite Files: 00000000000000000000000000000000 00000 charmtimetracker-1.9.0.tar.gz Charm-1.10.0/charmtimetracker.spec000066400000000000000000000046621260343353100170340ustar00rootroot00000000000000Name: charmtimetracker Version: 1.9.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 %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 * 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.10.0/debian.changelog000066400000000000000000000013151260343353100157160ustar00rootroot00000000000000charmtimetracker (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.10.0/debian.control000066400000000000000000000017511260343353100154530ustar00rootroot00000000000000Source: charmtimetracker Section: Miscellaneous Priority: optional Maintainer: Frank Osterfeld Build-Depends: debhelper (>= 4.1.16), cdbs, cmake, libqt4-dev, libxss-dev, libqt4-sql-sqlite Package: charmtimetracker Architecture: any 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.10.0/debian.rules000066400000000000000000000002271260343353100151220ustar00rootroot00000000000000#!/usr/bin/make -f DEB_CMAKE_EXTRA_FLAGS = -DCharm_VERSION=1.9.0 include /usr/share/cdbs/1/rules/debhelper.mk include /usr/share/cdbs/1/class/cmake.mk