diff options
author | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
---|---|---|
committer | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
commit | 460c52653ab0dcca6f19a4f492ed2c5e4e963ab0 (patch) | |
tree | 67208f7c145782a7e90b123b982ca78d88cc2c87 /karm | |
download | tdepim-460c52653ab0dcca6f19a4f492ed2c5e4e963ab0.tar.gz tdepim-460c52653ab0dcca6f19a4f492ed2c5e4e963ab0.zip |
Copy the KDE 3.5 branch to branches/trinity for new KDE 3.5 features.
BUG:215923
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/kdepim@1054174 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'karm')
115 files changed, 12616 insertions, 0 deletions
diff --git a/karm/BUGS b/karm/BUGS new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/karm/BUGS diff --git a/karm/ChangeLog b/karm/ChangeLog new file mode 100644 index 000000000..6bec23479 --- /dev/null +++ b/karm/ChangeLog @@ -0,0 +1,65 @@ +KArm 1.5 (Feb, 2005 -- KDE 3.4 Release) +-------- +- Added import of Imedio Planner files +- Added DCOP interface +- api docs now build +- Enable CSV export to a remote server +- If another program changes current ical file, KArm now refreshes +- Added testing subdirectory +- Add ability to use remote resources for ical file + +Karm 1.4 (Aug 19, 2004 -- KDE 3.3 Release) +-------- +- add confirm messagebox on "reset all times" action +- improve messagebox text for "copy totals to clipboard" action +- add preference setting to allow user to turn off logging +- add CSV export +- don't crash if timer running when you change storage file +- if storage read-only, disable actions that change data +- honor user's timezone +- save tree state (open/closed) between sessions) +- edit task dialog: show full desktop name, not just desktop number +- sort data in time columns numerically, not alphabetically +- allow negative task times +- Modify desktop tracking so timers delay before switching when desktop +switches. So if user switches rapidly, time log won't fill up with lots of +useless entries. + +KArm 1.3 (Feb 3, 2004 -- KDE 3.2 Release) +-------- +- desktop tracking added +- active task indication in the system tray +- uses iCalendar format for data storage +- stores start/stop history +- settings dialog converted to icon format +- "Reset Session Times" re-reversed and renamed to + "Start New Session" for clarity +- new timecard report +- ability to mark tasks as complete + +KArm 1.2 +-------- +- "Reset Session Times" wouldn't get reset correctly. + Thanks to dwayne for reporting and to Scott Monachello for + the fix. closes: #34343 + +KArm 1.1 +-------- +- clocks are stopped and times saved upon exit + +KArm 0.6 +-------- +- It is now possible to type in the time like 3:20, or 3:20 + 1:10 +- The two QListBoxes with time and description has been replaced with one + QListView. This makes the interface nicer, and simplifies a lot internally. + +KArm 0.3 +-------- +- Fixes edit task bugs +- Complies to new FSSTND + + +KArm 0.2 +-------- +- Now supports floating toolbar +- Documentation converted to linuxdoc diff --git a/karm/Makefile.am b/karm/Makefile.am new file mode 100644 index 000000000..a16ac1bad --- /dev/null +++ b/karm/Makefile.am @@ -0,0 +1,67 @@ + +############ Autoconf-generated variables ################### +INCLUDES = -I$(top_srcdir)/libkcal -I$(top_srcdir)/kresources/remote -I$(top_srcdir) $(all_includes) + +bin_PROGRAMS = karm +SUBDIRS = support pics test + +METASOURCES = AUTO + +COMPILE_BEFORE_karm = libkdepim kresources + +noinst_LTLIBRARIES = libkarm_shared.la + +libkarm_shared_la_SOURCES = kaccelmenuwatch.cpp desktoptracker.cpp \ + edittaskdialog.cpp idletimedetector.cpp \ + printdialog.cpp timekard.cpp karmutility.cpp ktimewidget.cpp \ + karmstorage.cpp mainwindow.cpp preferences.cpp print.cpp \ + task.cpp taskview.cpp tray.cpp \ + csvexportdialog_base.ui csvexportdialog.cpp plannerparser.cpp \ + karmdcopiface.skel taskviewwhatsthis.cpp +libkarm_shared_la_LIBADD = $(top_builddir)/libkcal/libkcal.la \ + $(top_builddir)/kresources/remote/libkcal_resourceremote.la \ + $(top_builddir)/libkdepim/libkdepim.la $(LIBXSS) -lkdeprint +karm_SOURCES = main.cpp +karm_LDADD = libkarm_shared.la +karm_LDFLAGS = $(all_libraries) $(KDE_RPATH) +noinst_HEADERS = desktoptracker.h edittaskdialog.h printdialog.h \ + idletimedetector.h kaccelmenuwatch.h timekard.h \ + karmutility.h ktimewidget.h karmstorage.h mainwindow.h \ + preferences.h print.h task.h taskview.h toolicons.h \ + tray.h version.h csvexportdialog.h plannerparser.h taskviewwhatsthis.h + +KDE_ICON = karm + +rcdir = $(kde_datadir)/karm +rc_DATA = karmui.rc + +install-data-local: uninstall.desktop + $(mkinstalldirs) $(DESTDIR)$(kde_appsdir)/Utilities + $(INSTALL_DATA) $(srcdir)/uninstall.desktop $(DESTDIR)$(kde_appsdir)/Utilities/karm.desktop + +messages: rc.cpp + $(XGETTEXT) *.cpp -o $(podir)/karm.pot + +srcdoc: + kdoc -a -p -H -d $(HOME)/web/src/karm karm *.h -lkdecore -lkdeui -lqt + +DOXYGEN_REFERENCES = kdeui +include $(top_srcdir)/admin/Doxyfile.am + +######################################################################### +# KPART SECTION +######################################################################### +kde_module_LTLIBRARIES = libkarmpart.la + +# the Part's source, library search path, and link libraries +libkarmpart_la_SOURCES = karm_part.cpp +libkarmpart_la_LDFLAGS = -module -avoid-version -no-undefined $(KDE_PLUGIN) $(all_libraries) +libkarmpart_la_LIBADD = libkarm_shared.la $(LIB_KPARTS) + +# this is where the desktop file will go +partdesktopdir = $(kde_servicesdir) +partdesktop_DATA = karm_part.desktop + +# this is where the part's XML-GUI resource file goes +partrcdir = $(kde_datadir)/karmpart +partrc_DATA = karmui.rc diff --git a/karm/README b/karm/README new file mode 100644 index 000000000..af0034310 --- /dev/null +++ b/karm/README @@ -0,0 +1,12 @@ +KArm tracks time spent on various tasks. It is useful for tracking hours to be +billed to different clients or just to find out what percentage of your day is +spent playing Doom or reading Slashdot. + +KArm was originaly written by Sirtaj Singh Kang. The word "karm" means "work" +in the author's native Punjabi. + +KArm was inspired by Harald Tveit Alvestrand's very useful utility called +titrax, the only failing of which is that it is based on the Xt toolkit. One +day KArm will do all that titrax does, but not today. + +There is more info about karm at: http://pim.kde.org/components/karm.php diff --git a/karm/TODO b/karm/TODO new file mode 100644 index 000000000..12df8f5c6 --- /dev/null +++ b/karm/TODO @@ -0,0 +1,90 @@ +* KarmWindow::makeMenus -> export KAction? KarmWindow::contextMenuRequest +* QPopupMenu should be static! put connect( ..contextMenu at the right place +* mainwindow: move tray signals into tray.cpp add mouse double-click action +* (start new timer, stop old) to "Configure + Shortcuts" dialog. + +Mark - Sooner ... -------------------------- + + * 2005-05-24: Modify test scripts for change in dcop iface. + + * 2005-05-24: Update docs for new dcop methods. + + * 2003-09-24: Generate man page (look for script mentioned on kde-devel). + + * 2004-01-27: BUG: if save fails (b/c of permission failure) KArm does not + give any feecback and proceeds as if there was no error. + + * 2003-09-17: BUG: entering a negative time increments total session time + (maybe only if session time is zero?) + +Mark - Later ... -------------------------- + + * 2004-06-14: In file exports, check if QT has a function to give you a + platform-specific end of line character. Right now, it is hard coded to a + "\n" + + * 2003-08-15: Implement comments: VTODO COMMENT or DESCRIPTION? + - Make sure wrapping works according to iCalendar spec. + - Handle multiple comments against same VEVENT. + - Show in history report. + + * 2004-01-27: Write dcop interface so time cards can be exported to other + systems. + + * 2003-09-24: Change menu entry from KArm to Time Clock. + + * 2003-08-15: Go over code and delete all cruft. In particular, take a look + at taskview.cpp. + + * 2003-09-26: Add totals table across entire date range. + + * 2003-09-19: If task active, editing task stops timer. Necessary? + + * 2003-08-15: When loading an empty ics file, ask user if they want to + import a task list from another file. If they say no, then pop up the new + task dialog. (TaskView::load). + + * 2003-09-03: Use file menu and delete storage page in preferences. + + * 2003-08-15: Add ability to import tasks from an iCalendar file. + - TaskView::load() + + * 2003-09-26: On timecard report, don't print rows for tasks with zero hours + unless a child task has hours logged (that is, show full tree for every + task with time logged). + + * 2003-10-13: New event: publish time card. Mark events that are published + as read-only. Create plug-in framework so that when this event occurs, we + export to a different backend. Write plugins for GnuCash and SQL-Ledger. + Hmmm ... or just write a sql calendar resource? + + +Wishlist: -------------------------- +* activeTasks should be cleaned up if two timers are active, associate a +* percentage with them, optimaly the + percentage should be configurable +* completed % per task don't display competed tasks context menu in headers +* for choosing if one wants to see some column or not + + +Design Notes -------------------------- + +- Taskview mediates access to KarmStorage. That is one overzealous ListView + subclass! + +- The original code was written with the expectation that the user would be + starting and stopping timers. This perspective loses relevance when you + want book time with an arbitrary start and stop time. This impacts method + names and the arguments passed. For example, see KarmStorage bookTime + versus changeTime. Duplicate code, different method signatures. + +- How do deal with keeping gui responsive while saving data. Really should be + asynchronous. Then we have to be careful with race conditions; for example, + if an external fires a DCOP event while the user is stopping a time and we + have a really slow remote resource. + +- Add the dcop method bookTime, I originally forgot to increment the total time + counter on the task. This is a bad design--a proper task class would provide + an interface to log a history event and would increment the total time for + me. diff --git a/karm/configure.in.in b/karm/configure.in.in new file mode 100644 index 000000000..f17fa84ef --- /dev/null +++ b/karm/configure.in.in @@ -0,0 +1,13 @@ +ac_ldflags_safe="$LDFLAGS" +ac_libs_safe="$LIBS" +LIBS="-lXext -lX11 $X_LDFLAGS" + +LIBXSS="" +AC_CHECK_LIB(Xss, XScreenSaverAllocInfo, [LIBXSS="-lXext -lXss"]) +if test "x$LIBXSS" != "x"; then + AC_DEFINE(HAVE_LIBXSS,1,[Define if you have libXss installed]) +fi + +LIBS="$ac_libs_safe" +LDFLAGS="$ac_ldflags_safe" +AC_SUBST(LIBXSS) diff --git a/karm/csvexportdialog.cpp b/karm/csvexportdialog.cpp new file mode 100644 index 000000000..4a5536d40 --- /dev/null +++ b/karm/csvexportdialog.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2004 Mark Bucciarelli <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ +#include <kdateedit.h> +#include <kdebug.h> +#include <kglobal.h> +#include <klineedit.h> +#include <klocale.h> +#include <kpushbutton.h> +#include <kurlrequester.h> +#include <qbuttongroup.h> +#include <qcombobox.h> +#include <qradiobutton.h> + +#include "csvexportdialog.h" +#include "reportcriteria.h" + +CSVExportDialog::CSVExportDialog( ReportCriteria::REPORTTYPE rt, + QWidget *parent, + const char *name + ) + : CSVExportDialogBase( parent, name ) +{ + switch ( rt ) { + case ReportCriteria::CSVTotalsExport: + grpDateRange->setEnabled( false ); + grpDateRange->hide(); + rc.reportType = rt; + break; + case ReportCriteria::CSVHistoryExport: + grpDateRange->setEnabled( true ); + rc.reportType = rt; + break; + default: + break; + + } + + // If decimal symbol is a comma, then default field seperator to semi-colon. + // In France and Germany, one-and-a-half is written as 1,5 not 1.5 + QString d = KGlobal::locale()->decimalSymbol(); + if ( "," == d ) CSVExportDialogBase::radioSemicolon->setChecked(true); + else CSVExportDialogBase::radioComma->setChecked(true); + +} + +void CSVExportDialog::enableExportButton() +{ + btnExport->setEnabled( !urlExportTo->lineEdit()->text().isEmpty() ); +} + +void CSVExportDialog::enableTasksToExportQuestion() +{ + return; + //grpTasksToExport->setEnabled( true ); +} + +ReportCriteria CSVExportDialog::reportCriteria() +{ + rc.url = urlExportTo->url(); + rc.from = dtFrom->date(); + rc.to = dtTo->date(); + + // Hard code to true for now as the CSV export of totals does not support + // this choice currenly and I'm trying to minimize pre-3.3 hacking at the + // moment. + rc.allTasks = true; + + QString t = grpTimeFormat->selected()->name(); + rc.decimalMinutes = ( t == i18n( "radioDecimal" ) ); + + QString d = grpDelimiter->selected()->name(); + if ( d == "radioComma" ) rc.delimiter = ","; + else if ( d == "radioTab" ) rc.delimiter = "\t"; + else if ( d == "radioSemicolon" ) rc.delimiter = ";"; + else if ( d == "radioSpace" ) rc.delimiter = " "; + else if ( d == "radioOther" ) rc.delimiter = txtOther->text(); + else { + kdDebug(5970) + << "*** CSVExportDialog::reportCriteria: Unexpected delimiter choice '" + << d << "'--defaulting to a tab" << endl; + rc.delimiter = "\t"; + } + + rc.quote = cboQuote->currentText(); + + return rc; +} + +#include "csvexportdialog.moc" diff --git a/karm/csvexportdialog.h b/karm/csvexportdialog.h new file mode 100644 index 000000000..eae6bd4ab --- /dev/null +++ b/karm/csvexportdialog.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2004 Mark Bucciarelli <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ +#ifndef CSVEXPORTDIALOG_H +#define CSVEXPORTDIALOG_H + +#include "csvexportdialog_base.h" +#include "reportcriteria.h" + +class CSVExportDialog : public CSVExportDialogBase +{ + Q_OBJECT + + public: + CSVExportDialog( ReportCriteria::REPORTTYPE rt, + QWidget *parent = 0, + const char *name = 0 + ); + + /** + Enable the "Tasks to export" question in the dialog. + + Since Karm does not have the concept of a single root task, when the user + requests a report on a top-level task, it is impossible to know if they + want all tasks or just the currently selected top-level task. + + Stubbed for 3.3 release as CSV export of totals doesn't suppor this option. + */ + void enableTasksToExportQuestion(); + + /** + Return an object that encapsulates the choices the user has made. + */ + ReportCriteria reportCriteria(); + + private slots: + + /** + Enable export button if export url entered. + */ + void enableExportButton(); + + private: + ReportCriteria rc; +}; + +#endif diff --git a/karm/csvexportdialog_base.ui b/karm/csvexportdialog_base.ui new file mode 100644 index 000000000..45ecde5de --- /dev/null +++ b/karm/csvexportdialog_base.ui @@ -0,0 +1,413 @@ +<!DOCTYPE UI><UI version="3.3" stdsetdef="1"> +<class>CSVExportDialogBase</class> +<widget class="QDialog"> + <property name="name"> + <cstring>CSVExportDialogBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>343</width> + <height>361</height> + </rect> + </property> + <property name="caption"> + <string>CSV Export</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer row="5" column="1" rowspan="1" colspan="2"> + <property name="name"> + <cstring>spacer3</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>29</height> + </size> + </property> + </spacer> + <spacer row="6" column="0" rowspan="1" colspan="2"> + <property name="name"> + <cstring>spacer1</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>160</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="KPushButton" row="6" column="2"> + <property name="name"> + <cstring>btnExport</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>&Export</string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + <widget class="KPushButton" row="6" column="3"> + <property name="name"> + <cstring>btnCancel</cstring> + </property> + <property name="text"> + <string>&Cancel</string> + </property> + </widget> + <widget class="QLabel" row="0" column="0"> + <property name="name"> + <cstring>textLabel1_3</cstring> + </property> + <property name="text"> + <string>Export to:</string> + </property> + </widget> + <widget class="KURLRequester" row="0" column="1" rowspan="1" colspan="3"> + <property name="name"> + <cstring>urlExportTo</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>The file where Karm will write the data.</string> + </property> + </widget> + <widget class="QLabel" row="4" column="0" rowspan="1" colspan="2"> + <property name="name"> + <cstring>quotesLabel</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>1</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Quotes:</string> + </property> + </widget> + <widget class="QComboBox" row="4" column="2" rowspan="1" colspan="2"> + <item> + <property name="text"> + <string>"</string> + </property> + </item> + <item> + <property name="text"> + <string>'</string> + </property> + </item> + <property name="name"> + <cstring>cboQuote</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>1</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="whatsThis" stdset="0"> + <string>All fields are quoted in the output.</string> + </property> + </widget> + <widget class="QGroupBox" row="1" column="0" rowspan="1" colspan="4"> + <property name="name"> + <cstring>grpDateRange</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="title"> + <string>Date Range</string> + </property> + <property name="whatsThis" stdset="0"> + <string><p>An inclusive date range for reporting on time card history. Not enabled when reporting on totals.</p></string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel" row="0" column="0"> + <property name="name"> + <cstring>textLabel1</cstring> + </property> + <property name="text"> + <string>From:</string> + </property> + </widget> + <widget class="KDateEdit" row="1" column="0"> + <property name="name"> + <cstring>dtFrom</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>1</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>StrongFocus</enum> + </property> + </widget> + <widget class="QLabel" row="0" column="1"> + <property name="name"> + <cstring>textLabel1_2</cstring> + </property> + <property name="text"> + <string>To:</string> + </property> + </widget> + <widget class="KDateEdit" row="1" column="1"> + <property name="name"> + <cstring>dtTo</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>1</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>StrongFocus</enum> + </property> + </widget> + </grid> + </widget> + <widget class="QButtonGroup" row="2" column="0" rowspan="1" colspan="4"> + <property name="name"> + <cstring>grpTimeFormat</cstring> + </property> + <property name="title"> + <string>Time Format</string> + </property> + <property name="whatsThis" stdset="0"> + <string><p>You can choose to output time values in fractions of an hour or in minutes.</p> +<p>For example, if the value is 5 hours and 45 minutes, then the Decimal option would output <tt>5.75</tt>, and the Hours:Minutes option would output <tt>5:45</tt></p></string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QRadioButton" row="0" column="1"> + <property name="name"> + <cstring>radioHoursMinutes</cstring> + </property> + <property name="focusPolicy"> + <enum>NoFocus</enum> + </property> + <property name="text"> + <string>Hours:Minutes</string> + </property> + </widget> + <widget class="QRadioButton" row="0" column="0"> + <property name="name"> + <cstring>radioDecimal</cstring> + </property> + <property name="text"> + <string>Decimal</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </grid> + </widget> + <widget class="QButtonGroup" row="3" column="0" rowspan="1" colspan="4"> + <property name="name"> + <cstring>grpDelimiter</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>1</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Delimiter</string> + </property> + <property name="whatsThis" stdset="0"> + <string>The character used to seperate one field from another in the output.</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QRadioButton" row="1" column="0"> + <property name="name"> + <cstring>radioTab</cstring> + </property> + <property name="text"> + <string>Tab</string> + </property> + </widget> + <widget class="QRadioButton" row="0" column="2"> + <property name="name"> + <cstring>radioOther</cstring> + </property> + <property name="text"> + <string>Other:</string> + </property> + </widget> + <widget class="QRadioButton" row="1" column="1"> + <property name="name"> + <cstring>radioSpace</cstring> + </property> + <property name="text"> + <string>Space</string> + </property> + </widget> + <widget class="QRadioButton" row="0" column="0"> + <property name="name"> + <cstring>radioComma</cstring> + </property> + <property name="text"> + <string>Comma</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + <widget class="QLineEdit" row="1" column="2"> + <property name="name"> + <cstring>txtOther</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>30</width> + <height>32767</height> + </size> + </property> + <property name="focusPolicy"> + <enum>StrongFocus</enum> + </property> + <property name="maxLength"> + <number>1</number> + </property> + </widget> + <widget class="QRadioButton" row="0" column="1"> + <property name="name"> + <cstring>radioSemicolon</cstring> + </property> + <property name="text"> + <string>Semicolon</string> + </property> + </widget> + </grid> + </widget> + </grid> +</widget> +<customwidgets> + <customwidget> + <class>KDateEdit</class> + <header location="global">kdateedit.h</header> + <sizehint> + <width>95</width> + <height>21</height> + </sizehint> + <container>0</container> + <sizepolicy> + <hordata>5</hordata> + <verdata>5</verdata> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + <pixmap>image0</pixmap> + </customwidget> +</customwidgets> +<images> + <image name="image0"> + <data format="XPM.GZ" length="5899">789ced97596fe3460cc7dff3298cf02d28b81e599225147dc87d1f4e7693ec167d18cf8c723a8763e75af4bb572239dac891bcfbd4264542c0c90fc321f9a74805fe34d73adedb6ecd7d9ab91be9d1996999533d6ccdd9f160f0f4e75f7f7c9f994da356a05a4aa95630fbdbcc2c8e5ba6056dfa2918a0e484784338e4733c2b99cef1d0b3e2f3a1b011ff883829e35b611fefaa648eb7ccacdaa126ff1be13022c65de144cebd7fc2e7f0e439669e670eda11e985ac60d52e2a22de63564990129f30076187f9deb3dcbf14f6f9ae899332de0e7390848aeadd2a3828ee73fde7cc9d50ea47e6d0e890788139b27d4bfe1bccb1121e11a7ca70bf6193b96324fe3573d88ef9f93e0a87c29f99e3c844c4abc296f323f5aba3a2a8db216e13e7f93aa40786cc817fde0f05872ac83b4e7c2a6c848f88b5f2f3b42a6c84ef0a8e826e94b27e43ac95cc03ac316b6d1dc5eb151c0745c78907cc615b9edfb670c88cfbcc7942d20b6725533fe1cab3e57e7488b58f0f9ab9d30eb8df9c5f27b6cffd0a998d73541fb882d3b01bf1bca0664ebdbe3eb3f7c780d8f87ec28039b2a2e794d906e24ffa7418fafeae33c7e28f17c4a53fec0ac7c2cf05f7c344497dc7ccce6531f10ab18d15ebc53be654c93c0073dfc93cee31e7f1e93e2c166ca238927a52e624e27d80b1b095795d624eadcce701b356c27d6613cbbc2e30fb7ca8987dfd40fa8dcde3f17e717cebe3c1b97024f14f845dd2a7739a671b657196d139edb375da89ff6dc91cff80d9ca7cc2beb09379bd612ee33d096b66a07d76b10b444f22ecfb49fbeb9cf1f96e3da7867885d96ae9cf33735e20e7cf84fd7cae33fbfc48ef8f2c5f7fd9fffb92293e7e65764ee6dd316701f7bb37128e657ed684b5702c9c49be6fc45916f0fee1a5702cfddb12d6d28f2f25b39e45cf12df94ccf1bbc2999cdb9233ae1701f5bb34e88d7803f3bffb6fd4349a29966be07762ee69dfa84d57e0507f28f83714f4464d86d9878237a7e0044ff10ccff182e812077885d778534385dde27022dba4cf1d8e709c5bb3cf3d3ee4191ff1699a827cda9bec7942c13c2ee0222ee132d10aaee21aaee3460d59dcc42ddc9ec856f5d9c15ddc7b5551d5a787fb798507f8f917148c2b56af80ed0b1ed2ef233cce3fbf8a9e2a7dc3362a0c26b2557d3a18d65454f58930ce3fbb423f7b0693f53b4c6a15a4802fa303bca22e68c86f8099c856b901161c647002a7537c0c9ce59fe792a356014dbb37aeff07d728800bb894196d540003b822df6bb93596699fbc41130237cd3e700b433c84bb7c177e4d4156a93f83d1a40218c33d3cd43eef97f4721e9b27e4a8a2a7de87b33ec2944d9e50904d5300cf305fd65fee1c2cd490ef7d355bc5071669429660b9d987fc566015d6a62a786eb257cfa0ec2cac23bdf76003367fbc4d5f50bd828a0f6cc176be0b3bb0dbec43b9f6a0d758ffcf14ecbf87ff68ff0b054993c1c1878237a3a0f88ef65f57daa8000e9acd7f477be7df93fffe7de61f779ef4a1</data> + </image> +</images> +<connections> + <connection> + <sender>btnCancel</sender> + <signal>clicked()</signal> + <receiver>CSVExportDialogBase</receiver> + <slot>reject()</slot> + </connection> + <connection> + <sender>radioOther</sender> + <signal>toggled(bool)</signal> + <receiver>txtOther</receiver> + <slot>setEnabled(bool)</slot> + </connection> + <connection> + <sender>btnExport</sender> + <signal>clicked()</signal> + <receiver>CSVExportDialogBase</receiver> + <slot>accept()</slot> + </connection> + <connection> + <sender>urlExportTo</sender> + <signal>textChanged(const QString&)</signal> + <receiver>CSVExportDialogBase</receiver> + <slot>enableExportButton()</slot> + </connection> +</connections> +<tabstops> + <tabstop>urlExportTo</tabstop> + <tabstop>dtFrom</tabstop> + <tabstop>dtTo</tabstop> + <tabstop>radioDecimal</tabstop> + <tabstop>radioComma</tabstop> + <tabstop>txtOther</tabstop> + <tabstop>cboQuote</tabstop> + <tabstop>btnExport</tabstop> + <tabstop>btnCancel</tabstop> +</tabstops> +<slots> + <slot>enableExportButton()</slot> +</slots> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kdateedit.h</includehint> + <includehint>kdateedit.h</includehint> +</includehints> +</UI> diff --git a/karm/desktoplist.h b/karm/desktoplist.h new file mode 100644 index 000000000..ab8404822 --- /dev/null +++ b/karm/desktoplist.h @@ -0,0 +1,8 @@ +#ifndef KARM_DESKTOP_LIST_H +#define KARM_DESKTOP_LIST_H + +#include <qvaluevector.h> + +typedef QValueVector<int> DesktopList; + +#endif // KARM_DESKTOP_LIST_H diff --git a/karm/desktoptracker.cpp b/karm/desktoptracker.cpp new file mode 100644 index 000000000..52eb67210 --- /dev/null +++ b/karm/desktoptracker.cpp @@ -0,0 +1,137 @@ +#include <algorithm> // std::find + +#include <qtimer.h> +#include <kdebug.h> + +#include "desktoptracker.h" + +// TODO: Put in config dialog +const int minimumInterval = 5; // seconds + +DesktopTracker::DesktopTracker () +{ + // Setup desktop change handling + connect( &kWinModule, SIGNAL( currentDesktopChanged(int) ), + this, SLOT( handleDesktopChange(int) )); + + _desktopCount = kWinModule.numberOfDesktops(); + _previousDesktop = kWinModule.currentDesktop()-1; + // TODO: removed? fixed by Lubos? + // currentDesktop will return 0 if no window manager is started + if( _previousDesktop < 0 ) _previousDesktop = 0; + + _timer = new QTimer(this); + connect( _timer, SIGNAL( timeout() ), this, SLOT( changeTimers() ) ); +} + +void DesktopTracker::handleDesktopChange( int desktop ) +{ + _desktop = desktop; + + // If user changes back and forth between desktops rapidly and frequently, + // the data file can get huge fast if logging is turned on. Then saving + // get's slower, etc. There's no benefit in saving a lot of start/stop + // events that are very small. Wait a bit to make sure the user is settled. + if ( !_timer->start( minimumInterval * 1000, true ) ) changeTimers(); +} + +void DesktopTracker::changeTimers() +{ + _desktop--; // desktopTracker starts with 0 for desktop 1 + // notify start all tasks setup for running on desktop + TaskVector::iterator it; + + // stop trackers for _previousDesktop + TaskVector tv = desktopTracker[_previousDesktop]; + for (it = tv.begin(); it != tv.end(); ++it) { + emit leftActiveDesktop(*it); + } + + // start trackers for desktop + tv = desktopTracker[_desktop]; + for (it = tv.begin(); it != tv.end(); ++it) { + emit reachedtActiveDesktop(*it); + } + _previousDesktop = _desktop; + + // emit updateButtons(); +} + +QString DesktopTracker::startTracking() +{ + QString err; + int currentDesktop = kWinModule.currentDesktop() -1; + // TODO: removed? fixed by Lubos? + // currentDesktop will return 0 if no window manager is started + if ( currentDesktop < 0 ) currentDesktop = 0; + if ( currentDesktop < maxDesktops ) + { + TaskVector &tv = desktopTracker[ currentDesktop ]; + TaskVector::iterator tit = tv.begin(); + while(tit!=tv.end()) + { + emit reachedtActiveDesktop(*tit); + tit++; + } + } + else err="ETooHighDeskTopNumber"; + return err; +} + +void DesktopTracker::registerForDesktops( Task* task, DesktopList desktopList) +{ + // if no desktop is marked, disable auto tracking for this task + if (desktopList.size()==0) { + for (int i=0; i<maxDesktops; i++) { + TaskVector *v = &(desktopTracker[i]); + TaskVector::iterator tit = std::find(v->begin(), v->end(), task); + if (tit != v->end()) + desktopTracker[i].erase(tit); + // if the task was priviously tracking this desktop then + // emit a signal that is not tracking it any more + if( i == kWinModule.currentDesktop() -1) + emit leftActiveDesktop(task); + } + + return; + } + + // If desktop contains entries then configure desktopTracker + // If a desktop was disabled, it will not be stopped automatically. + // If enabled: Start it now. + if (desktopList.size()>0) { + for (int i=0; i<maxDesktops; i++) { + TaskVector& v = desktopTracker[i]; + TaskVector::iterator tit = std::find(v.begin(), v.end(), task); + // Is desktop i in the desktop list? + if ( std::find( desktopList.begin(), desktopList.end(), i) + != desktopList.end()) { + if (tit == v.end()) // not yet in start vector + v.push_back(task); // track in desk i + } + else { // delete it + if (tit != v.end()) // not in start vector any more + { + v.erase(tit); // so we delete it from desktopTracker + // if the task was priviously tracking this desktop then + // emit a signal that is not tracking it any more + if( i == kWinModule.currentDesktop() -1) + emit leftActiveDesktop(task); + } + } + } + startTracking(); + } +} + +void DesktopTracker::printTrackers() { + TaskVector::iterator it; + for (int i=0; i<maxDesktops; i++) { + TaskVector& start = desktopTracker[i]; + it = start.begin(); + while (it != start.end()) { + it++; + } + } +} +#include "desktoptracker.moc" diff --git a/karm/desktoptracker.h b/karm/desktoptracker.h new file mode 100644 index 000000000..fee7a026f --- /dev/null +++ b/karm/desktoptracker.h @@ -0,0 +1,55 @@ +#ifndef KARM_DESKTOP_TRACKER_H +#define KARM_DESKTOP_TRACKER_H + +#include <vector> + +#include <kwinmodule.h> + +#include "desktoplist.h" + +class Task; +class QTimer; + +typedef std::vector<Task *> TaskVector; +const int maxDesktops = 20; + +/** A utility to associate tasks with desktops + * As soon as a desktop is activated/left - an signal is emited for + * each task tracking that all tasks that want to track that desktop + */ + +class DesktopTracker: public QObject +{ + Q_OBJECT + + public: + DesktopTracker(); + void printTrackers(); + QString startTracking(); + void registerForDesktops( Task* task, DesktopList dl ); + int desktopCount() const { return _desktopCount; }; + + private: // member variables + KWinModule kWinModule; + + // define vectors for at most 16 virtual desktops + // E.g.: desktopTrackerStop[3] contains a vector with + // all tasks to be notified, when switching to/from desk 3. + TaskVector desktopTracker[maxDesktops]; + int _previousDesktop; + int _desktopCount; + int _desktop; + QTimer *_timer; + + signals: + void reachedtActiveDesktop( Task* task ); + void leftActiveDesktop( Task* task ); + + public slots: + void handleDesktopChange( int desktop ); + + private slots: + void changeTimers(); +}; + +#endif // KARM_DESKTOP_TRACKER_H diff --git a/karm/doc/Mainpage.dox b/karm/doc/Mainpage.dox new file mode 100644 index 000000000..7c8aee5f5 --- /dev/null +++ b/karm/doc/Mainpage.dox @@ -0,0 +1,214 @@ +/** \mainpage %Karm API Overview + +\section intro Introduction + +%Karm is a simple, easy to use time tracking program. It keeps a hierarchical list of tasks. Each task has a timer +associated with it. The primary user interaction for karm is to start and stop the appropriate timer. + +\section map Road Map to the Classes + +MainWindow is the outermost layer and initializes the menus and actions, sets +up the status bar, and handles many of the signal-to-slot connections. It +holds a pointer to the TaskView and Preferences objects and implements the +%Karm's DCOP interface (defined in KarmDCOPIface). + +TaskView does most of the work in the application. This KListView subclass +sets up the columns in the list, the idle detection timer, the auto save +timer, and the desktop tracker. It starts and stops timers, handles importing +and exporting and displays the edit task dialog in response to user action. +TaskView holds a private pointer to Preferences and KarmStorage objects. + +A Task is a QListViewItem subclass stores state such as the timer totals, if a +timer is currently running for the task. It also defines the list view sort +order and can return a pointer to a KCal::Todo object that holds the same +information. + +Preferences is a singleton that stores configuration options. It raises +signals when options change (for example, the location where the karm data is +stored) so the application can react and adjust. + +KarmStorage is a singleton that creates an interface for storing KArm data. +Currently, it uses KDE Resource framework and stores data in the iCalendar +format. + +*/ + +/** \page sig_slot_index Index of Signals and Slots + +To get an understanding of the flow program, it may be useful to see an overview of all of the signals, slots, and +connections. See \see connections to get an index of what signal is connected to which slot. + +\section overview Summary of what each class provides + +<table> + <tr><td><b><center>Class</center></b></td> <td><b><center>Signal?</center></b></td> <td><b><center>Public Slot?</center></b></td> + <td><b><center>Protected Slot?</center></b></td> <td><b><center>Private Slot?</center></b></td></tr> + + + <tr><td>AddTaskDialog</td> <td> </td> <td> </td> <td> </td> <td>Y </td></tr> + <tr><td>IdleTimer</td> <td>Y </td> <td>Y </td> <td>Y </td> <td> </td></tr> + <tr><td>KAccelMenuWatch</td> <td> </td> <td>Y </td> <td> </td> <td>Y </td></tr> + <tr><td>Karm</td> <td>Y </td> <td>Y </td> <td>Y </td> <td> </td></tr> + <tr><td>KarmTray</td> <td> </td> <td>Y </td> <td>Y </td> <td> </td></tr> + <tr><td>KarmWindow</td> <td> </td> <td> </td> <td>Y </td> <td> </td></tr> + <tr><td>KTimeWidget</td> <td> </td> <td> </td> <td> </td> <td> </td></tr> + <tr><td>ListViewIterator</td> <td> </td> <td> </td> <td> </td> <td> </td></tr> + <tr><td>Loging</td> <td> </td> <td> </td> <td> </td> <td> </td></tr> + <tr><td>MyPrinter</td> <td> </td> <td> </td> <td> </td> <td> </td></tr> + <tr><td>Preferences</td> <td>Y </td> <td>Y </td> <td>Y </td> <td> </td></tr> + <tr><td>SubtreeIterator</td> <td> </td> <td> </td> <td> </td> <td> </td></tr> + <tr><td>Task</td> <td> </td> <td> </td> <td>Y </td> <td> </td></tr> +</table> + +\section signals Listing of all of the signals + +These are the signals: +<ol> + <li>IdleTimer::extractTime(int) + <li>IdleTimer::stopTimer() + <li>Karm::sessionTimeChanged() + <li>Karm::timerActive() + <li>Karm::timerInactive() + <li>Karm::timerTick() + <li>Karm::updateButtons() + <li>Karm::tasksChanged(QPtrList<Task>) + <li>Preferences::autoSave(bool) + <li>Preferences::autoSavePeriod(int) + <li>Preferences::detectIdleness(bool) + <li>Preferences::idlenessTimeout(int) + <li>Preferences::saveFile(QString) + <li>Preferences::setupChanged() + <li>Preferences::timeLog(QString) + <li>Preferences::timeLoging(bool) + <li>Preferences::hideOnClose(bool) +</ol> + +\section slots Listing of the slots + +\subsection public Public Slots + +<ol> + <li>IdleTimer::setMaxIdle(int maxIdle) + <li>IdleTimer::startIdleDetection() + <li>IdleTimer::stopIdleDetection() + <li>IdleTimer::toggleOverAllIdleDetection(bool) + <li>KAccelMenuWatch::updateMenus() + <li>Karm::changeTimer(QListViewItem*) + <li>Karm::deleteTask() + <li>Karm::editTask() + <li>Karm::extractTime(int) + <li>Karm::load() + <li>Karm::newSubTask() + <li>Karm::newTask() + <li>Karm::newTask(QString, QListViewItem*) + <li>Karm::parseLine(QString, long*, QString*, int*) + <li>Karm::resetSessionTimeForAllTasks() + <li>Karm::save() + <li>Karm::startTimer() + <li>Karm::stopAllTimers() + <li>Karm::stopCurrentTimer() + <li>Karm::stopTimer(Task*) + <li>KarmTray::initToolTip() + <li>KarmTray::resetClock() + <li>KarmTray::startClock() + <li>KarmTray::stopClock() + <li>KarmTray::updateToolTip(QPtrList<Task>) + <li>Karm::writeTaskToFile(QTextStream*, QListViewItem*, int) + <li>Preferences::load() + <li>Preferences::save() + <li>Preferences::showDialog() +</ol> + +\subsection protected Protected Slots + +<ol> + <li>IdleTimer::check() + <li>Karm::addTimeToActiveTasks(int) + <li>Karm::autoSaveChanged(bool) + <li>Karm::autoSavePeriodChanged(int) + <li>Karm::minuteUpdate() + <li>Karm::stopChildCounters(Task*) + <li>KarmTray::advanceClock() + <li>KarmWindow::contextMenuRequest(QListViewItem*, const QPoint&, int) + <li>KarmWindow::disableStopAll() + <li>KarmWindow::enableStopAll() + <li>KarmWindow::hideOnClose(bool) + <li>KarmWindow::keyBindings() + <li>KarmWindow::print() + <li>KarmWindow::quit() + <li>KarmWindow::resetSessionTime(); + <li>KarmWindow::save() + <li>KarmWindow::slotSelectionChanged() + <li>KarmWindow::updateStatusBar() + <li>KarmWindow::updateTime() + <li>Preferences::autoSaveCheckBoxChanged() + <li>Preferences::hideOnCloseCheckBoxChanged() + <li>Preferences::idleDetectCheckBoxChanged() + <li>Preferences::slotCancel() + <li>Preferences::slotOk() + <li>Preferences::timeLogingCheckBoxChanged() + <li>Task::updateActiveIcon() +</ol> + +\subsection private Private Slots + +<ol> + <li>AddTaskDialog::enterWhatsThis() + <li>AddTaskDialog::slotAbsolutePressed() + <li>AddTaskDialog::slotRelativePressed() + <li>KAccelMenuWatch::removeDeadMenu() +</ol> + +*/ + +/** \page connections Index of the signal/slot connections + +<table> + <tr><td><center><b>Class</b></center></td> <td><center><b>Sender</b></center></td> <td><center><b>Sending Type</b></center></td> +<td><center><b>Signal</b></center></td> <td><center><b>Receiver</b></center></td> <td><center><b>Slot</b></center></td></tr> + +<tr><td>AddTaskDialog</td> <td>_absoluteRB</td> <td>QRadioButton</td> <td>clicked()</td> <td>this</td> <td>slotAbsolutePressed</td></tr> +<tr><td>AddTaskDialog</td> <td>_relativeRB</td> <td>QRadioButton</td> <td>clicked()</td> <td>this</td> <td>slotRelativePressed</td></tr> +<tr><td>AddTaskDialog</td> <td>whatsThisBU</td> <td>QPushButton</td> <td>clicked()</td> <td>this</td> <td>enterWhatsThis</td></tr> +<tr><td>IdleTimer</td> <td>_timer</td> <td>QTimer</td> <td>timeout()</td> <td>this</td> <td>check</td></tr> +<tr><td>KAccelMenuWatch</td> <td>menu</td> <td>QPopupMenu</td> <td>destroyed()</td> <td>this</td> <td>removeeDeadMenu</td></tr> +<tr><td>Karm</td> <td>this</td> <td>Karm__QListView</td> <td>doubleClicked()</td> <td>this</td> <td>changeTimer</td></tr> +<tr><td>Karm</td> <td>_minuteTimer</td> <td>QTimer</td> <td>timeout()</td> <td>this</td> <td>minuteUpdate</td></tr> +<tr><td>Karm</td> <td>_idleTimer</td> <td>IdleTimer</td> <td>extractTime()</td> <td>this</td> <td>extractTime</td></tr> +<tr><td>Karm</td> <td>_idleTimer</td> <td>IdleTimer</td> <td>stopTimer()</td> <td>this</td> <td>stopAllTimers</td></tr> +<tr><td>Karm</td> <td>_preferences</td> <td>Preferences</td> <td>idlenessTimeout()</td> <td>_idleTimer</td> <td>setMaxIdle</td></tr> +<tr><td>Karm</td> <td>_preferences</td> <td>Preferences</td> <td>detectIdleness()</td> <td>_idleTimer</td> <td>toggleOverAllIdleDetection</td></tr> +<tr><td>Karm</td> <td>_preferences</td> <td>Preferences</td> <td>autoSave()</td> <td>this</td> <td>autoSaveChanged</td></tr> +<tr><td>Karm</td> <td>_preferences</td> <td>Preferences</td> <td>autoSavePeriod()</td> <td>this</td> <td>autoSavePeriodChanged</td></tr> +<tr><td>Karm</td> <td>_autoSaveTimer</td> <td>QTimer</td> <td>timeout()</td> <td>this</td> <td>save</td></tr> +<tr><td>Karm</td> <td>_menu</td> <td>QPopupMenu</td> <td>__()</td> <td>this</td> <td>startTimer</td></tr> +<tr><td>Karm</td> <td>_menu</td> <td>QPopupMenu</td> <td>__()</td> <td>this</td> <td>stopCurrentTimer</td></tr> +<tr><td>Karm</td> <td>this</td> <td>Karm__QListView</td> <td>contextMenuRequested()</td> <td>this</td> <td>slotRMB</td></tr> +<tr><td>Preferences</td> <td>_doAutoSaveW</td> <td>QCheckBox</td> <td>clicked()</td> <td>this</td> <td>autoSaveCheckboxChanged</td></tr> +<tr><td>Preferences</td> <td>_doTimeLogingW</td> <td>QCheckBox</td> <td>clicked()</td> <td>this</td> <td>timeLogingCheckboxChanged</td></tr> +<tr><td>Preferences</td> <td>_doIdleDetectionW</td> <td>QCheckBox</td> <td>clicked()</td> <td>this</td> <td>idleDetectCheckBoxChanged</td></tr> +<tr><td>Preferences</td> <td>__</td> <td>Preferences__KDialogBase</td> <td>__()</td> <td>__</td> <td>slotOk</td></tr> +<tr><td>Preferences</td> <td>__</td> <td>Preferences__KDialogBase</td> <td>__()</td> <td>__</td> <td>slotCancel</td></tr> +<tr><td>KArmWindow</td> <td>_karm</td> <td>Karm</td> <td>sessionTimeChanged()</td> <td>this</td> <td>updateTime</td></tr> +<tr><td>KarmWindow</td> <td>_karm</td> <td>Karm__QListView</td> <td>currentChanged()</td> <td>this</td> <td>slotSelectionChanged</td></tr> +<tr><td>KarmWindow</td> <td>_karm</td> <td>Karm__QListView</td> <td>selectionChanged()</td> <td>this</td> <td>slotSelectionChanged</td></tr> +<tr><td>KarmWindow</td> <td>_karm</td> <td>Karm</td> <td>timerTick()</td> <td>this</td> <td>updateTime</td></tr> +<tr><td>KarmWindow</td> <td>_karm</td> <td>Karm</td> <td>timerActive()</td> <td>this</td> <td>setActiveIcon</td></tr> +<tr><td>KarmWindow</td> <td>_karm</td> <td>Karm</td> <td>timerInactive()</td> <td>this</td> <td>setInactiveIcon</td></tr> +<tr><td>KarmWindow</td> <td>KStdAction__quit</td> <td>KAction</td> <td>__()</td> <td>this</td> <td>quit</td></tr> +<tr><td>KarmWindow</td> <td>KStdAction__print</td> <td>KAction</td> <td>__()</td> <td>this</td> <td>print</td></tr> +<tr><td>KarmWindow</td> <td>KStdAction__keyBindings</td> <td>KAction</td> <td>__()</td> <td>this</td> <td>keyBindings</td></tr> +<tr><td>KarmWindow</td> <td>KStdAction__preferences</td> <td>KAction</td> <td>__()</td> <td>_preferences</td> <td>showDialog</td></tr> +<tr><td>KarmWindow</td> <td>KStdAction__save</td> <td>KAction</td> <td>__()</td> <td>_preferences</td> <td>save</td></tr> +<tr><td>KarmWindow</td> <td>actionResetSession</td> <td>KAction</td> <td>__()</td> <td>this</td> <td>resetSessionTime</td></tr> +<tr><td>KarmWindow</td> <td>actionStart</td> <td>KAction</td> <td>__()</td> <td>_karm</td> <td>startTimer</td></tr> +<tr><td>KarmWindow</td> <td>actionStop</td> <td>KAction</td> <td>__()</td> <td>_karm</td> <td>stopCurrentTimer</td></tr> +<tr><td>KarmWindow</td> <td>actionNew</td> <td>KAction</td> <td>__()</td> <td>_karm</td> <td>newTask</td></tr> +<tr><td>KarmWindow</td> <td>actionNewSub</td> <td>KAction</td> <td>__()</td> <td>_karm</td> <td>newSubTask</td></tr> +<tr><td>KarmWindow</td> <td>actionDelete</td> <td>KAction</td> <td>__()</td> <td>_karm</td> <td>deleteTask</td></tr> +<tr><td>KarmWindow</td> <td>actionEdit</td> <td>KAction</td> <td>__()</td> <td>_karm</td> <td>editTask</td></tr> +<tr><td>Task</td> <td>_timer</td> <td>QTimer</td> <td>timeout()</td> <td>this</td> <td>updateActiveIcon</td></tr> + +</table> + +*/ diff --git a/karm/doc/design b/karm/doc/design new file mode 100644 index 000000000..be9a425fa --- /dev/null +++ b/karm/doc/design @@ -0,0 +1,143 @@ +This document is meant to provide some documentation of rough consens +of where karm should be going and how things should be done. + +It does not represent something set in stone. Things can be discussed +and changed. +--------------------------------------------------------------------- + +* karm should not interfere if the user wants to run multiple tasks at + the same time that add up to more that 100%. + + It'd be nice though to have the possibility to have one task at a time + only (currently through double click). + + Or to let tasks' shares add up to 100%. Maybe through the context menu + ("share time with other task"). + +* tasks should update their own time and pass changes on down to the root. + The root is responsable for updating the status bar. + +Subject: Re: [Kde-pim] karm: how tasks should work +From: Scott Monachello <[email protected]> +Date: 2002-10-26 9:38:23 + +On Thursday 24 October 2002 06:37 pm, tomas pospisek wrote: +> OK guys, I'm moving this into public space. I think we're open source so +> it's here where these things should be discussed. I hope citing your +> proposition in public is fine with you Scott. So here it comes, +> reformatted to fit into a mail: +> +> Scott Monachello propopsed [reformatted by tpo] +> +> > Requirements for Karm Subtask Functions +> > +> > This is how HEAD currently operates. +> > Id Description +> > --------------------------------------------------------------------- +> > 1 Karm shall provide a hierarchical structure of tasks. If a task +> > has at least one subtask it will be referred to as a parent task. +> > If a task has no children it will be referred to as a leaf task. +> > If a task has no parent tasks it will be referred to as a root +> > task. +> > 2 A new task can be created as a child of any existing task. +> > 2.1 If the parent had a timer active, it will continue to be active +> +> It depends on how you start it. If you double click it. Any other timer +> will be stopped and the new task started. If you start it through the +> start button, both tasks will be active. This a bug IMO. See at the bottom +> for my proposal. +> +> > 2.2 The session time for the parent will not be changed by adding +> > the new child task. +> > 2.3 The total time for the parent will not be changed by adding a +> > the new child task. +> > 3 Any task (parent, leaf, or root) may have an independent timer. +> > 4 The time (both session and total) for a parent will be the sum +> > of its independent timer and the sum of all of its child timers. +> > +> > Unstable Development +> > This is my proposal for how Unstable_Development should operate. I +> > changed 2.1 - 4 and added and added 2.2.1 and 1008. +> > +> > Id Description +> > --------------------------------------------------------------------- +> > 1 Karm shall provide a hierarchical structure of tasks. If a task +> > has at least one subtask it will be referred to as a parent task. +> > If a task has no children it will be referred to as a leaf task. +> > If a task has no parent tasks it will be referred to as a root +> > task. +> > 2 A new task can be created as a child of any existing task. +> > 2.1 If the parent had a timer active, it will be deactivated +> > 2.2 The session time for the parent will set to zero +> > 2.2.1 The session time for the child will be initialized to the last +> > session time of the parent. +> > 2.3 The total time for the parent will be set to zero. +> > 2.3.1 The total time for the parent will be initialized to the last +> > total time of the parent. +> > 3 Only a leaf task may have a timer. A parent may not have its own +> > timer. +> > 4 The time (both session and total) for a parent will be the sum +> > only of its child timers. +> +> I see where you want to go, but I think it's not the right direction for +> two reasons: +> +> 1. Let's say I'm working on karm - I have a generic task "karm". Now I +> start working on the docu and add a subtask "docu". Right now I can +> switch between a generic task "working on karm" and more specific +> subtask "docu". Times are added together at the task "karm". That makes +> sense IMO. If I don't want to be specific I can - if I do want to be +> more precise I can as well. With your proposal this is not possible any +> more. +> +> 2. You break current setups. People are (I guess) using karm for real life +> things. When you change the behaveour to what you propose this +> force them to reorganize their trees. As a user, personally I'm not +> looking forward having to do this. +> +> My proposition is: +> +> Id Description +> --------------------------------------------------------------------- +> 2.1 If a new task is double clicked or started with the start +> button the previous task is stopped. + +So, only one task is ever active at one time, right? Tasks should be more like +radio boxes rather than check boxes. + +> 2.1.1 If someone feels like it s/he can add an entry/functionality into +> the context menu of a task to have it share proportionally the +> time being stopped with other tasks currently running. All the +> times always add up to 100%. +> This can be useful when doing >1 task at a time (compiling and +> phoning f.ex.) + +I've been thinking about something along these lines too. I think it's a good +idea but can't quite see how the interface should work. + +> +> The rest stays the same as in HEAD. +> +> Additionaly I propose: +> +> Id Description +> --------------------------------------------------------------------- +> 5 Times can be dragged and dropped, whereby they get transferred and +> added to the destination. +> 6 We move to scheme where times have a beginning and an ending and +> not just an absolute value. +> +> Comments? +> *t +> +> PS: Please follow up to the mailing list. + +Ok. So, I'll undo the changes related to: +* summing only leaf tasks +* disallowing edits on parent tasks +_______________________________________________ +kde-pim mailing list +http://mail.kde.org/mailman/listinfo/kde-pim +kde-pim home page at http://pim.kde.org/ + diff --git a/karm/doc/updating_parents.html b/karm/doc/updating_parents.html new file mode 100644 index 000000000..3b55ce30f --- /dev/null +++ b/karm/doc/updating_parents.html @@ -0,0 +1,39 @@ +<p>This is an outline of how times should be updated and added together.</p> + +<p>Start the program, create a task three levels deep and give it some time. +You should get something like this.</p> +<table border=2> + <tr><th>Task</th> <th>Session</th> <th>Total</th></tr> + <tr><td>A</td> <td>5</td> <td>5</td></tr> + <tr><td> a1</td> <td>5</td> <td>5</td></tr> + <tr><td> a11</td> <td>5</td> <td>5</td></tr> + <tr><td> a12</td> <td>0</td> <td>0</td></tr> + + <tr><td> </td> <td>Session: 15</td> <td>Total: 15</td></tr> +</table> + +<p>Now exit out of the program and start it up again. You should see the following.</p> +<table border=2> + <tr><th>Task</th> <th>Session</th> <th>Total</th></tr> + <tr><td>A</td> <td>0</td> <td>5</td></tr> + <tr><td> a1</td> <td>0</td> <td>5</td></tr> + <tr><td> a11</td> <td>0</td> <td>5</td></tr> + <tr><td> a12</td> <td>0</td> <td>0</td></tr> + + <tr><td> </td> <td>Session: 0</td> <td>Total: 15</td></tr> +</table> + +<p>Now start the timer and let it run for a minute, then stop it.</p> +<table border=2> + <tr><th>Task</th> <th>Session</th> <th>Total</th></tr> + <tr><td>A</td> <td>1</td> <td>6</td></tr> + <tr><td> a1</td> <td>1</td> <td>6</td></tr> + <tr><td> a11</td> <td>1</td> <td>6</td></tr> + <tr><td> a12</td> <td>0</td> <td>0</td></tr> + + <tr><td> </td> <td>Session: 3</td> <td>Total: 18</td></tr> +</table> + +<p>Since a parent task can have a timer active on it, it needs to be included in the summary +Session and Total times. So, in this example, the Session time will jump three minutes for every +minute the timer is active.</p> diff --git a/karm/edittaskdialog.cpp b/karm/edittaskdialog.cpp new file mode 100644 index 000000000..fb3c49b88 --- /dev/null +++ b/karm/edittaskdialog.cpp @@ -0,0 +1,339 @@ +/* + * karm + * This file only: Copyright (C) 1999 Espen Sand, [email protected] + * Modifications (see CVS log) Copyright (C) 2000 Klar�lvdalens + * Datakonsult AB <[email protected]>, Jesper Pedersen <[email protected]> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ +#include <qbuttongroup.h> +#include <qcombobox.h> +#include <qgroupbox.h> +#include <qhbox.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qlineedit.h> +#include <qradiobutton.h> +#include <qsizepolicy.h> +#include <qstring.h> +#include <qwidget.h> +#include <qwhatsthis.h> + +#include <klocale.h> // i18n +#include <kwinmodule.h> + +#include "edittaskdialog.h" +#include "ktimewidget.h" +#include "kdebug.h" + +EditTaskDialog::EditTaskDialog( QString caption, bool editDlg, + DesktopList* desktopList) + : KDialogBase(0, "EditTaskDialog", true, caption, Ok|Cancel, Ok, true ), + origTime( 0 ), origSession( 0 ) +{ + QWidget *page = new QWidget( this ); + setMainWidget(page); + KWinModule kwinmodule(0, KWinModule::INFO_DESKTOP); + + QVBoxLayout *lay1 = new QVBoxLayout(page); + + QHBoxLayout *lay2 = new QHBoxLayout(); + lay1->addLayout(lay2); + + // The name of the widget + QLabel *label = new QLabel( i18n("Task &name:"), page, "name" ); + lay2->addWidget( label ); + lay2->addSpacing(5); + + + _name = new QLineEdit( page, "lineedit" ); + + _name->setMinimumWidth(fontMetrics().maxWidth()*15); + lay2->addWidget( _name ); + label->setBuddy( _name ); + + + // The "Edit Absolut" radio button + lay1->addSpacing(10);lay1->addStretch(1); + _absoluteRB = new QRadioButton( i18n( "Edit &absolute" ), page, + "_absoluteRB" ); + lay1->addWidget( _absoluteRB ); + connect( _absoluteRB, SIGNAL( clicked() ), this, SLOT( slotAbsolutePressed() ) ); + + + // Absolute times + QHBoxLayout *lay5 = new QHBoxLayout(); + lay1->addLayout(lay5); + lay5->addSpacing(20); + QGridLayout *lay3 = new QGridLayout( 2, 2, -1, "lay3" ); + lay5->addLayout(lay3); + + _sessionLA = new QLabel( i18n("&Session time: "), page, "session time" ); + + // Time + _timeLA = new QLabel( i18n("&Time:"), page, "time" ); + lay3->addWidget( _timeLA, 0, 0 ); + _timeLA->setSizePolicy( QSizePolicy( (QSizePolicy::SizeType)1, + (QSizePolicy::SizeType)0, + 0, + 0, + _timeLA->sizePolicy().hasHeightForWidth()) ); + + // Based on measuring pixels in a screenshot, it looks like the fontmetrics + // call includes the ampersand when calculating the width. To be sure + // things will line up (no matter what language or widget style), set all + // three date entry label controls to the same width. + _timeLA->setMinimumWidth( fontMetrics().width( _sessionLA->text() ) ); + + _timeTW = new KArmTimeWidget( page, "_timeTW" ); + lay3->addWidget( _timeTW, 0, 1 ); + _timeLA->setBuddy( _timeTW ); + + + // Session + lay3->addWidget( _sessionLA, 1, 0 ); + + _sessionTW = new KArmTimeWidget( page, "_sessionTW" ); + lay3->addWidget( _sessionTW, 1, 1 ); + _sessionLA->setBuddy( _sessionTW ); + _sessionLA->setSizePolicy( QSizePolicy( (QSizePolicy::SizeType)1, + (QSizePolicy::SizeType)0, + 0, + 0, + _sessionLA->sizePolicy().hasHeightForWidth()) ); + _sessionLA->setMinimumWidth( fontMetrics().width( _sessionLA->text() ) ); + + + // The "Edit relative" radio button + lay1->addSpacing(10); + lay1->addStretch(1); + _relativeRB = new QRadioButton( i18n( "Edit &relative (apply to both time and" + " session time)" ), page, "_relativeRB" ); + lay1->addWidget( _relativeRB ); + connect( _relativeRB, SIGNAL( clicked() ), this, SLOT(slotRelativePressed()) ); + + // The relative times + QHBoxLayout *lay4 = new QHBoxLayout(); + lay1->addLayout( lay4 ); + lay4->addSpacing(20); + + _operator = new QComboBox(page); + _operator->insertItem( QString::fromLatin1( "+" ) ); + _operator->insertItem( QString::fromLatin1( "-" ) ); + _operator->setSizePolicy( QSizePolicy( (QSizePolicy::SizeType)1, + (QSizePolicy::SizeType)0, + 0, + 0, + _operator->sizePolicy().hasHeightForWidth()) ); + //kdDebug() << "text width=" << fontMetrics().width( _sessionLA->text() ) << endl; + _operator->setMinimumWidth( fontMetrics().width( _sessionLA->text() ) ); + lay4->addWidget( _operator ); + + _diffTW = new KArmTimeWidget( page, "_sessionAddTW" ); + lay4->addWidget( _diffTW ); + + desktopCount = kwinmodule.numberOfDesktops(); + + // If desktopList contains higher numbered desktops than desktopCount then + // delete those from desktopList. This may be the case if the user has + // configured virtual desktops. The values in desktopList are sorted. + if ( (desktopList != 0) && (desktopList->size() > 0) ) + { + DesktopList::iterator rit = desktopList->begin(); + while (*rit < desktopCount && rit!=desktopList->end()) + { + ++rit; + } + desktopList->erase(rit, desktopList->end()); + } + + // The "Choose Desktop" checkbox + lay1->addSpacing(10); + lay1->addStretch(1); + + _desktopCB = new QCheckBox(i18n("A&uto tracking"), page); + _desktopCB->setEnabled(true); + lay1->addWidget(_desktopCB); + + QGroupBox* groupBox; + { + int lines = (int)(desktopCount/2); + if (lines*2 != desktopCount) lines++; + groupBox = new QButtonGroup( lines, QGroupBox::Horizontal, + i18n("In Desktop"), page, "_desktopsGB"); + } + lay1->addWidget(groupBox); + + QHBoxLayout *lay6 = new QHBoxLayout(); + + lay1->addLayout(lay6); + for (int i=0; i<desktopCount; i++) { + _deskBox.push_back(new QCheckBox(groupBox,QString::number(i).latin1())); + _deskBox[i]->setText(kwinmodule.desktopName(i+1)); + _deskBox[i]->setChecked(false); + + lay6->addWidget(_deskBox[i]); + } + // check specified Desktop Check Boxes + bool enableDesktops = false; + + if ( (desktopList != 0) && (desktopList->size() > 0) ) + { + DesktopList::iterator it = desktopList->begin(); + while (it != desktopList->end()) + { + _deskBox[*it]->setChecked(true); + it++; + } + enableDesktops = true; + } + // if some desktops were specified, then enable the parent box + _desktopCB->setChecked(enableDesktops); + + for (int i=0; i<desktopCount; i++) + _deskBox[i]->setEnabled(enableDesktops); + + connect(_desktopCB, SIGNAL(clicked()), this, SLOT(slotAutoTrackingPressed())); + + lay1->addStretch(1); + + + if ( editDlg ) { + // This is an edit dialog. + _operator->setFocus(); + } + else { + // This is an initial dialog + _name->setFocus(); + } + + slotRelativePressed(); + + // Whats this help. + QWhatsThis::add( _name, + i18n( "Enter the name of the task here. " + "This name is for your eyes only.")); + QWhatsThis::add( _absoluteRB, + i18n( "Use this option to set the time spent on this task " + "to an absolute value.\n\nFor example, if you have " + "worked exactly four hours on this task during the current " + "session, you would set the Session time to 4 hr." ) ); + QWhatsThis::add( _relativeRB, + i18n( "Use this option to change the time spent on this task " + "relative to its current value.\n\nFor example, if you worked " + "on this task for one hour without the timer running, you " + "would add 1 hr." ) ); + QWhatsThis::add( _timeTW, + i18n( "This is the time the task has been " + "running since all times were reset.")); + QWhatsThis::add( _sessionTW, + i18n( "This is the time the task has been running this " + "session.")); + QWhatsThis::add( _diffTW, i18n( "Specify how much time to add or subtract " + "to the overall and session time")); + + QWhatsThis::add( _desktopCB, + i18n( "Use this option to automatically start the timer " + "on this task when you switch to the specified desktop(s)." ) ); + QWhatsThis::add( groupBox, + i18n( "Select the desktop(s) that will automatically start the " + "timer on this task." ) ); +} + + +void EditTaskDialog::slotAbsolutePressed() +{ + _relativeRB->setChecked( false ); + _absoluteRB->setChecked( true ); + + _operator->setEnabled( false ); + _diffTW->setEnabled( false ); + + _timeLA->setEnabled( true ); + _sessionLA->setEnabled( true ); + _timeTW->setEnabled( true ); + _sessionTW->setEnabled( true ); +} + +void EditTaskDialog::slotRelativePressed() +{ + _relativeRB->setChecked( true ); + _absoluteRB->setChecked( false ); + + _operator->setEnabled( true ); + _diffTW->setEnabled( true ); + + _timeLA->setEnabled( false ); + _sessionLA->setEnabled( false ); + _timeTW->setEnabled( false ); + _sessionTW->setEnabled( false ); +} + +void EditTaskDialog::slotAutoTrackingPressed() +{ + bool checked = _desktopCB->isChecked(); + for (unsigned int i=0; i<_deskBox.size(); i++) + _deskBox[i]->setEnabled(checked); + + if (!checked) // uncheck all desktop boxes + for (int i=0; i<desktopCount; i++) + _deskBox[i]->setChecked(false); +} + +void EditTaskDialog::setTask( const QString &name, long time, long session ) +{ + _name->setText( name ); + + _timeTW->setTime( time ); + _sessionTW->setTime( session ); + origTime = time; + origSession = session; +} + + +QString EditTaskDialog::taskName() const +{ + return( _name->text() ); +} + + +void EditTaskDialog::status(long *time, long *timeDiff, long *session, + long *sessionDiff, DesktopList *desktopList) const +{ + if ( _absoluteRB->isChecked() ) { + *time = _timeTW->time(); + *session = _sessionTW->time(); + } + else { + int diff = _diffTW->time(); + if ( _operator->currentItem() == 1) { + diff = -diff; + } + *time = origTime + diff; + *session = origSession + diff; + } + + *timeDiff = *time - origTime; + *sessionDiff = *session - origSession; + + for (unsigned int i=0; i<_deskBox.size(); i++) { + if (_deskBox[i]->isChecked()) + desktopList->push_back(i); + } +} + +#include "edittaskdialog.moc" diff --git a/karm/edittaskdialog.h b/karm/edittaskdialog.h new file mode 100644 index 000000000..0954d8b5d --- /dev/null +++ b/karm/edittaskdialog.h @@ -0,0 +1,89 @@ +/* + * karm + * This file only: Copyright (C) 1999 Espen Sand, [email protected] + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ + +#ifndef KARM_EDIT_TASK_DIALOG_H +#define KARM_EDIT_TASK_DIALOG_H + +#include <vector> + +#include <kdialogbase.h> +#include <qcheckbox.h> + +#include "desktoplist.h" + +class QComboBox; +class QLabel; +class QLineEdit; +class QRadioButton; +class QString; + +class KArmTimeWidget; + +/** + * Dialog to add a new task or edit an existing task. + */ + +class EditTaskDialog : public KDialogBase +{ + Q_OBJECT + + public: + EditTaskDialog(QString caption, bool editDlg, DesktopList* desktopList=0); + void setTask(const QString &name, long time, long sessionTime); + QString taskName() const; + + // return user choices + void status( long *time, long *timeDiff, + long *session, long *sessionDiff, + DesktopList *desktopList) const; + + private slots: + void slotAbsolutePressed(); + void slotRelativePressed(); + void slotAutoTrackingPressed(); + + private: + QLineEdit* _name; + KArmTimeWidget* _timeTW; + KArmTimeWidget* _sessionTW; + KArmTimeWidget* _diffTW; + QComboBox* _operator; + std::vector<QCheckBox*> _deskBox; // we only need an array, but ISO forbids + // passing an array as a function argument + + long origTime; + long origSession; + + QRadioButton *_absoluteRB; + QRadioButton *_relativeRB; + + QCheckBox *_desktopCB; + int desktopCount; + + QLabel* _timeLA; + QLabel* _sessionLA; +}; + + + + + +#endif // KARM_EDIT_TASK_DIALOG + diff --git a/karm/hi128-app-karm.png b/karm/hi128-app-karm.png Binary files differnew file mode 100644 index 000000000..a6fd4ebc0 --- /dev/null +++ b/karm/hi128-app-karm.png diff --git a/karm/hi16-app-karm.png b/karm/hi16-app-karm.png Binary files differnew file mode 100644 index 000000000..787527ce2 --- /dev/null +++ b/karm/hi16-app-karm.png diff --git a/karm/hi22-app-karm.png b/karm/hi22-app-karm.png Binary files differnew file mode 100644 index 000000000..877f5bb31 --- /dev/null +++ b/karm/hi22-app-karm.png diff --git a/karm/hi32-app-karm.png b/karm/hi32-app-karm.png Binary files differnew file mode 100644 index 000000000..4d63947e5 --- /dev/null +++ b/karm/hi32-app-karm.png diff --git a/karm/hi48-app-karm.png b/karm/hi48-app-karm.png Binary files differnew file mode 100644 index 000000000..9398a9e96 --- /dev/null +++ b/karm/hi48-app-karm.png diff --git a/karm/hi64-app-karm.png b/karm/hi64-app-karm.png Binary files differnew file mode 100644 index 000000000..796dddfe5 --- /dev/null +++ b/karm/hi64-app-karm.png diff --git a/karm/idletimedetector.cpp b/karm/idletimedetector.cpp new file mode 100644 index 000000000..878993798 --- /dev/null +++ b/karm/idletimedetector.cpp @@ -0,0 +1,126 @@ +#include "idletimedetector.h" + +#include <qdatetime.h> +#include <qmessagebox.h> +#include <qtimer.h> + +#include <kglobal.h> +#include <klocale.h> // i18n + +IdleTimeDetector::IdleTimeDetector(int maxIdle) +// Trigger a warning after maxIdle minutes +{ + kdDebug(5970) << "Entering IdleTimeDetector::IdleTimeDetector" << endl; + _maxIdle = maxIdle; + +#ifdef HAVE_LIBXSS + kdDebug(5970) << "IdleTimeDetector: LIBXSS detected @ compile time" << endl; + int event_base, error_base; + if(XScreenSaverQueryExtension(qt_xdisplay(), &event_base, &error_base)) + { + _idleDetectionPossible = true; + } + else + { + _idleDetectionPossible = false; + } + + _timer = new QTimer(this); + connect(_timer, SIGNAL(timeout()), this, SLOT(check())); +#else + _idleDetectionPossible = false; +#endif // HAVE_LIBXSS + +} + +bool IdleTimeDetector::isIdleDetectionPossible() +{ + return _idleDetectionPossible; +} + +void IdleTimeDetector::check() +{ + kdDebug(5970) << "Entering IdleTimeDetector::check" << endl; +#ifdef HAVE_LIBXSS + if (_idleDetectionPossible) + { + _mit_info = XScreenSaverAllocInfo (); + XScreenSaverQueryInfo(qt_xdisplay(), qt_xrootwin(), _mit_info); + int idleSeconds = (_mit_info->idle/1000); + if (idleSeconds >= _maxIdle) + informOverrun(idleSeconds); + } +#endif // HAVE_LIBXSS +} + +void IdleTimeDetector::setMaxIdle(int maxIdle) +{ + _maxIdle = maxIdle; +} + +#ifdef HAVE_LIBXSS +void IdleTimeDetector::informOverrun(int idleSeconds) +{ + kdDebug(5970) << "Entering IdleTimeDetector::informOverrun" << endl; + if (!_overAllIdleDetect) + return; // preferences say the user does not want idle detection. + + _timer->stop(); + + QDateTime idleStart = QDateTime::currentDateTime().addSecs(-idleSeconds); + QString idleStartQString = KGlobal::locale()->formatTime(idleStart.time()); + + int id = QMessageBox::warning( 0, i18n("Idle Detection"), + i18n("Desktop has been idle since %1." + " What should we do?").arg(idleStartQString), + i18n("Revert && Stop"), + i18n("Revert && Continue"), + i18n("Continue Timing"),0,2); + QDateTime end = QDateTime::currentDateTime(); + int diff = idleStart.secsTo(end)/secsPerMinute; + + if (id == 0) + { + // Revert And Stop + kdDebug(5970) << "Now it is " << QDateTime::currentDateTime() << endl; + kdDebug(5970) << "Reverting timer to " << KGlobal::locale()->formatTime(idleStart.time()).ascii() << endl; + emit(extractTime(idleSeconds/60+diff)); // we need to subtract the time that has been added during idleness. + emit(stopAllTimersAt(idleStart)); + } + else if (id == 1) + { + // Revert and Continue + emit(extractTime(idleSeconds/60+diff)); + _timer->start(testInterval); + } + else + { + // Continue + _timer->start(testInterval); + } +} +#endif // HAVE_LIBXSS + +void IdleTimeDetector::startIdleDetection() +{ + kdDebug(5970) << "Entering IdleTimeDetector::startIdleDetection" << endl; +#ifdef HAVE_LIBXSS + kdDebug(5970) << "Starting Timer" << endl; + if (!_timer->isActive()) + _timer->start(testInterval); +#endif //HAVE_LIBXSS +} + +void IdleTimeDetector::stopIdleDetection() +{ +#ifdef HAVE_LIBXSS + if (_timer->isActive()) + _timer->stop(); +#endif // HAVE_LIBXSS +} +void IdleTimeDetector::toggleOverAllIdleDetection(bool on) +{ + _overAllIdleDetect = on; +} + +#include "idletimedetector.moc" diff --git a/karm/idletimedetector.h b/karm/idletimedetector.h new file mode 100644 index 000000000..b7092930c --- /dev/null +++ b/karm/idletimedetector.h @@ -0,0 +1,104 @@ +#ifndef KARM_IDLE_TIME_DETECTOR_H +#define KARM_IDLE_TIME_DETECTOR_H + +#include <qobject.h> +#include "config.h" // HAVE_LIBXSS +#include <qdatetime.h> +#include <kdebug.h> + +class QTimer; + +#ifdef HAVE_LIBXSS + #include <X11/Xlib.h> + #include <X11/Xutil.h> + #include <X11/extensions/scrnsaver.h> + #include <fixx11h.h> +#endif // HAVE_LIBXSS + +// Seconds per minutes - useful for speeding debugging up! +const int secsPerMinute = 60; + +// Minutes between each idle overrun test. +const int testInterval= secsPerMinute * 1000; + +/** + * Keep track of how long the computer has been idle. + */ + +class IdleTimeDetector :public QObject +{ +Q_OBJECT + +public: + /** + Initializes and idle test timer + @param maxIdle minutes before the idle timer will go off. + **/ + IdleTimeDetector(int maxIdle); + + /** + Returns true if it is possible to do idle detection. + Idle detection relys on a feature in the X server, which might not + always be present. + **/ + bool isIdleDetectionPossible(); + +signals: + /** + Tells the listener to extract time from current timing. + The time to extract is due to the idle time since the dialog wass + shown, and until the user answers the dialog. + @param minutes Minutes to extract. + **/ + void extractTime(int minutes); + + /** Tells the listener to stop timing **/ + void stopAllTimers(); + + /** Tells the listener to stop timing for QDateTime **/ + void stopAllTimersAt(QDateTime qdt); + +public slots: + /** + Sets the maximum allowed idle. + @param maxIdle Maximum allowed idle time in minutes + **/ + void setMaxIdle(int maxIdle); + + /** + Starts detecting idle time + **/ + void startIdleDetection(); + + /** + Stops detecting idle time. + **/ + void stopIdleDetection(); + + /** + Sets whether idle detection should be done at all + @param on If true idle detection is done based on + startIdleDetection and @ref stopIdleDetection + **/ + void toggleOverAllIdleDetection(bool on); + + +protected: +#ifdef HAVE_LIBXSS + void informOverrun(int idle); +#endif // HAVE_LIBXSS + +protected slots: + void check(); + +private: +#ifdef HAVE_LIBXSS + XScreenSaverInfo *_mit_info; +#endif + bool _idleDetectionPossible; + bool _overAllIdleDetect; // Based on preferences. + int _maxIdle; + QTimer *_timer; +}; + +#endif // KARM_IDLE_TIME_DETECTOR_H diff --git a/karm/kaccelmenuwatch.cpp b/karm/kaccelmenuwatch.cpp new file mode 100644 index 000000000..f3aac3230 --- /dev/null +++ b/karm/kaccelmenuwatch.cpp @@ -0,0 +1,113 @@ +/* +* kaccelmenuwatch.cpp -- Implementation of class KAccelMenuWatch. +* Author: Sirtaj Singh Kang +* Generated: Thu Jan 7 15:05:26 EST 1999 +*/ + +#include <assert.h> +#include <qpopupmenu.h> + +#include "kaccelmenuwatch.h" + +KAccelMenuWatch::KAccelMenuWatch( KAccel *accel, QObject *parent ) + : QObject( parent ), + _accel( accel ), + _menu ( 0 ) +{ + _accList.setAutoDelete( true ); + _menuList.setAutoDelete( false ); +} + +void KAccelMenuWatch::setMenu( QPopupMenu *menu ) +{ + assert( menu ); + + // we use _menuList to ensure that the signal is + // connected only once per menu. + + if ( !_menuList.findRef( menu ) ) { + _menuList.append( menu ); + connect( menu, SIGNAL(destroyed()), this, SLOT(removeDeadMenu()) ); + } + + _menu = menu; +} + +void KAccelMenuWatch::connectAccel( int itemId, const char *action ) +{ + AccelItem *item = newAccelItem( _menu, itemId, StringAccel ) ; + item->action = QString::fromLocal8Bit( action ); +} + +void KAccelMenuWatch::connectAccel( int itemId, KStdAccel::StdAccel accel ) +{ + AccelItem *item = newAccelItem( _menu, itemId, StdAccel ) ; + item->stdAction = accel; +} + +void KAccelMenuWatch::updateMenus() +{ + assert( _accel != 0 ); + + QPtrListIterator<AccelItem> iter( _accList ); + AccelItem *item; + + for( ; (item = iter.current()) ; ++iter ) { + // These setAccel calls were converted from all changeMenuAccel calls + // as descibed in KDE3PORTING.html + switch( item->type ) { + case StringAccel: + item->menu->setAccel( _accel->shortcut( item->action ).keyCodeQt(), item->itemId ); + break; + case StdAccel: + item->menu->setAccel( KStdAccel::shortcut( item->stdAction ).keyCodeQt(), item->itemId ); + break; + default: + break; + } + } + +} + +void KAccelMenuWatch::removeDeadMenu() +{ + QPopupMenu *sdr = (QPopupMenu *) sender(); + assert( sdr ); + + if ( !_menuList.findRef( sdr ) ) + return; + + // remove all accels + + AccelItem *accel; + for ( accel = _accList.first(); accel; accel = _accList.next() ) + { +loop: + if( accel && accel->menu == sdr ) { + _accList.remove(); + accel = _accList.current(); + goto loop; + } + } + + // remove from menu list + _menuList.remove( sdr ); + + return; +} + +KAccelMenuWatch::AccelItem *KAccelMenuWatch::newAccelItem( QPopupMenu *, + int itemId, AccelType type ) +{ + AccelItem *item = new AccelItem; + + item->menu = _menu; + item->itemId = itemId; + item->type = type; + + _accList.append( item ); + + return item; +} + +#include "kaccelmenuwatch.moc" diff --git a/karm/kaccelmenuwatch.h b/karm/kaccelmenuwatch.h new file mode 100644 index 000000000..76b27f018 --- /dev/null +++ b/karm/kaccelmenuwatch.h @@ -0,0 +1,113 @@ +/* +* kaccelmenuwatch.h -- Declaration of class KAccelMenuWatch. +* Generated by newclass on Thu Jan 7 15:05:26 EST 1999. +*/ +#ifndef KARM_K_ACCEL_MENU_WATCH_H +#define KARM_K_ACCEL_MENU_WATCH_H + +#include <qobject.h> +#include <qptrlist.h> + +#include <kaccel.h> + +class QPopupMenu; + +/** + * Easy updating of menu accels when changing a KAccel object. + * + * Since a KAccel object does not keep track of menu items to which it + * is connected, we normally have to manually call + * KAccel::changeMenuAccel() again for each update of the KAccel object. + * + * With KAccelMenuWatch you provide the kaccel object and the menu + * items to which it connects, and if you update the kaccel you just have + * to call KAccelMenuWatch::updateMenus() and the menu items will be updated. + * + * It is safe to delete menus that have connections handled by this class. + * On deletion of a menu, all associated accelerators will be deleted. + * + * Note that you _have_ to call KAccelMenuWatch::updateMenus() after you + * connect the * accelerators, as they are not activated till then. + * + * @author Sirtaj Singh Kang ([email protected]) + */ + +class KAccelMenuWatch : public QObject +{ + Q_OBJECT + + private: + enum AccelType { StdAccel, StringAccel }; + + typedef struct AccelItem { + QPopupMenu *menu; + int itemId; + + AccelType type; + + // only one of these is used at a time + QString action; + KStdAccel::StdAccel stdAction; + } AccelItem; + + KAccel *_accel; + QPtrList<AccelItem> _accList; + QPtrList<QPopupMenu> _menuList; + + QPopupMenu *_menu; + + KAccelMenuWatch::AccelItem *newAccelItem( QPopupMenu *menu, + int itemId, AccelType type ); + + public: + /** + * KAccelMenuWatch Constructor + */ + KAccelMenuWatch( KAccel *accel, QObject *parent = 0 ); + + /** + * KAccelMenuWatch Destructor + */ + virtual ~KAccelMenuWatch() {} + + /** + * Set the menu on which connectAccel calls will operate. + * All subsequent calls to connectAccel will be associated + * with this menu. You can call this function any number of + * times, so multiple menus can be handled. + */ + void setMenu( QPopupMenu *menu ); + + /** + * Return the last menu set with KAccelMenuWatch::setMenu(QPopupMenu*), + * or 0 if none has been set. + */ + QPopupMenu *currentMenu() const { return _menu; } + + /** + * Connect the menu item identified to currentMenu()/id to + * the accelerator action. + */ + void connectAccel( int itemId, const char *action ); + + /** + * Same as above, but connects to standard accelerators. + */ + void connectAccel( int itemId, KStdAccel::StdAccel ); + + public slots: + /** + * Updates all menu accelerators. Call this after all accelerators + * have been connected or the kaccel object has been updated. + */ + void updateMenus(); + + private slots: + void removeDeadMenu(); + + private: + KAccelMenuWatch& operator=( const KAccelMenuWatch& ); + KAccelMenuWatch( const KAccelMenuWatch& ); +}; + +#endif // KARM_K_ACCEL_MENU_WATCH_H diff --git a/karm/karm.kdevelop b/karm/karm.kdevelop new file mode 100644 index 000000000..c64212df4 --- /dev/null +++ b/karm/karm.kdevelop @@ -0,0 +1,142 @@ +<?xml version = '1.0'?> +<kdevelop> + <general> + <author>root</author> + <email>root@duffman</email> + <version>$VERSION$</version> + <projectmanagement>KDevKDEAutoProject</projectmanagement> + <primarylanguage>C++</primarylanguage> + <keywords> + <keyword>Qt</keyword> + <keyword>KDE</keyword> + </keywords> + <projectdirectory>.</projectdirectory> + <absoluteprojectpath>false</absoluteprojectpath> + <description/> + <ignoreparts/> + </general> + <kdevfileview> + <groups> + <group pattern="*.cpp;*.cxx;*.h" name="Sources" /> + <group pattern="*.ui" name="User Interface" /> + <group pattern="*.png" name="Icons" /> + <group pattern="*.po;*.ts" name="Translations" /> + <group pattern="*" name="Others" /> + <hidenonprojectfiles>false</hidenonprojectfiles> + <hidenonlocation>false</hidenonlocation> + </groups> + <tree> + <hidepatterns>*.o,*.lo,CVS</hidepatterns> + <hidenonprojectfiles>false</hidenonprojectfiles> + </tree> + </kdevfileview> + <kdevdoctreeview> + <ignoretocs> + <toc>ada</toc> + <toc>ada_bugs_gcc</toc> + <toc>bash</toc> + <toc>bash_bugs</toc> + <toc>clanlib</toc> + <toc>fortran_bugs_gcc</toc> + <toc>gnome1</toc> + <toc>gnustep</toc> + <toc>gtk</toc> + <toc>gtk_bugs</toc> + <toc>haskell</toc> + <toc>haskell_bugs_ghc</toc> + <toc>java_bugs_gcc</toc> + <toc>java_bugs_sun</toc> + <toc>opengl</toc> + <toc>pascal_bugs_fp</toc> + <toc>php</toc> + <toc>php_bugs</toc> + <toc>perl</toc> + <toc>perl_bugs</toc> + <toc>python</toc> + <toc>python_bugs</toc> + <toc>ruby</toc> + <toc>ruby_bugs</toc> + <toc>sdl</toc> + <toc>stl</toc> + <toc>sw</toc> + <toc>w3c-dom-level2-html</toc> + <toc>w3c-svg</toc> + <toc>w3c-uaag10</toc> + <toc>wxwidgets_bugs</toc> + </ignoretocs> + <ignoreqt_xml> + <toc>qmake User Guide</toc> + </ignoreqt_xml> + </kdevdoctreeview> + <kdevdebugger> + <general> + <dbgshell>libtool</dbgshell> + <programargs/> + <gdbpath/> + <configGdbScript/> + <runShellScript/> + <runGdbScript/> + <breakonloadinglibs>true</breakonloadinglibs> + <separatetty>false</separatetty> + <floatingtoolbar>false</floatingtoolbar> + </general> + <display> + <staticmembers>false</staticmembers> + <demanglenames>true</demanglenames> + <outputradix>10</outputradix> + </display> + </kdevdebugger> + <kdevfilecreate> + <useglobaltypes> + <type ext="ui" /> + <type ext="cpp" /> + <type ext="h" /> + </useglobaltypes> + </kdevfilecreate> + <kdevautoproject> + <make> + <envvars> + <envvar value="1" name="WANT_AUTOCONF_2_5" /> + <envvar value="1" name="WANT_AUTOMAKE_1_6" /> + </envvars> + </make> + <run> + <directoryradio>executable</directoryradio> + <customdirectory>/</customdirectory> + <mainprogram>karm</mainprogram> + <programargs/> + <terminal>false</terminal> + <autocompile>true</autocompile> + <envvars/> + </run> + </kdevautoproject> + <cppsupportpart> + <filetemplates> + <interfacesuffix>.h</interfacesuffix> + <implementationsuffix>.cpp</implementationsuffix> + </filetemplates> + </cppsupportpart> + <kdevcppsupport> + <codecompletion> + <includeGlobalFunctions>true</includeGlobalFunctions> + <includeTypes>true</includeTypes> + <includeEnums>true</includeEnums> + <includeTypedefs>false</includeTypedefs> + <automaticCodeCompletion>true</automaticCodeCompletion> + <automaticArgumentsHint>true</automaticArgumentsHint> + <automaticHeaderCompletion>true</automaticHeaderCompletion> + <codeCompletionDelay>250</codeCompletionDelay> + <argumentsHintDelay>400</argumentsHintDelay> + <headerCompletionDelay>250</headerCompletionDelay> + </codecompletion> + <creategettersetter> + <prefixGet/> + <prefixSet>set</prefixSet> + <prefixVariable>m_,_</prefixVariable> + <parameterName>theValue</parameterName> + <inlineGet>true</inlineGet> + <inlineSet>true</inlineSet> + </creategettersetter> + <references/> + </kdevcppsupport> +</kdevelop> diff --git a/karm/karm_part.cpp b/karm/karm_part.cpp new file mode 100644 index 000000000..c7729f0fa --- /dev/null +++ b/karm/karm_part.cpp @@ -0,0 +1,703 @@ + +#include "kaccelmenuwatch.h" +#include "karm_part.h" +#include "karmerrors.h" +#include "task.h" +#include "preferences.h" +#include "tray.h" +#include "version.h" +#include <kaccel.h> + +#include <kinstance.h> +#include <kaction.h> +#include <kstdaction.h> +#include <kfiledialog.h> +#include <kglobal.h> +#include <klocale.h> + +#include <qfile.h> +#include <qtextstream.h> +#include <qmultilineedit.h> +#include <qpopupmenu.h> +#include "mainwindow.h" + +karmPart::karmPart( QWidget *parentWidget, const char *widgetName, + QObject *parent, const char *name ) + : DCOPObject ( "KarmDCOPIface" ), KParts::ReadWritePart(parent, name), + _accel ( new KAccel( parentWidget ) ), + _watcher ( new KAccelMenuWatch( _accel, parentWidget ) ) +{ + // we need an instance + setInstance( karmPartFactory::instance() ); + + // this should be your custom internal widget + _taskView = new TaskView( parentWidget, widgetName ); + + // setup PreferenceDialog. + _preferences = Preferences::instance(); + + // notify the part that this is our internal widget + setWidget(_taskView); + + // create our actions + KStdAction::open(this, SLOT(fileOpen()), actionCollection()); + KStdAction::saveAs(this, SLOT(fileSaveAs()), actionCollection()); + KStdAction::save(this, SLOT(save()), actionCollection()); + + makeMenus(); + + _watcher->updateMenus(); + + // connections + + connect( _taskView, SIGNAL( totalTimesChanged( long, long ) ), + this, SLOT( updateTime( long, long ) ) ); + connect( _taskView, SIGNAL( selectionChanged ( QListViewItem * )), + this, SLOT(slotSelectionChanged())); + connect( _taskView, SIGNAL( updateButtons() ), + this, SLOT(slotSelectionChanged())); + + // Setup context menu request handling + connect( _taskView, + SIGNAL( contextMenuRequested( QListViewItem*, const QPoint&, int )), + this, + SLOT( contextMenuRequest( QListViewItem*, const QPoint&, int ))); + + _tray = new KarmTray( this ); + + connect( _tray, SIGNAL( quitSelected() ), SLOT( quit() ) ); + + connect( _taskView, SIGNAL( timersActive() ), _tray, SLOT( startClock() ) ); + connect( _taskView, SIGNAL( timersActive() ), this, SLOT( enableStopAll() )); + connect( _taskView, SIGNAL( timersInactive() ), _tray, SLOT( stopClock() ) ); + connect( _taskView, SIGNAL( timersInactive() ), this, SLOT( disableStopAll())); + connect( _taskView, SIGNAL( tasksChanged( QPtrList<Task> ) ), + _tray, SLOT( updateToolTip( QPtrList<Task> ) )); + + _taskView->load(); + + // Everything that uses Preferences has been created now, we can let it + // emit its signals + _preferences->emitSignals(); + slotSelectionChanged(); + + // set our XML-UI resource file + setXMLFile("karmui.rc"); + + // we are read-write by default + setReadWrite(true); + + // we are not modified since we haven't done anything yet + setModified(false); +} + +karmPart::~karmPart() +{ +} + +void karmPart::slotSelectionChanged() +{ + Task* item= _taskView->current_item(); + actionDelete->setEnabled(item); + actionEdit->setEnabled(item); + actionStart->setEnabled(item && !item->isRunning() && !item->isComplete()); + actionStop->setEnabled(item && item->isRunning()); + actionMarkAsComplete->setEnabled(item && !item->isComplete()); + actionMarkAsIncomplete->setEnabled(item && item->isComplete()); +} + +void karmPart::makeMenus() +{ + KAction + *actionKeyBindings, + *actionNew, + *actionNewSub; + + (void) KStdAction::quit( this, SLOT( quit() ), actionCollection()); + (void) KStdAction::print( this, SLOT( print() ), actionCollection()); + actionKeyBindings = KStdAction::keyBindings( this, SLOT( keyBindings() ), + actionCollection() ); + actionPreferences = KStdAction::preferences(_preferences, + SLOT(showDialog()), + actionCollection() ); + (void) KStdAction::save( this, SLOT( save() ), actionCollection() ); + KAction* actionStartNewSession = new KAction( i18n("Start &New Session"), + 0, + this, + SLOT( startNewSession() ), + actionCollection(), + "start_new_session"); + KAction* actionResetAll = new KAction( i18n("&Reset All Times"), + 0, + this, + SLOT( resetAllTimes() ), + actionCollection(), + "reset_all_times"); + actionStart = new KAction( i18n("&Start"), + QString::fromLatin1("1rightarrow"), Key_S, + _taskView, + SLOT( startCurrentTimer() ), actionCollection(), + "start"); + actionStop = new KAction( i18n("S&top"), + QString::fromLatin1("stop"), 0, + _taskView, + SLOT( stopCurrentTimer() ), actionCollection(), + "stop"); + actionStopAll = new KAction( i18n("Stop &All Timers"), + Key_Escape, + _taskView, + SLOT( stopAllTimers() ), actionCollection(), + "stopAll"); + actionStopAll->setEnabled(false); + + actionNew = new KAction( i18n("&New..."), + QString::fromLatin1("filenew"), CTRL+Key_N, + _taskView, + SLOT( newTask() ), actionCollection(), + "new_task"); + actionNewSub = new KAction( i18n("New &Subtask..."), + QString::fromLatin1("kmultiple"), CTRL+ALT+Key_N, + _taskView, + SLOT( newSubTask() ), actionCollection(), + "new_sub_task"); + actionDelete = new KAction( i18n("&Delete"), + QString::fromLatin1("editdelete"), Key_Delete, + _taskView, + SLOT( deleteTask() ), actionCollection(), + "delete_task"); + actionEdit = new KAction( i18n("&Edit..."), + QString::fromLatin1("edit"), CTRL + Key_E, + _taskView, + SLOT( editTask() ), actionCollection(), + "edit_task"); +// actionAddComment = new KAction( i18n("&Add Comment..."), +// QString::fromLatin1("document"), +// CTRL+ALT+Key_E, +// _taskView, +// SLOT( addCommentToTask() ), +// actionCollection(), +// "add_comment_to_task"); + actionMarkAsComplete = new KAction( i18n("&Mark as Complete"), + QString::fromLatin1("document"), + CTRL+Key_M, + _taskView, + SLOT( markTaskAsComplete() ), + actionCollection(), + "mark_as_complete"); + actionMarkAsIncomplete = new KAction( i18n("&Mark as Incomplete"), + QString::fromLatin1("document"), + CTRL+Key_M, + _taskView, + SLOT( markTaskAsIncomplete() ), + actionCollection(), + "mark_as_incomplete"); + actionClipTotals = new KAction( i18n("&Copy Totals to Clipboard"), + QString::fromLatin1("klipper"), + CTRL+Key_C, + _taskView, + SLOT( clipTotals() ), + actionCollection(), + "clip_totals"); + actionClipHistory = new KAction( i18n("Copy &History to Clipboard"), + QString::fromLatin1("klipper"), + CTRL+ALT+Key_C, + _taskView, + SLOT( clipHistory() ), + actionCollection(), + "clip_history"); + + new KAction( i18n("Import &Legacy Flat File..."), 0, + _taskView, SLOT(loadFromFlatFile()), actionCollection(), + "import_flatfile"); + new KAction( i18n("&Export to CSV File..."), 0, + _taskView, SLOT(exportcsvFile()), actionCollection(), + "export_csvfile"); + new KAction( i18n("Export &History to CSV File..."), 0, + this, SLOT(exportcsvHistory()), actionCollection(), + "export_csvhistory"); + new KAction( i18n("Import Tasks From &Planner..."), 0, + _taskView, SLOT(importPlanner()), actionCollection(), + "import_planner"); + new KAction( i18n("Configure KArm..."), 0, + _preferences, SLOT(showDialog()), actionCollection(), + "configure_karm"); + +/* + new KAction( i18n("Import E&vents"), 0, + _taskView, + SLOT( loadFromKOrgEvents() ), actionCollection(), + "import_korg_events"); + */ + + // Tool tops must be set after the createGUI. + actionKeyBindings->setToolTip( i18n("Configure key bindings") ); + actionKeyBindings->setWhatsThis( i18n("This will let you configure key" + "bindings which is specific to karm") ); + + actionStartNewSession->setToolTip( i18n("Start a new session") ); + actionStartNewSession->setWhatsThis( i18n("This will reset the session time " + "to 0 for all tasks, to start a " + "new session, without affecting " + "the totals.") ); + actionResetAll->setToolTip( i18n("Reset all times") ); + actionResetAll->setWhatsThis( i18n("This will reset the session and total " + "time to 0 for all tasks, to restart from " + "scratch.") ); + + actionStart->setToolTip( i18n("Start timing for selected task") ); + actionStart->setWhatsThis( i18n("This will start timing for the selected " + "task.\n" + "It is even possible to time several tasks " + "simultaneously.\n\n" + "You may also start timing of a tasks by " + "double clicking the left mouse " + "button on a given task. This will, however, " + "stop timing of other tasks.")); + + actionStop->setToolTip( i18n("Stop timing of the selected task") ); + actionStop->setWhatsThis( i18n("Stop timing of the selected task") ); + + actionStopAll->setToolTip( i18n("Stop all of the active timers") ); + actionStopAll->setWhatsThis( i18n("Stop all of the active timers") ); + + actionNew->setToolTip( i18n("Create new top level task") ); + actionNew->setWhatsThis( i18n("This will create a new top level task.") ); + + actionDelete->setToolTip( i18n("Delete selected task") ); + actionDelete->setWhatsThis( i18n("This will delete the selected task and " + "all its subtasks.") ); + + actionEdit->setToolTip( i18n("Edit name or times for selected task") ); + actionEdit->setWhatsThis( i18n("This will bring up a dialog box where you " + "may edit the parameters for the selected " + "task.")); + //actionAddComment->setToolTip( i18n("Add a comment to a task") ); + //actionAddComment->setWhatsThis( i18n("This will bring up a dialog box where " + // "you can add a comment to a task. The " + // "comment can for instance add information on what you " + // "are currently doing. The comment will " + // "be logged in the log file.")); + actionClipTotals->setToolTip(i18n("Copy task totals to clipboard")); + actionClipHistory->setToolTip(i18n("Copy time card history to clipboard.")); + + slotSelectionChanged(); +} + +void karmPart::setReadWrite(bool rw) +{ + // notify your internal widget of the read-write state + if (rw) + connect(_taskView, SIGNAL(textChanged()), + this, SLOT(setModified())); + else + { + disconnect(_taskView, SIGNAL(textChanged()), + this, SLOT(setModified())); + } + + ReadWritePart::setReadWrite(rw); +} + +void karmPart::setModified(bool modified) +{ + // get a handle on our Save action and make sure it is valid + KAction *save = actionCollection()->action(KStdAction::stdName(KStdAction::Save)); + if (!save) + return; + + // if so, we either enable or disable it based on the current + // state + if (modified) + save->setEnabled(true); + else + save->setEnabled(false); + + // in any event, we want our parent to do it's thing + ReadWritePart::setModified(modified); +} + +bool karmPart::openFile() +{ + // m_file is always local so we can use QFile on it + _taskView->load(m_file); + + // just for fun, set the status bar + emit setStatusBarText( m_url.prettyURL() ); + + return true; +} + +bool karmPart::saveFile() +{ + // if we aren't read-write, return immediately + if (isReadWrite() == false) + return false; + + // m_file is always local, so we use QFile + QFile file(m_file); + if (file.open(IO_WriteOnly) == false) + return false; + + // use QTextStream to dump the text to the file + QTextStream stream(&file); + + file.close(); + + return true; +} + +void karmPart::fileOpen() +{ + // this slot is called whenever the File->Open menu is selected, + // the Open shortcut is pressed (usually CTRL+O) or the Open toolbar + // button is clicked + QString file_name = KFileDialog::getOpenFileName(); + + if (file_name.isEmpty() == false) + openURL(file_name); +} + +void karmPart::fileSaveAs() +{ + // this slot is called whenever the File->Save As menu is selected, + QString file_name = KFileDialog::getSaveFileName(); + if (file_name.isEmpty() == false) + saveAs(file_name); +} + + +// It's usually safe to leave the factory code alone.. with the +// notable exception of the KAboutData data +#include <kaboutdata.h> +#include <klocale.h> + +KInstance* karmPartFactory::s_instance = 0L; +KAboutData* karmPartFactory::s_about = 0L; + +karmPartFactory::karmPartFactory() + : KParts::Factory() +{ +} + +karmPartFactory::~karmPartFactory() +{ + delete s_instance; + delete s_about; + + s_instance = 0L; +} + +KParts::Part* karmPartFactory::createPartObject( QWidget *parentWidget, const char *widgetName, + QObject *parent, const char *name, + const char *classname, const QStringList &args ) +{ + // Create an instance of our Part + karmPart* obj = new karmPart( parentWidget, widgetName, parent, name ); + + // See if we are to be read-write or not + if (QCString(classname) == "KParts::ReadOnlyPart") + obj->setReadWrite(false); + + return obj; +} + +KInstance* karmPartFactory::instance() +{ + if( !s_instance ) + { + s_about = new KAboutData("karmpart", I18N_NOOP("karmPart"), "0.1"); + s_about->addAuthor("Thorsten Staerk", 0, "[email protected]"); + s_instance = new KInstance(s_about); + } + return s_instance; +} + +extern "C" +{ + KDE_EXPORT void* init_libkarmpart() + { + KGlobal::locale()->insertCatalogue("karm"); + return new karmPartFactory; + } +} + +void karmPart::contextMenuRequest( QListViewItem*, const QPoint& point, int ) +{ + QPopupMenu* pop = dynamic_cast<QPopupMenu*>( + factory()->container( i18n( "task_popup" ), this ) ); + if ( pop ) + pop->popup( point ); +} + +//---------------------------------------------------------------------------- +// +// D C O P I N T E R F A C E +// +//---------------------------------------------------------------------------- + +QString karmPart::version() const +{ + return KARM_VERSION; +} + +QString karmPart::deletetodo() +{ + _taskView->deleteTask(); + return ""; +} + +bool karmPart::getpromptdelete() +{ + return _preferences->promptDelete(); +} + +QString karmPart::setpromptdelete( bool prompt ) +{ + _preferences->setPromptDelete( prompt ); + return ""; +} + +QString karmPart::taskIdFromName( const QString &taskname ) const +{ + QString rval = ""; + + Task* task = _taskView->first_child(); + while ( rval.isEmpty() && task ) + { + rval = _hasTask( task, taskname ); + task = task->nextSibling(); + } + + return rval; +} + +void karmPart::quit() +{ + // TODO: write something for kapp->quit(); +} + +bool karmPart::save() +{ + kdDebug(5970) << "Saving time data to disk." << endl; + QString err=_taskView->save(); // untranslated error msg. + // TODO: + /* if (err.isEmpty()) statusBar()->message(i18n("Successfully saved tasks and history"),1807); + else statusBar()->message(i18n(err.ascii()),7707); // no msgbox since save is called when exiting */ + return true; +} + +int karmPart::addTask( const QString& taskname ) +{ + DesktopList desktopList; + QString uid = _taskView->addTask( taskname, 0, 0, desktopList ); + kdDebug(5970) << "MainWindow::addTask( " << taskname << " ) returns " << uid << endl; + if ( uid.length() > 0 ) return 0; + else + { + // We can't really tell what happened, b/c the resource framework only + // returns a boolean. + return KARM_ERR_GENERIC_SAVE_FAILED; + } +} + +QString karmPart::setPerCentComplete( const QString& taskName, int perCent ) +{ + int index = 0; + QString err="no such task"; + for (int i=0; i<_taskView->count(); i++) + { + if ((_taskView->item_at_index(i)->name()==taskName)) + { + index=i; + if (err==QString::null) err="task name is abigious"; + if (err=="no such task") err=QString::null; + } + } + if (err==QString::null) + { + _taskView->item_at_index(index)->setPercentComplete( perCent, _taskView->storage() ); + } + return err; +} + +int karmPart::bookTime +( const QString& taskId, const QString& datetime, long minutes ) +{ + int rval = 0; + QDate startDate; + QTime startTime; + QDateTime startDateTime; + Task *task, *t; + + if ( minutes <= 0 ) rval = KARM_ERR_INVALID_DURATION; + + // Find task + task = _taskView->first_child(); + t = NULL; + while ( !t && task ) + { + t = _hasUid( task, taskId ); + task = task->nextSibling(); + } + if ( t == NULL ) rval = KARM_ERR_UID_NOT_FOUND; + + // Parse datetime + if ( !rval ) + { + startDate = QDate::fromString( datetime, Qt::ISODate ); + if ( datetime.length() > 10 ) // "YYYY-MM-DD".length() = 10 + { + startTime = QTime::fromString( datetime, Qt::ISODate ); + } + else startTime = QTime( 12, 0 ); + if ( startDate.isValid() && startTime.isValid() ) + { + startDateTime = QDateTime( startDate, startTime ); + } + else rval = KARM_ERR_INVALID_DATE; + + } + + // Update task totals (session and total) and save to disk + if ( !rval ) + { + t->changeTotalTimes( t->sessionTime() + minutes, t->totalTime() + minutes ); + if ( ! _taskView->storage()->bookTime( t, startDateTime, minutes * 60 ) ) + { + rval = KARM_ERR_GENERIC_SAVE_FAILED; + } + } + + return rval; +} + +// There was something really bad going on with DCOP when I used a particular +// argument name; if I recall correctly, the argument name was errno. +QString karmPart::getError( int mkb ) const +{ + if ( mkb <= KARM_MAX_ERROR_NO ) return m_error[ mkb ]; + else return i18n( "Invalid error number: %1" ).arg( mkb ); +} + +int karmPart::totalMinutesForTaskId( const QString& taskId ) +{ + int rval = 0; + Task *task, *t; + + kdDebug(5970) << "MainWindow::totalTimeForTask( " << taskId << " )" << endl; + + // Find task + task = _taskView->first_child(); + t = NULL; + while ( !t && task ) + { + t = _hasUid( task, taskId ); + task = task->nextSibling(); + } + if ( t != NULL ) + { + rval = t->totalTime(); + kdDebug(5970) << "MainWindow::totalTimeForTask - task found: rval = " << rval << endl; + } + else + { + kdDebug(5970) << "MainWindow::totalTimeForTask - task not found" << endl; + rval = KARM_ERR_UID_NOT_FOUND; + } + + return rval; +} + +QString karmPart::_hasTask( Task* task, const QString &taskname ) const +{ + QString rval = ""; + if ( task->name() == taskname ) + { + rval = task->uid(); + } + else + { + Task* nexttask = task->firstChild(); + while ( rval.isEmpty() && nexttask ) + { + rval = _hasTask( nexttask, taskname ); + nexttask = nexttask->nextSibling(); + } + } + return rval; +} + +Task* karmPart::_hasUid( Task* task, const QString &uid ) const +{ + Task *rval = NULL; + + //kdDebug(5970) << "MainWindow::_hasUid( " << task << ", " << uid << " )" << endl; + + if ( task->uid() == uid ) rval = task; + else + { + Task* nexttask = task->firstChild(); + while ( !rval && nexttask ) + { + rval = _hasUid( nexttask, uid ); + nexttask = nexttask->nextSibling(); + } + } + return rval; +} + +QString karmPart::starttimerfor( const QString& taskname ) +{ + QString err="no such task"; + for (int i=0; i<_taskView->count(); i++) + { + if ((_taskView->item_at_index(i)->name()==taskname)) + { + _taskView->startTimerFor( _taskView->item_at_index(i) ); + err=""; + } + } + return err; +} + +QString karmPart::stoptimerfor( const QString& taskname ) +{ + QString err="no such task"; + for (int i=0; i<_taskView->count(); i++) + { + if ((_taskView->item_at_index(i)->name()==taskname)) + { + _taskView->stopTimerFor( _taskView->item_at_index(i) ); + err=""; + } + } + return err; +} + +QString karmPart::exportcsvfile( QString filename, QString from, QString to, int type, bool decimalMinutes, bool allTasks, QString delimiter, QString quote ) +{ + ReportCriteria rc; + rc.allTasks=allTasks; + rc.decimalMinutes=decimalMinutes; + rc.delimiter=delimiter; + rc.from=QDate::fromString( from ); + rc.quote=quote; + rc.reportType=(ReportCriteria::REPORTTYPE) type; + rc.to=QDate::fromString( to ); + rc.url=filename; + return _taskView->report( rc ); +} + +QString karmPart::importplannerfile( QString fileName ) +{ + return _taskView->importPlanner(fileName); +} + +void karmPart::startNewSession() +{ + _taskView->startNewSession(); + _taskView->save(); +} + +#include <qpopupmenu.h> +#include "karm_part.moc" diff --git a/karm/karm_part.desktop b/karm/karm_part.desktop new file mode 100644 index 000000000..566678597 --- /dev/null +++ b/karm/karm_part.desktop @@ -0,0 +1,16 @@ +[Desktop Entry] +Name=karmPart +Name[cs]=karm komponenta +Name[de]=KArm-Komponente +Name[et]=KArmi komponent +Name[fr]=Composant KArm +Name[is]=karm hluti +Name[nds]=karm-Komponent +Name[pt]=Componente KArm +Name[pt_BR]=Componente do KArm +Name[sv]=Karm-delprogram +Name[tr]=karmpart +MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++; +ServiceTypes=KParts/ReadOnlyPart,KParts/ReadWritePart +X-KDE-Library=libkarmpart +Type=Service diff --git a/karm/karm_part.h b/karm/karm_part.h new file mode 100644 index 000000000..85c1e99f7 --- /dev/null +++ b/karm/karm_part.h @@ -0,0 +1,137 @@ +#ifndef _KARMPART_H_ +#define _KARMPART_H_ + +#include <kparts/part.h> +#include "karmerrors.h" +#include <kparts/factory.h> +#include <karmdcopiface.h> +#include "reportcriteria.h" +#include <qlistview.h> + +class KAccel; +class KAccelMenuWatch; +class KarmTray; +class QWidget; +class QPainter; +class KURL; + +class Preferences; +class Task; +class TaskView; + +/** + * This is a "Part". It that does all the real work in a KPart + * application. + * + * @short Main Part + * @author Thorsten Staerk <kde at staerk dot de> + * @version 0.1 + */ +class karmPart : public KParts::ReadWritePart, virtual public KarmDCOPIface +{ + Q_OBJECT + + private: + void makeMenus(); + QString _hastodo( Task* task, const QString &taskname ) const; + QString _hasTask( Task* task, const QString &taskname ) const; + Task* _hasUid( Task* task, const QString &uid ) const; + + KAccel* _accel; + KAccelMenuWatch* _watcher; + TaskView* _taskView; + Preferences* _preferences; + KarmTray* _tray; + KAction* actionStart; + KAction* actionStop; + KAction* actionStopAll; + KAction* actionDelete; + KAction* actionEdit; +// KAction* actionAddComment; + KAction* actionMarkAsComplete; + KAction* actionMarkAsIncomplete; + KAction* actionPreferences; + KAction* actionClipTotals; + KAction* actionClipHistory; + QString m_error[ KARM_MAX_ERROR_NO + 1 ]; + + friend class KarmTray; + +public: + karmPart(QWidget *parentWidget, const char *widgetName, + QObject *parent, const char *name); + // DCOP + void quit(); + virtual bool save(); + QString version() const; + QString taskIdFromName( const QString &taskName ) const; + /** @reimp from KarmDCOPIface */ + int addTask( const QString &taskName ); + /** @reimp from KarmDCOPIface */ + QString setPerCentComplete( const QString& taskName, int PerCent ); + /** @reimp from KarmDCOPIface */ + int bookTime( const QString& taskId, const QString& iso8601StartDateTime, long durationInMinutes ); + /** @reimp from KarmDCOPIface */ + QString getError( int karmErrorNumber ) const; + int totalMinutesForTaskId( const QString& taskId ); + QString starttimerfor( const QString &taskname ); + QString stoptimerfor( const QString &taskname ); + QString deletetodo(); + bool getpromptdelete(); + QString setpromptdelete( bool prompt ); + QString exportcsvfile( QString filename, QString from, QString to, int type = 0, bool decimalMinutes=true, bool allTasks=true, QString delimiter="r", QString quote="q" ); + QString importplannerfile( QString filename ); + + virtual ~karmPart(); + + /** + * This is a virtual function inherited from KParts::ReadWritePart. + * A shell will use this to inform this Part if it should act + * read-only + */ + virtual void setReadWrite(bool rw); + + /** + * Reimplemented to disable and enable Save action + */ + virtual void setModified(bool modified); + +protected: + /** + * This must be implemented by each part + */ + virtual bool openFile(); + + /** + * This must be implemented by each read-write part + */ + virtual bool saveFile(); + +protected slots: + void contextMenuRequest( QListViewItem*, const QPoint& point, int ); + void fileOpen(); + void fileSaveAs(); + void slotSelectionChanged(); + void startNewSession(); +}; + +class KInstance; +class KAboutData; + +class karmPartFactory : public KParts::Factory +{ + Q_OBJECT +public: + karmPartFactory(); + virtual ~karmPartFactory(); + virtual KParts::Part* createPartObject( QWidget *parentWidget, const char *widgetName, + QObject *parent, const char *name, + const char *classname, const QStringList &args ); + static KInstance* instance(); + +private: + static KInstance* s_instance; + static KAboutData* s_about; +}; + +#endif // _KARMPART_H_ diff --git a/karm/karmdcopiface.h b/karm/karmdcopiface.h new file mode 100644 index 000000000..1135c81e6 --- /dev/null +++ b/karm/karmdcopiface.h @@ -0,0 +1,132 @@ +/* + * This file only: + * Copyright (C) 2004 Mark Bucciarelli <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + */ +#ifndef KARM_DCOP_IFAC_H +#define KARM_DCOP_IFAC_H + +#include <dcopobject.h> + +/** Define DCOP interface to karm. Methods implemented in MainWindow */ +class KarmDCOPIface : virtual public DCOPObject +{ + K_DCOP + k_dcop: + + /** Return karm version. */ + virtual QString version() const = 0; + + /** Return id of task found, empty string if no match. */ + virtual QString taskIdFromName( const QString& taskName ) const = 0; + + /** + * Add a new top-level task. + * + * A top-level task is one that has no parent tasks. + * + * @param taskName Name of new task. + * + * @return 0 on success, error number on failure. + */ + virtual int addTask( const QString& taskName ) = 0; + + /** + * Set percent complete to a task + * + * @param taskName Name of new task. + * @param perCent percent, e.g. 99 + * + * @return "" on success, error msg on failure. + */ + virtual QString setPerCentComplete( const QString& taskName, int perCent ) = 0; + + /** + * Add time to a task. + * + * The GUI will be non-responsive until this method returns. + * + * @return 0 on success, error number on failure. + * + * @param taskId Unique ID of task to add time to + * + * @param iso8601StartDateTime Date and time the booking starts, in extended + * ISO-8601 format; for example, YYYY-MM-DDTHH:MI:SS format (see + * Qt::ISODate). No timezone support--time is interpreted as the local time. + * If just the date is passed in (i.e., YYYY-MM-DD) , then the time is set to + * noon. + * + * @param durationInMinutes The amount of time to book against the taskId. + * + */ + virtual int bookTime( const QString& taskId, const QString& iso8601StartDateTime, + long durationInMinutes ) = 0; + + /** + * Return error string associated with karm error number. + * + * @param karmErrorNumber An integer error number. + * + * @return String associated with error number. These strings are + * internationalized. An unknown error number produces an empty string as + * the return value. + * + */ + virtual QString getError( int karmErrorNumber ) const = 0; + + /** + * Total time currently associated with a task. + * + * A task has two counters: the total session time and the total time. Note + * that th euser can reset both counters. + * + * @param taskId Unique ID of task to lookup bookings for. + */ + virtual int totalMinutesForTaskId( const QString& taskId ) = 0; + + /** Start timer for all tasks with the summary taskname. */ + // may conflict with unitaskmode + virtual QString starttimerfor( const QString& taskname ) = 0; + + /** Stop timer for all tasks with the summary taskname. */ + // may conflict with unitaskmode + virtual QString stoptimerfor( const QString& taskname ) = 0; + + /** delete the current item */ + virtual QString deletetodo() = 0; + + /** set if prompted on deleting a task */ + virtual QString setpromptdelete( bool prompt ) = 0; + + /** get if prompted on deleting a task */ + virtual bool getpromptdelete() = 0; + + /** export csv history or totals file */ + virtual QString exportcsvfile( QString filename, QString from, QString to, int type = 0, bool decimalMinutes=true, bool allTasks=true, QString delimiter=";", QString quote="'" ) = 0; + + /** import planner project file */ + virtual QString importplannerfile( QString filename ) = 0; + + /** save your tasks */ + virtual bool save() = 0; + + /** Graceful shutdown. */ + virtual void quit() = 0; +}; + +#endif // KARM_DCOP_IFAC_H diff --git a/karm/karmerrors.h b/karm/karmerrors.h new file mode 100644 index 000000000..8962be37d --- /dev/null +++ b/karm/karmerrors.h @@ -0,0 +1,39 @@ +/* + * This file only: + * Copyright (C) 2005 Mark Bucciarelli <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ +#ifndef KARM_ERRORS_H +#define KARM_ERRORS_H + +enum KARM_Errors { + KARM_ERR_GENERIC_SAVE_FAILED = 1, + KARM_ERR_COULD_NOT_MODIFY_RESOURCE, + KARM_ERR_MEMORY_EXHAUSTED, + KARM_ERR_UID_NOT_FOUND, + KARM_ERR_INVALID_DATE, + KARM_ERR_INVALID_TIME, + KARM_ERR_INVALID_DURATION, + + KARM_MAX_ERROR_NO = KARM_ERR_INVALID_DURATION +}; + +#endif // KARM_ERRORS_H + + diff --git a/karm/karmstorage.cpp b/karm/karmstorage.cpp new file mode 100644 index 000000000..7e150d057 --- /dev/null +++ b/karm/karmstorage.cpp @@ -0,0 +1,1241 @@ +/* + * This file only: + * Copyright (C) 2003, 2004 Mark Bucciarelli <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> + +#include <cassert> + +#include <qfile.h> +#include <qsize.h> +#include <qdict.h> +#include <qdatetime.h> +#include <qstring.h> +#include <qstringlist.h> + +#include "incidence.h" +#include "kapplication.h" // kapp +#include <kdebug.h> +#include <kemailsettings.h> +#include <klocale.h> // i18n +#include <kmessagebox.h> +#include <kprogress.h> +#include <ktempfile.h> +#include <resourcecalendar.h> +#include <resourcelocal.h> +#include <resourceremote.h> +#include <kpimprefs.h> +#include <taskview.h> +#include <timekard.h> +#include <karmutility.h> +#include <kio/netaccess.h> +#include <kurl.h> +#include <vector> + +//#include <calendarlocal.h> +//#include <journal.h> +//#include <event.h> +//#include <todo.h> + +#include "karmstorage.h" +#include "preferences.h" +#include "task.h" +#include "reportcriteria.h" + +using namespace std; + +KarmStorage *KarmStorage::_instance = 0; +static long linenr; // how many lines written by printTaskHistory so far + + +KarmStorage *KarmStorage::instance() +{ + if (_instance == 0) _instance = new KarmStorage(); + return _instance; +} + +KarmStorage::KarmStorage() +{ + _calendar = 0; +} + +QString KarmStorage::load (TaskView* view, const Preferences* preferences, QString fileName ) +// loads data from filename into view. If no filename is given, filename from preferences is used. +// filename might be of use if this program is run as embedded konqueror plugin. +{ + // When I tried raising an exception from this method, the compiler + // complained that exceptions are not allowed. Not sure how apps + // typically handle error conditions in KDE, but I'll return the error + // as a string (empty is no error). -- Mark, Aug 8, 2003 + + // Use KDE_CXXFLAGS=$(USE_EXCEPTIONS) in Makefile.am if you want to use + // exceptions (David Faure) + + QString err; + KEMailSettings settings; + if ( fileName.isEmpty() ) fileName = preferences->iCalFile(); + + // If same file, don't reload + if ( fileName == _icalfile ) return err; + + + // If file doesn't exist, create a blank one to avoid ResourceLocal load + // error. We make it user and group read/write, others read. This is + // masked by the users umask. (See man creat) + if ( ! remoteResource( _icalfile ) ) + { + int handle; + handle = open ( + QFile::encodeName( fileName ), + O_CREAT|O_EXCL|O_WRONLY, + S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH + ); + if (handle != -1) close(handle); + } + + if ( _calendar) + closeStorage(view); + + // Create local file resource and add to resources + _icalfile = fileName; + + KCal::ResourceCached *resource; + if ( remoteResource( _icalfile ) ) + { + KURL url( _icalfile ); + resource = new KCal::ResourceRemote( url, url ); // same url for upload and download + } + else + { + resource = new KCal::ResourceLocal( _icalfile ); + } + _calendar = resource; + + QObject::connect (_calendar, SIGNAL(resourceChanged(ResourceCalendar *)), + view, SLOT(iCalFileModified(ResourceCalendar *))); + _calendar->setTimeZoneId( KPimPrefs::timezone() ); + _calendar->setResourceName( QString::fromLatin1("KArm") ); + _calendar->open(); + _calendar->load(); + + // Claim ownership of iCalendar file if no one else has. + KCal::Person owner = resource->getOwner(); + if ( owner.isEmpty() ) + { + resource->setOwner( KCal::Person( + settings.getSetting( KEMailSettings::RealName ), + settings.getSetting( KEMailSettings::EmailAddress ) ) ); + } + + // Build task view from iCal data + if (!err) + { + KCal::Todo::List todoList; + KCal::Todo::List::ConstIterator todo; + QDict< Task > map; + + // Build dictionary to look up Task object from Todo uid. Each task is a + // QListViewItem, and is initially added with the view as the parent. + todoList = _calendar->rawTodos(); + kdDebug(5970) << "KarmStorage::load " + << "rawTodo count (includes completed todos) =" + << todoList.count() << endl; + for( todo = todoList.begin(); todo != todoList.end(); ++todo ) + { + // Initially, if a task was complete, it was removed from the view. + // However, this increased the complexity of reporting on task history. + // + // For example, if a task is complete yet has time logged to it during + // the date range specified on the history report, we have to figure out + // how that task fits into the task hierarchy. Currently, this + // structure is held in memory by the structure in the list view. + // + // I considered creating a second tree that held the full structure of + // all complete and incomplete tasks. But this seemed to much of a + // change with an impending beta release and a full todo list. + // + // Hence this "solution". Include completed tasks, but mark them as + // inactive in the view. + // + //if ((*todo)->isCompleted()) continue; + + Task* task = new Task(*todo, view); + map.insert( (*todo)->uid(), task ); + view->setRootIsDecorated(true); + task->setPixmapProgress(); + } + + // Load each task under it's parent task. + for( todo = todoList.begin(); todo != todoList.end(); ++todo ) + { + Task* task = map.find( (*todo)->uid() ); + + // No relatedTo incident just means this is a top-level task. + if ( (*todo)->relatedTo() ) + { + Task* newParent = map.find( (*todo)->relatedToUid() ); + + // Complete the loading but return a message + if ( !newParent ) + err = i18n("Error loading \"%1\": could not find parent (uid=%2)") + .arg(task->name()) + .arg((*todo)->relatedToUid()); + + if (!err) task->move( newParent); + } + } + + kdDebug(5970) << "KarmStorage::load - loaded " << view->count() + << " tasks from " << _icalfile << endl; + } + + return err; +} + +QString KarmStorage::icalfile() +{ + kdDebug(5970) << "Entering KarmStorage::icalfile" << endl; + return _icalfile; +} + +QString KarmStorage::buildTaskView(KCal::ResourceCalendar *rc, TaskView *view) +// makes *view contain the tasks out of *rc. +{ + QString err; + KCal::Todo::List todoList; + KCal::Todo::List::ConstIterator todo; + QDict< Task > map; + vector<QString> runningTasks; + vector<QDateTime> startTimes; + + // remember tasks that are running and their start times + for ( int i=0; i<view->count(); i++) + { + if ( view->item_at_index(i)->isRunning() ) + { + runningTasks.push_back( view->item_at_index(i)->uid() ); + startTimes.push_back( view->item_at_index(i)->lastStart() ); + } + } + + //view->stopAllTimers(); + // delete old tasks + while (view->item_at_index(0)) view->item_at_index(0)->cut(); + + // 1. insert tasks form rc into taskview + // 1.1. Build dictionary to look up Task object from Todo uid. Each task is a + // QListViewItem, and is initially added with the view as the parent. + todoList = rc->rawTodos(); + for( todo = todoList.begin(); todo != todoList.end(); ++todo ) + { + Task* task = new Task(*todo, view); + map.insert( (*todo)->uid(), task ); + view->setRootIsDecorated(true); + task->setPixmapProgress(); + } + + // 1.1. Load each task under it's parent task. + for( todo = todoList.begin(); todo != todoList.end(); ++todo ) + { + Task* task = map.find( (*todo)->uid() ); + + // No relatedTo incident just means this is a top-level task. + if ( (*todo)->relatedTo() ) + { + Task* newParent = map.find( (*todo)->relatedToUid() ); + + // Complete the loading but return a message + if ( !newParent ) + err = i18n("Error loading \"%1\": could not find parent (uid=%2)") + .arg(task->name()) + .arg((*todo)->relatedToUid()); + + if (!err) task->move( newParent); + } + } + + view->clearActiveTasks(); + // restart tasks that have been running with their start times + for ( int i=0; i<view->count(); i++) + { + for ( unsigned int n=0; n<runningTasks.size(); n++) + { + if ( runningTasks[n] == view->item_at_index(i)->uid() ) + { + view->startTimerFor( view->item_at_index(i), startTimes[n] ); + } + } + } + + view->refresh(); + + return err; +} + +void KarmStorage::closeStorage(TaskView* view) +{ + if ( _calendar ) + { + _calendar->close(); + delete _calendar; + _calendar = 0; + + view->clear(); + } +} + +QString KarmStorage::save(TaskView* taskview) +{ + kdDebug(5970) << "entering KarmStorage::save" << endl; + QString err=QString(); + + QPtrStack< KCal::Todo > parents; + + for (Task* task=taskview->first_child(); task; task = task->nextSibling()) + { + err=writeTaskAsTodo(task, 1, parents ); + } + + if ( !saveCalendar() ) + { + err="Could not save"; + } + + if ( err.isEmpty() ) + { + kdDebug(5970) + << "KarmStorage::save : wrote " + << taskview->count() << " tasks to " << _icalfile << endl; + } + else + { + kdWarning(5970) << "KarmStorage::save : " << err << endl; + } + + return err; +} + +QString KarmStorage::writeTaskAsTodo(Task* task, const int level, + QPtrStack< KCal::Todo >& parents ) +{ + QString err; + KCal::Todo* todo; + + todo = _calendar->todo(task->uid()); + if ( !todo ) + { + kdDebug(5970) << "Could not get todo from calendar" << endl; + return "Could not get todo from calendar"; + } + task->asTodo(todo); + if ( !parents.isEmpty() ) todo->setRelatedTo( parents.top() ); + parents.push( todo ); + + for ( Task* nextTask = task->firstChild(); nextTask; + nextTask = nextTask->nextSibling() ) + { + err = writeTaskAsTodo(nextTask, level+1, parents ); + } + + parents.pop(); + return err; +} + +bool KarmStorage::isEmpty() +{ + KCal::Todo::List todoList; + + todoList = _calendar->rawTodos(); + return todoList.empty(); +} + +bool KarmStorage::isNewStorage(const Preferences* preferences) const +{ + if ( !_icalfile.isNull() ) return preferences->iCalFile() != _icalfile; + else return false; +} + +//---------------------------------------------------------------------------- +// Routines that handle legacy flat file format. +// These only stored total and session times. +// + +QString KarmStorage::loadFromFlatFile(TaskView* taskview, + const QString& filename) +{ + QString err; + + kdDebug(5970) + << "KarmStorage::loadFromFlatFile: " << filename << endl; + + QFile f(filename); + if( !f.exists() ) + err = i18n("File \"%1\" not found.").arg(filename); + + if (!err) + { + if( !f.open( IO_ReadOnly ) ) + err = i18n("Could not open \"%1\".").arg(filename); + } + + if (!err) + { + + QString line; + + QPtrStack<Task> stack; + Task *task; + + QTextStream stream(&f); + + while( !stream.atEnd() ) { + // lukas: this breaks for non-latin1 chars!!! + // if ( file.readLine( line, T_LINESIZE ) == 0 ) + // break; + + line = stream.readLine(); + kdDebug(5970) << "DEBUG: line: " << line << "\n"; + + if (line.isNull()) + break; + + long minutes; + int level; + QString name; + DesktopList desktopList; + if (!parseLine(line, &minutes, &name, &level, &desktopList)) + continue; + + unsigned int stackLevel = stack.count(); + for (unsigned int i = level; i<=stackLevel ; i++) { + stack.pop(); + } + + if (level == 1) { + kdDebug(5970) << "KarmStorage::loadFromFlatFile - toplevel task: " + << name << " min: " << minutes << "\n"; + task = new Task(name, minutes, 0, desktopList, taskview); + task->setUid(addTask(task, 0)); + } + else { + Task *parent = stack.top(); + kdDebug(5970) << "KarmStorage::loadFromFlatFile - task: " << name + << " min: " << minutes << " parent" << parent->name() << "\n"; + task = new Task(name, minutes, 0, desktopList, parent); + + task->setUid(addTask(task, parent)); + + // Legacy File Format (!): + parent->changeTimes(0, -minutes); + taskview->setRootIsDecorated(true); + parent->setOpen(true); + } + if (!task->uid().isNull()) + stack.push(task); + else + delete task; + } + + f.close(); + + } + + return err; +} + +QString KarmStorage::loadFromFlatFileCumulative(TaskView* taskview, + const QString& filename) +{ + QString err = loadFromFlatFile(taskview, filename); + if (!err) + { + for (Task* task = taskview->first_child(); task; + task = task->nextSibling()) + { + adjustFromLegacyFileFormat(task); + } + } + return err; +} + +bool KarmStorage::parseLine(QString line, long *time, QString *name, + int *level, DesktopList* desktopList) +{ + if (line.find('#') == 0) { + // A comment line + return false; + } + + int index = line.find('\t'); + if (index == -1) { + // This doesn't seem like a valid record + return false; + } + + QString levelStr = line.left(index); + QString rest = line.remove(0,index+1); + + index = rest.find('\t'); + if (index == -1) { + // This doesn't seem like a valid record + return false; + } + + QString timeStr = rest.left(index); + rest = rest.remove(0,index+1); + + bool ok; + + index = rest.find('\t'); // check for optional desktops string + if (index >= 0) { + *name = rest.left(index); + QString deskLine = rest.remove(0,index+1); + + // now transform the ds string (e.g. "3", or "1,4,5") into + // an DesktopList + QString ds; + int d; + int commaIdx = deskLine.find(','); + while (commaIdx >= 0) { + ds = deskLine.left(commaIdx); + d = ds.toInt(&ok); + if (!ok) + return false; + + desktopList->push_back(d); + deskLine.remove(0,commaIdx+1); + commaIdx = deskLine.find(','); + } + + d = deskLine.toInt(&ok); + + if (!ok) + return false; + + desktopList->push_back(d); + } + else { + *name = rest.remove(0,index+1); + } + + *time = timeStr.toLong(&ok); + + if (!ok) { + // the time field was not a number + return false; + } + *level = levelStr.toInt(&ok); + if (!ok) { + // the time field was not a number + return false; + } + + return true; +} + +void KarmStorage::adjustFromLegacyFileFormat(Task* task) +{ + // unless the parent is the listView + if ( task->parent() ) + task->parent()->changeTimes(-task->sessionTime(), -task->time()); + + // traverse depth first - + // as soon as we're in a leaf, we'll substract it's time from the parent + // then, while descending back we'll do the same for each node untill + // we reach the root + for ( Task* subtask = task->firstChild(); subtask; + subtask = subtask->nextSibling() ) + adjustFromLegacyFileFormat(subtask); +} + +//---------------------------------------------------------------------------- +// Routines that handle Comma-Separated Values export file format. +// +QString KarmStorage::exportcsvFile( TaskView *taskview, + const ReportCriteria &rc ) +{ + QString delim = rc.delimiter; + QString dquote = rc.quote; + QString double_dquote = dquote + dquote; + bool to_quote = true; + + QString err; + Task* task; + int maxdepth=0; + + kdDebug(5970) + << "KarmStorage::exportcsvFile: " << rc.url << endl; + + QString title = i18n("Export Progress"); + KProgressDialog dialog( taskview, 0, title ); + dialog.setAutoClose( true ); + dialog.setAllowCancel( true ); + dialog.progressBar()->setTotalSteps( 2 * taskview->count() ); + + // The default dialog was not displaying all the text in the title bar. + int width = taskview->fontMetrics().width(title) * 3; + QSize dialogsize; + dialogsize.setWidth(width); + dialog.setInitialSize( dialogsize, true ); + + if ( taskview->count() > 1 ) dialog.show(); + + QString retval; + + // Find max task depth + int tasknr = 0; + while ( tasknr < taskview->count() && !dialog.wasCancelled() ) + { + dialog.progressBar()->advance( 1 ); + if ( tasknr % 15 == 0 ) kapp->processEvents(); // repainting is slow + if ( taskview->item_at_index(tasknr)->depth() > maxdepth ) + maxdepth = taskview->item_at_index(tasknr)->depth(); + tasknr++; + } + + // Export to file + tasknr = 0; + while ( tasknr < taskview->count() && !dialog.wasCancelled() ) + { + task = taskview->item_at_index( tasknr ); + dialog.progressBar()->advance( 1 ); + if ( tasknr % 15 == 0 ) kapp->processEvents(); + + // indent the task in the csv-file: + for ( int i=0; i < task->depth(); ++i ) retval += delim; + + /* + // CSV compliance + // Surround the field with quotes if the field contains + // a comma (delim) or a double quote + if (task->name().contains(delim) || task->name().contains(dquote)) + to_quote = true; + else + to_quote = false; + */ + to_quote = true; + + if (to_quote) + retval += dquote; + + // Double quotes replaced by a pair of consecutive double quotes + retval += task->name().replace( dquote, double_dquote ); + + if (to_quote) + retval += dquote; + + // maybe other tasks are more indented, so to align the columns: + for ( int i = 0; i < maxdepth - task->depth(); ++i ) retval += delim; + + retval += delim + formatTime( task->sessionTime(), + rc.decimalMinutes ) + + delim + formatTime( task->time(), + rc.decimalMinutes ) + + delim + formatTime( task->totalSessionTime(), + rc.decimalMinutes ) + + delim + formatTime( task->totalTime(), + rc.decimalMinutes ) + + "\n"; + tasknr++; + } + + // save, either locally or remote + if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/"))) + { + QString filename=rc.url.path(); + if (filename.isEmpty()) filename=rc.url.url(); + QFile f( filename ); + if( !f.open( IO_WriteOnly ) ) { + err = i18n( "Could not open \"%1\"." ).arg( filename ); + } + if (!err) + { + QTextStream stream(&f); + // Export to file + stream << retval; + f.close(); + } + } + else // use remote file + { + KTempFile tmpFile; + if ( tmpFile.status() != 0 ) err = QString::fromLatin1( "Unable to get temporary file" ); + else + { + QTextStream *stream=tmpFile.textStream(); + *stream << retval; + tmpFile.close(); + if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=QString::fromLatin1("Could not upload"); + } + } + + return err; +} + +//---------------------------------------------------------------------------- +// Routines that handle logging KArm history +// + +// +// public routines: +// + +QString KarmStorage::addTask(const Task* task, const Task* parent) +{ + KCal::Todo* todo; + QString uid; + + todo = new KCal::Todo(); + if ( _calendar->addTodo( todo ) ) + { + task->asTodo( todo ); + if (parent) + todo->setRelatedTo(_calendar->todo(parent->uid())); + uid = todo->uid(); + } + else + { + // Most likely a lock could not be pulled, although there are other + // possiblities (like a really confused resource manager). + uid = ""; + } + + return uid; +} + +bool KarmStorage::removeTask(Task* task) +{ + + // delete history + KCal::Event::List eventList = _calendar->rawEvents(); + for(KCal::Event::List::iterator i = eventList.begin(); + i != eventList.end(); + ++i) + { + //kdDebug(5970) << "KarmStorage::removeTask: " + // << (*i)->uid() << " - relatedToUid() " + // << (*i)->relatedToUid() + // << ", relatedTo() = " << (*i)->relatedTo() <<endl; + if ( (*i)->relatedToUid() == task->uid() + || ( (*i)->relatedTo() + && (*i)->relatedTo()->uid() == task->uid())) + { + _calendar->deleteEvent(*i); + } + } + + // delete todo + KCal::Todo *todo = _calendar->todo(task->uid()); + _calendar->deleteTodo(todo); + + // Save entire file + saveCalendar(); + + return true; +} + +void KarmStorage::addComment(const Task* task, const QString& comment) +{ + + KCal::Todo* todo; + + todo = _calendar->todo(task->uid()); + + // Do this to avoid compiler warnings about comment not being used. once we + // transition to using the addComment method, we need this second param. + QString s = comment; + + // TODO: Use libkcal comments + // todo->addComment(comment); + // temporary + todo->setDescription(task->comment()); + + saveCalendar(); +} + +long KarmStorage::printTaskHistory ( + const Task *task, + const QMap<QString,long> &taskdaytotals, + QMap<QString,long> &daytotals, + const QDate &from, + const QDate &to, + const int level, + vector <QString> &matrix, + const ReportCriteria &rc) +// to>=from is precondition +{ + long ownline=linenr++; // the how many-th instance of this function is this + long colrectot=0; // colum where to write the task's total recursive time + vector <QString> cell; // each line of the matrix is stored in an array of cells, one containing the recursive total + long add; // total recursive time of all subtasks + QString delim = rc.delimiter; + QString dquote = rc.quote; + QString double_dquote = dquote + dquote; + bool to_quote = true; + + const QString cr = QString::fromLatin1("\n"); + QString buf; + QString daytaskkey, daykey; + QDate day; + long sum; + + if ( !task ) return 0; + + day = from; + sum = 0; + while (day <= to) + { + // write the time in seconds for the given task for the given day to s + daykey = day.toString(QString::fromLatin1("yyyyMMdd")); + daytaskkey = QString::fromLatin1("%1_%2") + .arg(daykey) + .arg(task->uid()); + + if (taskdaytotals.contains(daytaskkey)) + { + cell.push_back(QString::fromLatin1("%1") + .arg(formatTime(taskdaytotals[daytaskkey]/60, rc.decimalMinutes))); + sum += taskdaytotals[daytaskkey]; // in seconds + + if (daytotals.contains(daykey)) + daytotals.replace(daykey, daytotals[daykey]+taskdaytotals[daytaskkey]); + else + daytotals.insert(daykey, taskdaytotals[daytaskkey]); + } + cell.push_back(delim); + + day = day.addDays(1); + } + + // Total for task + cell.push_back(QString::fromLatin1("%1").arg(formatTime(sum/60, rc.decimalMinutes))); + + // room for the recursive total time (that cannot be calculated now) + cell.push_back(delim); + colrectot = cell.size(); + cell.push_back("???"); + cell.push_back(delim); + + // Task name + for ( int i = level + 1; i > 0; i-- ) cell.push_back(delim); + + /* + // CSV compliance + // Surround the field with quotes if the field contains + // a comma (delim) or a double quote + to_quote = task->name().contains(delim) || task->name().contains(dquote); + */ + to_quote = true; + if ( to_quote) cell.push_back(dquote); + + + // Double quotes replaced by a pair of consecutive double quotes + cell.push_back(task->name().replace( dquote, double_dquote )); + + if ( to_quote) cell.push_back(dquote); + + cell.push_back(cr); + + add=0; + for (Task* subTask = task->firstChild(); + subTask; + subTask = subTask->nextSibling()) + { + add += printTaskHistory( subTask, taskdaytotals, daytotals, from, to , level+1, matrix, + rc ); + } + cell[colrectot]=(QString::fromLatin1("%1").arg(formatTime((add+sum)/60, rc.decimalMinutes ))); + for (unsigned int i=0; i < cell.size(); i++) matrix[ownline]+=cell[i]; + return add+sum; +} + +QString KarmStorage::report( TaskView *taskview, const ReportCriteria &rc ) +{ + QString err; + if ( rc.reportType == ReportCriteria::CSVHistoryExport ) + err = exportcsvHistory( taskview, rc.from, rc.to, rc ); + else if ( rc.reportType == ReportCriteria::CSVTotalsExport ) + err = exportcsvFile( taskview, rc ); + else { + // hmmmm ... assert(0)? + } + return err; +} + +// export history report as csv, all tasks X all dates in one block +QString KarmStorage::exportcsvHistory ( TaskView *taskview, + const QDate &from, + const QDate &to, + const ReportCriteria &rc) +{ + QString delim = rc.delimiter; + const QString cr = QString::fromLatin1("\n"); + QString err; + + // below taken from timekard.cpp + QString retval; + QString taskhdr, totalhdr; + QString line, buf; + long sum; + + QValueList<HistoryEvent> events; + QValueList<HistoryEvent>::iterator event; + QMap<QString, long> taskdaytotals; + QMap<QString, long> daytotals; + QString daytaskkey, daykey; + QDate day; + QDate dayheading; + + // parameter-plausi + if ( from > to ) + { + err = QString::fromLatin1 ( + "'to' has to be a date later than or equal to 'from'."); + } + + // header + retval += i18n("Task History\n"); + retval += i18n("From %1 to %2") + .arg(KGlobal::locale()->formatDate(from)) + .arg(KGlobal::locale()->formatDate(to)); + retval += cr; + retval += i18n("Printed on: %1") + .arg(KGlobal::locale()->formatDateTime(QDateTime::currentDateTime())); + retval += cr; + + day=from; + events = taskview->getHistory(from, to); + taskdaytotals.clear(); + daytotals.clear(); + + // Build lookup dictionary used to output data in table cells. keys are + // in this format: YYYYMMDD_NNNNNN, where Y = year, M = month, d = day and + // NNNNN = the VTODO uid. The value is the total seconds logged against + // that task on that day. Note the UID is the todo id, not the event id, + // so times are accumulated for each task. + for (event = events.begin(); event != events.end(); ++event) + { + daykey = (*event).start().date().toString(QString::fromLatin1("yyyyMMdd")); + daytaskkey = QString(QString::fromLatin1("%1_%2")) + .arg(daykey) + .arg((*event).todoUid()); + + if (taskdaytotals.contains(daytaskkey)) + taskdaytotals.replace(daytaskkey, + taskdaytotals[daytaskkey] + (*event).duration()); + else + taskdaytotals.insert(daytaskkey, (*event).duration()); + } + + // day headings + dayheading = from; + while ( dayheading <= to ) + { + // Use ISO 8601 format for date. + retval += dayheading.toString(QString::fromLatin1("yyyy-MM-dd")); + retval += delim; + dayheading=dayheading.addDays(1); + } + retval += i18n("Sum") + delim + i18n("Total Sum") + delim + i18n("Task Hierarchy"); + retval += cr; + retval += line; + + // the tasks + vector <QString> matrix; + linenr=0; + for (int i=0; i<=taskview->count()+1; i++) matrix.push_back(""); + if (events.empty()) + { + retval += i18n(" No hours logged."); + } + else + { + if ( rc.allTasks ) + { + for ( Task* task= taskview->item_at_index(0); + task; task= task->nextSibling() ) + { + printTaskHistory( task, taskdaytotals, daytotals, from, to, 0, + matrix, rc ); + } + } + else + { + printTaskHistory( taskview->current_item(), taskdaytotals, daytotals, + from, to, 0, matrix, rc ); + } + for (unsigned int i=0; i<matrix.size(); i++) retval+=matrix[i]; + retval += line; + + // totals + sum = 0; + day = from; + while (day<=to) + { + daykey = day.toString(QString::fromLatin1("yyyyMMdd")); + + if (daytotals.contains(daykey)) + { + retval += QString::fromLatin1("%1") + .arg(formatTime(daytotals[daykey]/60, rc.decimalMinutes)); + sum += daytotals[daykey]; // in seconds + } + retval += delim; + day = day.addDays(1); + } + + retval += QString::fromLatin1("%1%2%3%4") + .arg( formatTime( sum/60, rc.decimalMinutes ) ) + .arg( delim ).arg( delim ) + .arg( i18n( "Total" ) ); + } + + // above taken from timekard.cpp + + // save, either locally or remote + + if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/"))) + { + QString filename=rc.url.path(); + if (filename.isEmpty()) filename=rc.url.url(); + QFile f( filename ); + if( !f.open( IO_WriteOnly ) ) { + err = i18n( "Could not open \"%1\"." ).arg( filename ); + } + if (!err) + { + QTextStream stream(&f); + // Export to file + stream << retval; + f.close(); + } + } + else // use remote file + { + KTempFile tmpFile; + if ( tmpFile.status() != 0 ) + { + err = QString::fromLatin1( "Unable to get temporary file" ); + } + else + { + QTextStream *stream=tmpFile.textStream(); + *stream << retval; + tmpFile.close(); + if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=QString::fromLatin1("Could not upload"); + } + } + return err; +} + +void KarmStorage::stopTimer(const Task* task, QDateTime when) +{ + kdDebug(5970) << "Entering KarmStorage::stopTimer" << endl; + long delta = task->startTime().secsTo(when); + changeTime(task, delta); +} + +bool KarmStorage::bookTime(const Task* task, + const QDateTime& startDateTime, + const long durationInSeconds) +{ + // Ignores preferences setting re: logging history. + KCal::Event* e; + QDateTime end; + + e = baseEvent( task ); + e->setDtStart( startDateTime ); + e->setDtEnd( startDateTime.addSecs( durationInSeconds ) ); + + // Use a custom property to keep a record of negative durations + e->setCustomProperty( kapp->instanceName(), + QCString("duration"), + QString::number(durationInSeconds)); + + return _calendar->addEvent(e); +} + +void KarmStorage::changeTime(const Task* task, const long deltaSeconds) +{ + kdDebug(5970) << "Entering KarmStorage::changeTime ( " << task->name() << "," << deltaSeconds << " )" << endl; + KCal::Event* e; + QDateTime end; + + // Don't write events (with timer start/stop duration) if user has turned + // this off in the settings dialog. + if ( ! task->taskView()->preferences()->logging() ) return; + + e = baseEvent(task); + + // Don't use duration, as ICalFormatImpl::writeIncidence never writes a + // duration, even though it looks like it's used in event.cpp. + end = task->startTime(); + if ( deltaSeconds > 0 ) end = task->startTime().addSecs(deltaSeconds); + e->setDtEnd(end); + + // Use a custom property to keep a record of negative durations + e->setCustomProperty( kapp->instanceName(), + QCString("duration"), + QString::number(deltaSeconds)); + + _calendar->addEvent(e); + + // This saves the entire iCal file each time, which isn't efficient but + // ensures no data loss. A faster implementation would be to append events + // to a file, and then when KArm closes, append the data in this file to the + // iCal file. + // + // Meanwhile, we simply use a timer to delay the full-saving until the GUI + // has updated, for better user feedback. Feel free to get rid of this + // if/when implementing the faster saving (DF). + task->taskView()->scheduleSave(); +} + + +KCal::Event* KarmStorage::baseEvent(const Task * task) +{ + KCal::Event* e; + QStringList categories; + + e = new KCal::Event; + e->setSummary(task->name()); + + // Can't use setRelatedToUid()--no error, but no RelatedTo written to disk + e->setRelatedTo(_calendar->todo(task->uid())); + + // Debugging: some events where not getting a related-to field written. + assert(e->relatedTo()->uid() == task->uid()); + + // Have to turn this off to get datetimes in date fields. + e->setFloats(false); + e->setDtStart(task->startTime()); + + // So someone can filter this mess out of their calendar display + categories.append(i18n("KArm")); + e->setCategories(categories); + + return e; +} + +HistoryEvent::HistoryEvent(QString uid, QString name, long duration, + QDateTime start, QDateTime stop, QString todoUid) +{ + _uid = uid; + _name = name; + _duration = duration; + _start = start; + _stop = stop; + _todoUid = todoUid; +} + + +QValueList<HistoryEvent> KarmStorage::getHistory(const QDate& from, + const QDate& to) +{ + QValueList<HistoryEvent> retval; + QStringList processed; + KCal::Event::List events; + KCal::Event::List::iterator event; + QString duration; + + for(QDate d = from; d <= to; d = d.addDays(1)) + { + events = _calendar->rawEventsForDate( d ); + for (event = events.begin(); event != events.end(); ++event) + { + + // KArm events have the custom property X-KDE-Karm-duration + if (! processed.contains( (*event)->uid())) + { + // If an event spans multiple days, CalendarLocal::rawEventsForDate + // will return the same event on both days. To avoid double-counting + // such events, we (arbitrarily) attribute the hours from both days on + // the first day. This mis-reports the actual time spent, but it is + // an easy fix for a (hopefully) rare situation. + processed.append( (*event)->uid()); + + duration = (*event)->customProperty(kapp->instanceName(), + QCString("duration")); + if ( ! duration.isNull() ) + { + if ( (*event)->relatedTo() + && ! (*event)->relatedTo()->uid().isEmpty() ) + { + retval.append(HistoryEvent( + (*event)->uid(), + (*event)->summary(), + duration.toLong(), + (*event)->dtStart(), + (*event)->dtEnd(), + (*event)->relatedTo()->uid() + )); + } + else + // Something is screwy with the ics file, as this KArm history event + // does not have a todo related to it. Could have been deleted + // manually? We'll continue with report on with report ... + kdDebug(5970) << "KarmStorage::getHistory(): " + << "The event " << (*event)->uid() + << " is not related to a todo. Dropped." << endl; + } + } + } + } + + return retval; +} + +bool KarmStorage::remoteResource( const QString& file ) const +{ + QString f = file.lower(); + bool rval = f.startsWith( "http://" ) || f.startsWith( "ftp://" ); + + kdDebug(5970) << "KarmStorage::remoteResource( " << file << " ) returns " << rval << endl; + return rval; +} + +bool KarmStorage::saveCalendar() +{ + kdDebug(5970) << "KarmStorage::saveCalendar" << endl; + +#if 0 + Event::List evl=_calendar->rawEvents(); + kdDebug(5970) << "summary - dtStart - dtEnd" << endl; + for (unsigned int i=0; i<evl.count(); i++) + { + kdDebug() << evl[i]->summary() << evl[i]->dtStart() << evl[i]->dtEnd() << endl; + } +#endif + KABC::Lock *lock = _calendar->lock(); + if ( !lock || !lock->lock() ) + return false; + + if ( _calendar && _calendar->save() ) { + lock->unlock(); + return true; + } + + lock->unlock(); + return false; +} diff --git a/karm/karmstorage.h b/karm/karmstorage.h new file mode 100644 index 000000000..c84658297 --- /dev/null +++ b/karm/karmstorage.h @@ -0,0 +1,381 @@ +/* + * This file only: + * Copyright (C) 2003 Mark Bucciarelli <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ + +#ifndef KARM_STORAGE_H +#define KARM_STORAGE_H + +#include <qdict.h> +#include <qptrstack.h> + +#include "journal.h" +#include "reportcriteria.h" + +#include "desktoplist.h" + +#include <calendarresources.h> +#include <vector> +#include "resourcecalendar.h" +#include <kdepimmacros.h> + +class QDateTime; +class Preferences; +class Task; +class TaskView; +class HistoryEvent; +class KCal::Todo; + +/** + * Singleton to store/retrieve KArm data to/from persistent storage. + * + * The storage is an iCalendar file. Also included are methods to + * import KArm data from the two legacy file formats. + * + * All logic that deals with getting and saving data should go here. The + * storage logic has changed at least twice already in KArm's history, and + * chances are good it will change again (for example, allowing KOrganizer and + * KArm to access the same iCalendar file simultaneously). + * + * Prior to KDE 3.2, KArm just stored totals for each task--a session total + * and a task total. The session total was reset to zero each time KArm + * started up or after the user reset the session times to zero. With the + * release of KDE 3.2, KArm now stores these task totals as well as logging + * the history of each start/stop event; that is, every time you start a timer + * and then stop a timer on a task, KArm records this as an iCalendar event. + * + * @short Logic that gets and stores KArm data to disk. + * @author Mark Bucciarelli <[email protected]> + */ + +class KarmStorage +{ + public: + /* + * Return reference to storage singleton. + * + * The constructors are private, so this must be used to create a + * KarmStorage instance. + */ + static KarmStorage *instance(); + + /* + * Load the list view with tasks read from iCalendar file. + * + * Parses iCalendar file, builds list view items in the proper + * hierarchy, and loads them into the list view widget. + * + * If the file name passed in is the same as the last file name that was + * loaded, this method does nothing. + * + * This method considers any of the following conditions errors: + * + * @li the iCalendar file does not exist + * @li the iCalendar file is not readable + * @li the list group currently has list items + * @li an iCalendar todo has no related to attribute + * @li a todo is related to another todo which does not exist + * + * @param taskview The list group used in the TaskView + * @param preferences The current KArm preferences. + * @param fileName Override preferences' filename + * + * @return empty string if success, error message if error. + * + */ + QString load(TaskView* taskview, const Preferences* preferences, QString fileName="" ); + + /* + * Return the name of the iCal file + */ + QString icalfile(); + + /* + * Build up the taskview. + * + * This is needed if the iCal file has been modified + */ + QString buildTaskView(KCal::ResourceCalendar *rc, TaskView *view); + + /* Close calendar and clear view. Release lock if holding one. */ + void closeStorage(TaskView* view); + + /* + * Save all tasks and their totals to an iCalendar file. + * + * All tasks must have an associated VTODO object already created in the + * calendar file; that is, the task->uid() must refer to a valid VTODO in + * the calender. + * Delivers empty string if successful, else error msg. + * + * @param taskview The list group used in the TaskView + */ + QString save(TaskView* taskview); + + /** + * Read tasks and their total times from a text file (legacy storage). + * + * This reads from one of the two legacy file formats. In this version, + * the parent task times do not include the sum of all their children's + * times. + * + * The format of the file is zero or more lines of: + * 1 task id (a number) + * time in minutes + * string task name + * [string] desktops, in which to count. e.g. "1,2,5" (optional) + */ + QString loadFromFlatFile(TaskView* taskview, const QString& filename); + + /** + * Reads tasks and their total times from text file (legacy). + * + * This is the older legacy format, where the task totals included the + * children totals. + * + * @see loadFromFlatFile + */ + QString loadFromFlatFileCumulative(TaskView* taskview, + const QString& filename); + + /** + Output a report based on contents of ReportCriteria. + */ + QString report( TaskView *taskview, const ReportCriteria &rc ); + + /** + * Log the change in a task's time. + * + * We create an iCalendar event to store each change. The event start + * date is set to the current datetime. If time is added to the task, the + * task end date is set to start time + delta. If the time is negative, + * the end date is set to the start time. + * + * In both cases (postive or negative delta), we create a custom iCalendar + * property that stores the delta (in seconds). This property is called + * X-KDE-karm-duration. + * + * Note that the KArm UI allows the user to change both the session and + * the total task time, and this routine does not account for all posibile + * cases. For example, it is possible for the user to do something crazy + * like add 10 minutes to the session time and subtract 50 minutes from + * the total time. Although this change violates a basic law of physics, + * it is allowed. + * + * For now, you should pass in the change to the total task time. + * Eventually, the UI should be changed. + * + * @param task The task the change is for. + * @param delta Change in task time, in seconds. Can be negative. + */ + void changeTime(const Task* task, const long deltaSeconds); + + /** + * Book time to a task. + * + * Creates an iCalendar event and adds it to the calendar. Does not write + * calender to disk, just adds event to calendar in memory. However, the + * resource framework does try to get a lock on the file. After a + * succesful lock, the calendar marks this incidence as modified and then + * releases the lock. + * + * @param task Task + * @param startDateTime Date and time the booking starts. + * @param durationInSeconds Duration of time to book, in seconds. + * + * @return true if event was added, false if not (if, for example, the + * attempted file lock failed). + */ + bool bookTime(const Task* task, const QDateTime& startDateTime, + long durationInSeconds); + + /** + * Log a change to a task name. + * + * For iCalendar storage, there is no need to log an Event for this event, + * since unique id's are used to link Events to Todos. No matter how many + * times you change a task's name, the uid stays the same. + * + * @param task The task + * @param oldname The old name of the task. The new name is in the task + * object already. + */ + void setName(const Task* task, const QString& oldname) { Q_UNUSED(task); Q_UNUSED(oldname); } + + + /** + * Log the event that a timer has started for a task. + * + * For the iCalendar storage, there is no need to log anything for this + * event. We log an event when the timer is stopped. + * + * @param task The task the timer was started for. + */ + void startTimer(const Task* task) { Q_UNUSED(task); } + + /** + * Log the event that the timer has stopped for this task. + * + * The task stores the last time a timer was started, so we log a new iCal + * Event with the start and end times for this task. + * @see KarmStorage::changeTime + * + * @param task The task the timer was stopped for. + */ + void stopTimer(const Task* task, QDateTime when=QDateTime::currentDateTime()); + + /** + * Log a new comment for this task. + * + * iCal allows multiple comment tags. So we just add a new comment to the + * todo for this task and write the calendar. + * + * @param task The task that gets the comment + * @param comment The comment + */ + void addComment(const Task* task, const QString& comment); + + + /** + * Remove this task from iCalendar file. + * + * Removes task as well as all event history for this task. + * + * @param task The task to be removed. + * @return true if change was saved, false otherwise + */ + bool removeTask(Task* task); + + /** + * Add this task from iCalendar file. + * + * Create a new KCal::Todo object and load with task information. If + * parent is not zero, then set the RELATED-TO attribute for this Todo. + * + * @param task The task to be removed. + * @param parent The parent of this task. Must have a uid() that is in + * the existing calendar. If zero, this task is considered a root task. + * @return The unique ID for the new VTODO. Return an null QString if + * there was an error creating the new calendar object. + */ + QString addTask(const Task* task, const Task* parent); + + /** + * Check if the iCalendar file currently loaded has any Todos in it. + * + * @return true if iCalendar file has any todos + */ + bool isEmpty(); + + /** + * Check if iCalendar file name in the preferences has changed since the + * last call to load. If there is no calendar file currently loaded, + * return false. + * + * @param preferences Set of KArm preferences. + * + * @return true if a previous file has been loaded and the iCalendar file + * specified in the preferences is different. + */ + bool isNewStorage(const Preferences* preferences) const; + + /** Return a list of start/stop events for the given date range. */ + QValueList<HistoryEvent> getHistory(const QDate& from, const QDate& to); + + private: + static KarmStorage *_instance; + KCal::ResourceCalendar *_calendar; + QString _icalfile; + + KarmStorage(); + void adjustFromLegacyFileFormat(Task* task); + bool parseLine(QString line, long *time, QString *name, int *level, + DesktopList* desktopList); + QString writeTaskAsTodo + (Task* task, const int level, QPtrStack< KCal::Todo >& parents); + bool saveCalendar(); + + KCal::Event* baseEvent(const Task*); + bool remoteResource( const QString& file ) const; + + /** + * Writes all tasks and their totals to a Comma-Separated Values file. + * + * The format of this file is zero or more lines of: + * taskName,subtaskName,..,sessionTime,time,totalSessionTime,totalTime + * the number of subtasks is determined at runtime. + */ + QString exportcsvFile( TaskView *taskview, const ReportCriteria &rc ); + + /** + * Write task history to file as comma-delimited data. + */ + QString exportcsvHistory ( + TaskView* taskview, + const QDate& from, + const QDate& to, + const ReportCriteria &rc + ); + + long printTaskHistory ( + const Task *task, + const QMap<QString,long>& taskdaytotals, + QMap<QString,long>& daytotals, + const QDate& from, + const QDate& to, + const int level, + std::vector <QString> &matrix, + const ReportCriteria &rc + ); +}; + +/** + * One start/stop event that has been logged. + * + * When a task is running and the user stops it, KArm logs this event and + * saves it in the history. This class represents such an event read from + * storage, and abstracts it from the specific storage used. + */ +class HistoryEvent +{ + public: + /** Needed to be used in a value list. */ + HistoryEvent() {} + HistoryEvent(QString uid, QString name, long duration, + QDateTime start, QDateTime stop, QString todoUid); + QString uid() {return _uid; } + QString name() {return _name; } + /** In seconds. */ + long duration() {return _duration; } + QDateTime start() {return _start; } + QDateTime stop() { return _stop; } + QString todoUid() {return _todoUid; } + + private: + QString _uid; + QString _todoUid; + QString _name; + long _duration; + QDateTime _start; + QDateTime _stop; + +}; + +#endif // KARM_STORAGE_H diff --git a/karm/karmui.rc b/karm/karmui.rc new file mode 100644 index 000000000..77818aeb9 --- /dev/null +++ b/karm/karmui.rc @@ -0,0 +1,82 @@ +<!DOCTYPE kpartgui ><kpartgui name="karm" version="5" > +<MenuBar> + <Menu name="file" > + <text>&File</text> + <Action name="start_new_session" /> + <Action name="reset_all_times" /> + <Separator /> + <Menu name="importexport"> + <text>&Import/Export</text> + <Action name="import_flatfile" /> + <Action name="import_planner" /> + <Action name="export_csvfile" /> + <Action name="export_csvhistory" /> + </Menu> + <Action name="clip_totals" /> + <Action name="clip_history" /> + </Menu> + <Menu name="clock" > + <text>&Clock</text> + <Action name="start" /> + <Action name="stop" /> + <Action name="stopAll" /> + </Menu> + <Menu name="task" > + <text>&Task</text> + <Action name="new_task" /> + <Action name="new_sub_task" /> + <Action name="delete_task" /> + <Action name="edit_task" /> +<!-- <Action name="add_comment_to_task" /> --> + <Separator /> + <Action name="mark_as_complete" /> + <Action name="mark_as_incomplete" /> + </Menu> + <Menu name="settings" > + <text>&Settings</text> + <Action name="configure_karm" /> + </Menu> +</MenuBar> +<ToolBar alreadyVisited="1" position="Top" iconText="IconOnly" noMerge="1" name="mainToolBar" > + <text>Main Toolbar</text> + <Action name="start" /> + <Action name="stop" /> + <Action name="new_task" /> + <Action name="new_sub_task" /> + <Action name="delete_task" /> + <Action name="edit_task" /> +<!-- <Action name="add_comment_to_task" /> --> +</ToolBar> +<Menu name="task_popup"> + <Action name="start" /> + <Action name="stop" /> + <Action name="stopAll" /> + <Separator /> + <Action name="new_task" /> + <Action name="new_sub_task" /> + <Action name="delete_task" /> + <Action name="edit_task" /> +<!-- <Action name="add_comment_to_task" /> --> + <Separator /> + <Action name="mark_as_complete" /> + <Action name="mark_as_incomplete" /> + <Separator /> + <Action name="clip_totals" /> + <Action name="clip_history" /> + <Action name="clip_session" /> +</Menu> + +<State name="readonly"> + <Disable> + <Action name="file_save" /> + <Action name="new_task" /> + <Action name="new_sub_task" /> + <Action name="delete_task" /> + <Action name="edit_task" /> + <Action name="mark_as_complete" /> + <Action name="start_new_session" /> + <Action name="reset_all_times" /> +<!-- <Action name="add_comment_to_task" /> --> + </Disable> +</State> +</kpartgui> diff --git a/karm/karmutility.cpp b/karm/karmutility.cpp new file mode 100644 index 000000000..1663b178c --- /dev/null +++ b/karm/karmutility.cpp @@ -0,0 +1,23 @@ +#ifndef KARM_UTILITY_H +#define KARM_UTILITY_H + +#include <stdlib.h> + +#include <kglobal.h> +#include <klocale.h> +#include "karmutility.h" + +QString formatTime( long minutes, bool decimal ) +{ + QString time; + if ( decimal ) { + time.sprintf("%.2f", minutes / 60.0); + time.replace( '.', KGlobal::locale()->decimalSymbol() ); + } + else time.sprintf("%s%ld:%02ld", + (minutes < 0) ? KGlobal::locale()->negativeSign().utf8().data() : "", + labs(minutes / 60), labs(minutes % 60)); + return time; +} + +#endif // KARM_UTILITY_H diff --git a/karm/karmutility.h b/karm/karmutility.h new file mode 100644 index 000000000..28fbdc029 --- /dev/null +++ b/karm/karmutility.h @@ -0,0 +1,16 @@ +#ifndef KARMUTILITY_H +#define KARMUTILITY_H + +#include <qstring.h> + +/** + * Format time for output. All times output on screen or report output go + * through this function. + * + * If the second argument is true, the time is output as a two-place decimal. + * Otherwise the format is hh:mi. + * + */ +QString formatTime( long minutes, bool decimal=false ); + +#endif diff --git a/karm/ktimewidget.cpp b/karm/ktimewidget.cpp new file mode 100644 index 000000000..e7040f4f7 --- /dev/null +++ b/karm/ktimewidget.cpp @@ -0,0 +1,129 @@ +#include <stdlib.h> // abs() + +#include <qlabel.h> +#include <qlayout.h> +#include <qlineedit.h> +#include <qstring.h> +#include <qvalidator.h> +#include <qwidget.h> + +#include <klocale.h> // i18n +#include <kglobal.h> +#include "ktimewidget.h" + +enum ValidatorType { HOUR, MINUTE }; + +class TimeValidator : public QValidator +{ + public: + TimeValidator( ValidatorType tp, QWidget *parent=0, const char *name=0) + : QValidator(parent, name) + { + _tp = tp; + } + State validate(QString &str, int &) const + { + if (str.isEmpty()) + return Acceptable; + + bool ok; + int val = str.toInt( &ok ); + if ( ! ok ) + return Invalid; + + if ( _tp==MINUTE && val >= 60 ) + return Invalid; + else + return Acceptable; + } + + public: + ValidatorType _tp; +}; + + +class KarmLineEdit : public QLineEdit +{ + + public: + KarmLineEdit( QWidget* parent, const char* name = 0 ) + : QLineEdit( parent, name ) {} + +protected: + + virtual void keyPressEvent( QKeyEvent *event ) + { + QLineEdit::keyPressEvent( event ); + if ( text().length() == 2 && !event->text().isEmpty() ) + focusNextPrevChild(true); + } +}; + + +KArmTimeWidget::KArmTimeWidget( QWidget* parent, const char* name ) + : QWidget(parent, name) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + + _hourLE = new QLineEdit( this); + // 9999 hours > 1 year! + // 999 hours = 41 days (That should be enough ...) + _hourLE->setFixedWidth( fontMetrics().maxWidth() * 3 + + 2 * _hourLE->frameWidth() + 2); + layout->addWidget(_hourLE); + TimeValidator *validator = new TimeValidator( HOUR, _hourLE, + "Validator for _hourLE"); + _hourLE->setValidator( validator ); + _hourLE->setAlignment( Qt::AlignRight ); + + + QLabel *hr = new QLabel( i18n( "abbreviation for hours", " hr. " ), this ); + layout->addWidget( hr ); + + _minuteLE = new KarmLineEdit(this); + + // Minutes lineedit: Make room for 2 digits + _minuteLE->setFixedWidth( fontMetrics().maxWidth() * 2 + + 2 * _minuteLE->frameWidth() + 2); + layout->addWidget(_minuteLE); + validator = new TimeValidator( MINUTE, _minuteLE, "Validator for _minuteLE"); + _minuteLE->setValidator( validator ); + _minuteLE->setMaxLength(2); + _minuteLE->setAlignment( Qt::AlignRight ); + + QLabel *min = new QLabel( i18n( "abbreviation for minutes", " min. " ), this ); + layout->addWidget( min ); + + layout->addStretch(1); + setFocusProxy( _hourLE ); +} + +void KArmTimeWidget::setTime( long minutes ) +{ + QString dummy; + long hourpart = labs(minutes) / 60; + long minutepart = labs(minutes) % 60; + + dummy.setNum( hourpart ); + if (minutes < 0) + dummy = KGlobal::locale()->negativeSign() + dummy; + _hourLE->setText( dummy ); + + dummy.setNum( minutepart ); + if (minutepart < 10 ) { + dummy = QString::fromLatin1( "0" ) + dummy; + } + _minuteLE->setText( dummy ); +} + +long KArmTimeWidget::time() const +{ + bool ok, isNegative; + int h, m; + + h = abs(_hourLE->text().toInt( &ok )); + m = _minuteLE->text().toInt( &ok ); + isNegative = _hourLE->text().startsWith(KGlobal::locale()->negativeSign()); + + return (h * 60 + m) * ((isNegative) ? -1 : 1); +} diff --git a/karm/ktimewidget.h b/karm/ktimewidget.h new file mode 100644 index 000000000..9c6eb3b42 --- /dev/null +++ b/karm/ktimewidget.h @@ -0,0 +1,25 @@ +#ifndef KARM_K_TIME_WIDGET_H +#define KARM_K_TIME_WIDGET_H + +class QLineEdit; +class QWidget; + +class KarmLineEdit; + +/** + * Widget used for entering minutes and seconds with validation. + */ + +class KArmTimeWidget : public QWidget +{ + public: + KArmTimeWidget( QWidget* parent = 0, const char* name = 0 ); + void setTime( long minutes ); + long time() const; + + private: + QLineEdit *_hourLE; + KarmLineEdit *_minuteLE; +}; + +#endif // KARM_K_TIME_WIDGET_H diff --git a/karm/main.cpp b/karm/main.cpp new file mode 100644 index 000000000..cc1fc1cde --- /dev/null +++ b/karm/main.cpp @@ -0,0 +1,93 @@ +#include <signal.h> +#include <kapplication.h> +#include <klocale.h> +#include <kcmdlineargs.h> +#include <kaboutdata.h> +#include <kdebug.h> +#include "version.h" +#include "mainwindow.h" + + +namespace +{ + const char* description = I18N_NOOP("KDE Time tracker tool"); + + void cleanup( int ) + { + kdDebug(5970) << i18n("Just caught a software interrupt.") << endl; + kapp->exit(); + } +} + +static const KCmdLineOptions options[] = +{ + { "+file", I18N_NOOP( "The iCalendar file to open" ), 0 }, + KCmdLineLastOption +}; + +int main( int argc, char *argv[] ) +{ + KAboutData aboutData( "karm", I18N_NOOP("KArm"), + KARM_VERSION, description, KAboutData::License_GPL, + "(c) 1997-2004, KDE PIM Developers" ); + + aboutData.addAuthor( "Mark Bucciarelli", I18N_NOOP( "Current Maintainer" ), + "[email protected]" ); + aboutData.addAuthor( "Sirtaj Singh Kang", I18N_NOOP( "Original Author" ), + "[email protected]" ); + aboutData.addAuthor( "Allen Winter", 0, "[email protected]" ); + aboutData.addAuthor( "David Faure", 0, "[email protected]" ); + aboutData.addAuthor( "Espen Sand", 0, "[email protected]" ); + aboutData.addAuthor( "Gioele Barabucci", 0, "[email protected]" ); + aboutData.addAuthor( "Jan Schaumann", 0, "[email protected]" ); + aboutData.addAuthor( "Jesper Pedersen", 0, "[email protected]" ); + aboutData.addAuthor( "Kalle Dalheimer", 0, "[email protected]" ); + aboutData.addAuthor( "Scott Monachello", 0, "[email protected]" ); + aboutData.addAuthor( "Thorsten Staerk", 0, "[email protected]" ); + aboutData.addAuthor( "Tomas Pospisek", 0, "[email protected]" ); + aboutData.addAuthor( "Willi Richert", 0, "[email protected]" ); + + KCmdLineArgs::init( argc, argv, &aboutData ); + KCmdLineArgs::addCmdLineOptions( options ); + KApplication myApp; + + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + + MainWindow *mainWindow; + if ( args->count() > 0 ) + { + QString icsfile = QString::fromLocal8Bit( args->arg( 0 ) ); + // FIXME: there is probably a Qt or KDE fcn for this test + if ( icsfile.startsWith( "/" ) + || icsfile.lower().startsWith( "http://" ) + || icsfile.lower().startsWith( "ftp://" ) + ) + { + // leave as is + ; + } + else + { + icsfile = KCmdLineArgs::cwd() + "/" + icsfile; + } + mainWindow = new MainWindow( icsfile ); + } + else + { + mainWindow = new MainWindow(); + } + + myApp.setMainWidget( mainWindow ); + + if (kapp->isRestored() && KMainWindow::canBeRestored( 1 )) + mainWindow->restore( 1, false ); + else + mainWindow->show(); + + signal( SIGQUIT, cleanup ); + signal( SIGINT, cleanup ); + int ret = myApp.exec(); + + delete mainWindow; + return ret; +} diff --git a/karm/mainwindow.cpp b/karm/mainwindow.cpp new file mode 100644 index 000000000..141eafef9 --- /dev/null +++ b/karm/mainwindow.cpp @@ -0,0 +1,740 @@ +/* +* Top Level window for KArm. +* Distributed under the GPL. +*/ + +#include <numeric> + +#include "kaccelmenuwatch.h" +#include <dcopclient.h> +#include <kaccel.h> +#include <kaction.h> +#include <kapplication.h> // kapp +#include <kconfig.h> +#include <kdebug.h> +#include <kglobal.h> +#include <kkeydialog.h> +#include <klocale.h> // i18n +#include <kmessagebox.h> +#include <kstatusbar.h> // statusBar() +#include <kstdaction.h> +#include <qkeycode.h> +#include <qpopupmenu.h> +#include <qptrlist.h> +#include <qstring.h> + +#include "karmerrors.h" +#include "karmutility.h" +#include "mainwindow.h" +#include "preferences.h" +#include "print.h" +#include "task.h" +#include "taskview.h" +#include "timekard.h" +#include "tray.h" +#include "version.h" + +MainWindow::MainWindow( const QString &icsfile ) + : DCOPObject ( "KarmDCOPIface" ), + KParts::MainWindow(0,Qt::WStyle_ContextHelp), + _accel ( new KAccel( this ) ), + _watcher ( new KAccelMenuWatch( _accel, this ) ), + _totalSum ( 0 ), + _sessionSum( 0 ) +{ + + _taskView = new TaskView( this, 0, icsfile ); + + setCentralWidget( _taskView ); + // status bar + startStatusBar(); + + // setup PreferenceDialog. + _preferences = Preferences::instance(); + + // popup menus + makeMenus(); + _watcher->updateMenus(); + + // connections + connect( _taskView, SIGNAL( totalTimesChanged( long, long ) ), + this, SLOT( updateTime( long, long ) ) ); + connect( _taskView, SIGNAL( selectionChanged ( QListViewItem * )), + this, SLOT(slotSelectionChanged())); + connect( _taskView, SIGNAL( updateButtons() ), + this, SLOT(slotSelectionChanged())); + connect( _taskView, SIGNAL( setStatusBar( QString ) ), + this, SLOT(setStatusBar( QString ))); + + loadGeometry(); + + // Setup context menu request handling + connect( _taskView, + SIGNAL( contextMenuRequested( QListViewItem*, const QPoint&, int )), + this, + SLOT( contextMenuRequest( QListViewItem*, const QPoint&, int ))); + + _tray = new KarmTray( this ); + + connect( _tray, SIGNAL( quitSelected() ), SLOT( quit() ) ); + + connect( _taskView, SIGNAL( timersActive() ), _tray, SLOT( startClock() ) ); + connect( _taskView, SIGNAL( timersActive() ), this, SLOT( enableStopAll() )); + connect( _taskView, SIGNAL( timersInactive() ), _tray, SLOT( stopClock() ) ); + connect( _taskView, SIGNAL( timersInactive() ), this, SLOT( disableStopAll())); + connect( _taskView, SIGNAL( tasksChanged( QPtrList<Task> ) ), + _tray, SLOT( updateToolTip( QPtrList<Task> ) )); + + _taskView->load(); + + // Everything that uses Preferences has been created now, we can let it + // emit its signals + _preferences->emitSignals(); + slotSelectionChanged(); + + // Register with DCOP + if ( !kapp->dcopClient()->isRegistered() ) + { + kapp->dcopClient()->registerAs( "karm" ); + kapp->dcopClient()->setDefaultObject( objId() ); + } + + // Set up DCOP error messages + m_error[ KARM_ERR_GENERIC_SAVE_FAILED ] = + i18n( "Save failed, most likely because the file could not be locked." ); + m_error[ KARM_ERR_COULD_NOT_MODIFY_RESOURCE ] = + i18n( "Could not modify calendar resource." ); + m_error[ KARM_ERR_MEMORY_EXHAUSTED ] = + i18n( "Out of memory--could not create object." ); + m_error[ KARM_ERR_UID_NOT_FOUND ] = + i18n( "UID not found." ); + m_error[ KARM_ERR_INVALID_DATE ] = + i18n( "Invalidate date--format is YYYY-MM-DD." ); + m_error[ KARM_ERR_INVALID_TIME ] = + i18n( "Invalid time--format is YYYY-MM-DDTHH:MM:SS." ); + m_error[ KARM_ERR_INVALID_DURATION ] = + i18n( "Invalid task duration--must be greater than zero." ); +} + +void MainWindow::slotSelectionChanged() +{ + Task* item= _taskView->current_item(); + actionDelete->setEnabled(item); + actionEdit->setEnabled(item); + actionStart->setEnabled(item && !item->isRunning() && !item->isComplete()); + actionStop->setEnabled(item && item->isRunning()); + actionMarkAsComplete->setEnabled(item && !item->isComplete()); + actionMarkAsIncomplete->setEnabled(item && item->isComplete()); +} + +// This is _old_ code, but shows how to enable/disable add comment menu item. +// We'll need this kind of logic when comments are implemented. +//void MainWindow::timeLoggingChanged(bool on) +//{ +// actionAddComment->setEnabled( on ); +//} + +void MainWindow::setStatusBar(QString qs) +{ + statusBar()->message(i18n(qs.ascii())); +} + +bool MainWindow::save() +{ + kdDebug(5970) << "Saving time data to disk." << endl; + QString err=_taskView->save(); // untranslated error msg. + if (err.isEmpty()) statusBar()->message(i18n("Successfully saved tasks and history"),1807); + else statusBar()->message(i18n(err.ascii()),7707); // no msgbox since save is called when exiting + saveGeometry(); + return true; +} + +void MainWindow::exportcsvHistory() +{ + kdDebug(5970) << "Exporting History to disk." << endl; + QString err=_taskView->exportcsvHistory(); + if (err.isEmpty()) statusBar()->message(i18n("Successfully exported History to CSV-file"),1807); + else KMessageBox::error(this, err.ascii()); + saveGeometry(); + +} + +void MainWindow::quit() +{ + kapp->quit(); +} + + +MainWindow::~MainWindow() +{ + kdDebug(5970) << "MainWindow::~MainWindows: Quitting karm." << endl; + _taskView->stopAllTimers(); + save(); + _taskView->closeStorage(); +} + +void MainWindow::enableStopAll() +{ + actionStopAll->setEnabled(true); +} + +void MainWindow::disableStopAll() +{ + actionStopAll->setEnabled(false); +} + + +/** + * Calculate the sum of the session time and the total time for all + * toplevel tasks and put it in the statusbar. + */ + +void MainWindow::updateTime( long sessionDiff, long totalDiff ) +{ + _sessionSum += sessionDiff; + _totalSum += totalDiff; + + updateStatusBar(); +} + +void MainWindow::updateStatusBar( ) +{ + QString time; + + time = formatTime( _sessionSum ); + statusBar()->changeItem( i18n("Session: %1").arg(time), 0 ); + + time = formatTime( _totalSum ); + statusBar()->changeItem( i18n("Total: %1" ).arg(time), 1); +} + +void MainWindow::startStatusBar() +{ + statusBar()->insertItem( i18n("Session"), 0, 0, true ); + statusBar()->insertItem( i18n("Total" ), 1, 0, true ); +} + +void MainWindow::saveProperties( KConfig* cfg ) +{ + _taskView->stopAllTimers(); + _taskView->save(); + cfg->writeEntry( "WindowShown", isVisible()); +} + +void MainWindow::readProperties( KConfig* cfg ) +{ + if( cfg->readBoolEntry( "WindowShown", true )) + show(); +} + +void MainWindow::keyBindings() +{ + KKeyDialog::configure( actionCollection(), this ); +} + +void MainWindow::startNewSession() +{ + _taskView->startNewSession(); +} + +void MainWindow::resetAllTimes() +{ + if ( KMessageBox::warningContinueCancel( this, i18n( "Do you really want to reset the time to zero for all tasks?" ), + i18n( "Confirmation Required" ), KGuiItem( i18n( "Reset All Times" ) ) ) == KMessageBox::Continue ) + _taskView->resetTimeForAllTasks(); +} + +void MainWindow::makeMenus() +{ + KAction + *actionKeyBindings, + *actionNew, + *actionNewSub; + + (void) KStdAction::quit( this, SLOT( quit() ), actionCollection()); + (void) KStdAction::print( this, SLOT( print() ), actionCollection()); + actionKeyBindings = KStdAction::keyBindings( this, SLOT( keyBindings() ), + actionCollection() ); + actionPreferences = KStdAction::preferences(_preferences, + SLOT(showDialog()), + actionCollection() ); + (void) KStdAction::save( this, SLOT( save() ), actionCollection() ); + KAction* actionStartNewSession = new KAction( i18n("Start &New Session"), + 0, + this, + SLOT( startNewSession() ), + actionCollection(), + "start_new_session"); + KAction* actionResetAll = new KAction( i18n("&Reset All Times"), + 0, + this, + SLOT( resetAllTimes() ), + actionCollection(), + "reset_all_times"); + actionStart = new KAction( i18n("&Start"), + QString::fromLatin1("1rightarrow"), Key_S, + _taskView, + SLOT( startCurrentTimer() ), actionCollection(), + "start"); + actionStop = new KAction( i18n("S&top"), + QString::fromLatin1("stop"), Key_S, + _taskView, + SLOT( stopCurrentTimer() ), actionCollection(), + "stop"); + actionStopAll = new KAction( i18n("Stop &All Timers"), + Key_Escape, + _taskView, + SLOT( stopAllTimers() ), actionCollection(), + "stopAll"); + actionStopAll->setEnabled(false); + + actionNew = new KAction( i18n("&New..."), + QString::fromLatin1("filenew"), CTRL+Key_N, + _taskView, + SLOT( newTask() ), actionCollection(), + "new_task"); + actionNewSub = new KAction( i18n("New &Subtask..."), + QString::fromLatin1("kmultiple"), CTRL+ALT+Key_N, + _taskView, + SLOT( newSubTask() ), actionCollection(), + "new_sub_task"); + actionDelete = new KAction( i18n("&Delete"), + QString::fromLatin1("editdelete"), Key_Delete, + _taskView, + SLOT( deleteTask() ), actionCollection(), + "delete_task"); + actionEdit = new KAction( i18n("&Edit..."), + QString::fromLatin1("edit"), CTRL + Key_E, + _taskView, + SLOT( editTask() ), actionCollection(), + "edit_task"); +// actionAddComment = new KAction( i18n("&Add Comment..."), +// QString::fromLatin1("document"), +// CTRL+ALT+Key_E, +// _taskView, +// SLOT( addCommentToTask() ), +// actionCollection(), +// "add_comment_to_task"); + actionMarkAsComplete = new KAction( i18n("&Mark as Complete"), + QString::fromLatin1("document"), + CTRL+Key_M, + _taskView, + SLOT( markTaskAsComplete() ), + actionCollection(), + "mark_as_complete"); + actionMarkAsIncomplete = new KAction( i18n("&Mark as Incomplete"), + QString::fromLatin1("document"), + CTRL+Key_M, + _taskView, + SLOT( markTaskAsIncomplete() ), + actionCollection(), + "mark_as_incomplete"); + actionClipTotals = new KAction( i18n("&Copy Totals to Clipboard"), + QString::fromLatin1("klipper"), + CTRL+Key_C, + _taskView, + SLOT( clipTotals() ), + actionCollection(), + "clip_totals"); + // actionClipTotals will never be used again, overwrite it + actionClipTotals = new KAction( i18n("&Copy Session Time to Clipboard"), + QString::fromLatin1("klipper"), + 0, + _taskView, + SLOT( clipSession() ), + actionCollection(), + "clip_session"); + actionClipHistory = new KAction( i18n("Copy &History to Clipboard"), + QString::fromLatin1("klipper"), + CTRL+ALT+Key_C, + _taskView, + SLOT( clipHistory() ), + actionCollection(), + "clip_history"); + + new KAction( i18n("Import &Legacy Flat File..."), 0, + _taskView, SLOT(loadFromFlatFile()), actionCollection(), + "import_flatfile"); + new KAction( i18n("&Export to CSV File..."), 0, + _taskView, SLOT(exportcsvFile()), actionCollection(), + "export_csvfile"); + new KAction( i18n("Export &History to CSV File..."), 0, + this, SLOT(exportcsvHistory()), actionCollection(), + "export_csvhistory"); + new KAction( i18n("Import Tasks From &Planner..."), 0, + _taskView, SLOT(importPlanner()), actionCollection(), + "import_planner"); + +/* + new KAction( i18n("Import E&vents"), 0, + _taskView, + SLOT( loadFromKOrgEvents() ), actionCollection(), + "import_korg_events"); + */ + + setXMLFile( QString::fromLatin1("karmui.rc") ); + createGUI( 0 ); + + // Tool tips must be set after the createGUI. + actionKeyBindings->setToolTip( i18n("Configure key bindings") ); + actionKeyBindings->setWhatsThis( i18n("This will let you configure key" + "bindings which is specific to karm") ); + + actionStartNewSession->setToolTip( i18n("Start a new session") ); + actionStartNewSession->setWhatsThis( i18n("This will reset the session time " + "to 0 for all tasks, to start a " + "new session, without affecting " + "the totals.") ); + actionResetAll->setToolTip( i18n("Reset all times") ); + actionResetAll->setWhatsThis( i18n("This will reset the session and total " + "time to 0 for all tasks, to restart from " + "scratch.") ); + + actionStart->setToolTip( i18n("Start timing for selected task") ); + actionStart->setWhatsThis( i18n("This will start timing for the selected " + "task.\n" + "It is even possible to time several tasks " + "simultaneously.\n\n" + "You may also start timing of a tasks by " + "double clicking the left mouse " + "button on a given task. This will, however, " + "stop timing of other tasks.")); + + actionStop->setToolTip( i18n("Stop timing of the selected task") ); + actionStop->setWhatsThis( i18n("Stop timing of the selected task") ); + + actionStopAll->setToolTip( i18n("Stop all of the active timers") ); + actionStopAll->setWhatsThis( i18n("Stop all of the active timers") ); + + actionNew->setToolTip( i18n("Create new top level task") ); + actionNew->setWhatsThis( i18n("This will create a new top level task.") ); + + actionDelete->setToolTip( i18n("Delete selected task") ); + actionDelete->setWhatsThis( i18n("This will delete the selected task and " + "all its subtasks.") ); + + actionEdit->setToolTip( i18n("Edit name or times for selected task") ); + actionEdit->setWhatsThis( i18n("This will bring up a dialog box where you " + "may edit the parameters for the selected " + "task.")); + //actionAddComment->setToolTip( i18n("Add a comment to a task") ); + //actionAddComment->setWhatsThis( i18n("This will bring up a dialog box where " + // "you can add a comment to a task. The " + // "comment can for instance add information on what you " + // "are currently doing. The comment will " + // "be logged in the log file.")); + actionClipTotals->setToolTip(i18n("Copy task totals to clipboard")); + actionClipHistory->setToolTip(i18n("Copy time card history to clipboard.")); + + slotSelectionChanged(); +} + +void MainWindow::print() +{ + MyPrinter printer(_taskView); + printer.print(); +} + +void MainWindow::loadGeometry() +{ + if (initialGeometrySet()) setAutoSaveSettings(); + else + { + KConfig &config = *kapp->config(); + + config.setGroup( QString::fromLatin1("Main Window Geometry") ); + int w = config.readNumEntry( QString::fromLatin1("Width"), 100 ); + int h = config.readNumEntry( QString::fromLatin1("Height"), 100 ); + w = QMAX( w, sizeHint().width() ); + h = QMAX( h, sizeHint().height() ); + resize(w, h); + } +} + + +void MainWindow::saveGeometry() +{ + KConfig &config = *KGlobal::config(); + config.setGroup( QString::fromLatin1("Main Window Geometry")); + config.writeEntry( QString::fromLatin1("Width"), width()); + config.writeEntry( QString::fromLatin1("Height"), height()); + config.sync(); +} + +bool MainWindow::queryClose() +{ + if ( !kapp->sessionSaving() ) { + hide(); + return false; + } + return KMainWindow::queryClose(); +} + +void MainWindow::contextMenuRequest( QListViewItem*, const QPoint& point, int ) +{ + QPopupMenu* pop = dynamic_cast<QPopupMenu*>( + factory()->container( i18n( "task_popup" ), this ) ); + if ( pop ) + pop->popup( point ); +} + +//---------------------------------------------------------------------------- +// +// D C O P I N T E R F A C E +// +//---------------------------------------------------------------------------- + +QString MainWindow::version() const +{ + return KARM_VERSION; +} + +QString MainWindow::deletetodo() +{ + _taskView->deleteTask(); + return ""; +} + +bool MainWindow::getpromptdelete() +{ + return _preferences->promptDelete(); +} + +QString MainWindow::setpromptdelete( bool prompt ) +{ + _preferences->setPromptDelete( prompt ); + return ""; +} + +QString MainWindow::taskIdFromName( const QString &taskname ) const +{ + QString rval = ""; + + Task* task = _taskView->first_child(); + while ( rval.isEmpty() && task ) + { + rval = _hasTask( task, taskname ); + task = task->nextSibling(); + } + + return rval; +} + +int MainWindow::addTask( const QString& taskname ) +{ + DesktopList desktopList; + QString uid = _taskView->addTask( taskname, 0, 0, desktopList ); + kdDebug(5970) << "MainWindow::addTask( " << taskname << " ) returns " << uid << endl; + if ( uid.length() > 0 ) return 0; + else + { + // We can't really tell what happened, b/c the resource framework only + // returns a boolean. + return KARM_ERR_GENERIC_SAVE_FAILED; + } +} + +QString MainWindow::setPerCentComplete( const QString& taskName, int perCent ) +{ + int index; + QString err="no such task"; + for (int i=0; i<_taskView->count(); i++) + { + if ((_taskView->item_at_index(i)->name()==taskName)) + { + index=i; + if (err==QString::null) err="task name is abigious"; + if (err=="no such task") err=QString::null; + } + } + if (err==QString::null) + { + _taskView->item_at_index(index)->setPercentComplete( perCent, _taskView->storage() ); + } + return err; +} + +int MainWindow::bookTime +( const QString& taskId, const QString& datetime, long minutes ) +{ + int rval = 0; + QDate startDate; + QTime startTime; + QDateTime startDateTime; + Task *task, *t; + + if ( minutes <= 0 ) rval = KARM_ERR_INVALID_DURATION; + + // Find task + task = _taskView->first_child(); + t = NULL; + while ( !t && task ) + { + t = _hasUid( task, taskId ); + task = task->nextSibling(); + } + if ( t == NULL ) rval = KARM_ERR_UID_NOT_FOUND; + + // Parse datetime + if ( !rval ) + { + startDate = QDate::fromString( datetime, Qt::ISODate ); + if ( datetime.length() > 10 ) // "YYYY-MM-DD".length() = 10 + { + startTime = QTime::fromString( datetime, Qt::ISODate ); + } + else startTime = QTime( 12, 0 ); + if ( startDate.isValid() && startTime.isValid() ) + { + startDateTime = QDateTime( startDate, startTime ); + } + else rval = KARM_ERR_INVALID_DATE; + + } + + // Update task totals (session and total) and save to disk + if ( !rval ) + { + t->changeTotalTimes( t->sessionTime() + minutes, t->totalTime() + minutes ); + if ( ! _taskView->storage()->bookTime( t, startDateTime, minutes * 60 ) ) + { + rval = KARM_ERR_GENERIC_SAVE_FAILED; + } + } + + return rval; +} + +// There was something really bad going on with DCOP when I used a particular +// argument name; if I recall correctly, the argument name was errno. +QString MainWindow::getError( int mkb ) const +{ + if ( mkb <= KARM_MAX_ERROR_NO ) return m_error[ mkb ]; + else return i18n( "Invalid error number: %1" ).arg( mkb ); +} + +int MainWindow::totalMinutesForTaskId( const QString& taskId ) +{ + int rval = 0; + Task *task, *t; + + kdDebug(5970) << "MainWindow::totalTimeForTask( " << taskId << " )" << endl; + + // Find task + task = _taskView->first_child(); + t = NULL; + while ( !t && task ) + { + t = _hasUid( task, taskId ); + task = task->nextSibling(); + } + if ( t != NULL ) + { + rval = t->totalTime(); + kdDebug(5970) << "MainWindow::totalTimeForTask - task found: rval = " << rval << endl; + } + else + { + kdDebug(5970) << "MainWindow::totalTimeForTask - task not found" << endl; + rval = KARM_ERR_UID_NOT_FOUND; + } + + return rval; +} + +QString MainWindow::_hasTask( Task* task, const QString &taskname ) const +{ + QString rval = ""; + if ( task->name() == taskname ) + { + rval = task->uid(); + } + else + { + Task* nexttask = task->firstChild(); + while ( rval.isEmpty() && nexttask ) + { + rval = _hasTask( nexttask, taskname ); + nexttask = nexttask->nextSibling(); + } + } + return rval; +} + +Task* MainWindow::_hasUid( Task* task, const QString &uid ) const +{ + Task *rval = NULL; + + //kdDebug(5970) << "MainWindow::_hasUid( " << task << ", " << uid << " )" << endl; + + if ( task->uid() == uid ) rval = task; + else + { + Task* nexttask = task->firstChild(); + while ( !rval && nexttask ) + { + rval = _hasUid( nexttask, uid ); + nexttask = nexttask->nextSibling(); + } + } + return rval; +} +QString MainWindow::starttimerfor( const QString& taskname ) +{ + int index; + QString err="no such task"; + for (int i=0; i<_taskView->count(); i++) + { + if ((_taskView->item_at_index(i)->name()==taskname)) + { + index=i; + if (err==QString::null) err="task name is abigious"; + if (err=="no such task") err=QString::null; + } + } + if (err==QString::null) _taskView->startTimerFor( _taskView->item_at_index(index) ); + return err; +} + +QString MainWindow::stoptimerfor( const QString& taskname ) +{ + int index; + QString err="no such task"; + for (int i=0; i<_taskView->count(); i++) + { + if ((_taskView->item_at_index(i)->name()==taskname)) + { + index=i; + if (err==QString::null) err="task name is abigious"; + if (err=="no such task") err=QString::null; + } + } + if (err==QString::null) _taskView->stopTimerFor( _taskView->item_at_index(index) ); + return err; +} + +QString MainWindow::exportcsvfile( QString filename, QString from, QString to, int type, bool decimalMinutes, bool allTasks, QString delimiter, QString quote ) +{ + ReportCriteria rc; + rc.url=filename; + rc.from=QDate::fromString( from ); + if ( rc.from.isNull() ) rc.from=QDate::fromString( from, Qt::ISODate ); + kdDebug(5970) << "rc.from " << rc.from << endl; + rc.to=QDate::fromString( to ); + if ( rc.to.isNull() ) rc.to=QDate::fromString( to, Qt::ISODate ); + kdDebug(5970) << "rc.to " << rc.to << endl; + rc.reportType=(ReportCriteria::REPORTTYPE) type; // history report or totals report + rc.decimalMinutes=decimalMinutes; + rc.allTasks=allTasks; + rc.delimiter=delimiter; + rc.quote=quote; + return _taskView->report( rc ); +} + +QString MainWindow::importplannerfile( QString fileName ) +{ + return _taskView->importPlanner(fileName); +} + + +#include "mainwindow.moc" diff --git a/karm/mainwindow.h b/karm/mainwindow.h new file mode 100644 index 000000000..7f066af30 --- /dev/null +++ b/karm/mainwindow.h @@ -0,0 +1,119 @@ +#ifndef KARM_MAIN_WINDOW_H +#define KARM_MAIN_WINDOW_H + +#include <kparts/mainwindow.h> + +#include "karmerrors.h" +#include <karmdcopiface.h> +#include "reportcriteria.h" + +class KAccel; +class KAccelMenuWatch; +class KarmTray; +class QListViewItem; +class QPoint; +class QString; + +class Preferences; +class PrintDialog; +class Task; +class TaskView; + +/** + * Main window to tie the application together. + */ + +class MainWindow : public KParts::MainWindow, virtual public KarmDCOPIface +{ + Q_OBJECT + + private: + void makeMenus(); + QString _hasTask( Task* task, const QString &taskname ) const; + Task* _hasUid( Task* task, const QString &uid ) const; + + KAccel* _accel; + KAccelMenuWatch* _watcher; + TaskView* _taskView; + long _totalSum; + long _sessionSum; + Preferences* _preferences; + KarmTray* _tray; + KAction* actionStart; + KAction* actionStop; + KAction* actionStopAll; + KAction* actionDelete; + KAction* actionEdit; + KAction* actionMarkAsComplete; + KAction* actionMarkAsIncomplete; + KAction* actionPreferences; + KAction* actionClipTotals; + KAction* actionClipHistory; + QString m_error[ KARM_MAX_ERROR_NO + 1 ]; + + friend class KarmTray; + + //private: + + //KDialogBase *dialog; + + + + public: + MainWindow( const QString &icsfile = "" ); + virtual ~MainWindow(); + + // DCOP + QString version() const; + QString taskIdFromName( const QString &taskName ) const; + /** @reimp from KarmDCOPIface::addTask */ + int addTask( const QString &storage ); + /** @reimp from KarmDCOPIface::setPerCentComplete */ + QString setPerCentComplete( const QString& taskName, int PerCent ); + /** @reimp from KarmDCOPIface::bookTime */ + int bookTime( const QString& taskId, const QString& iso8601StartDateTime, long durationInMinutes ); + /** @reimp from KarmDCOPIface::getError */ + QString getError( int karmErrorNumber ) const; + int totalMinutesForTaskId( const QString& taskId ); + /** start the timer for taskname */ + QString starttimerfor( const QString &taskname ); + /** stop the timer for taskname */ + QString stoptimerfor( const QString &taskname ); + QString deletetodo(); + /** shall there be a "really delete" question */ + bool getpromptdelete(); + /** set if there will be a "really delete" question */ + QString setpromptdelete( bool prompt ); + QString exportcsvfile( QString filename, QString from, QString to, int type, bool decimalMinutes, bool allTasks, QString delimiter, QString quote ); + QString importplannerfile( QString filename ); + + public slots: + void setStatusBar( QString ); + void quit(); + + protected slots: + void keyBindings(); + void startNewSession(); + void resetAllTimes(); + void updateTime( long, long ); + void updateStatusBar(); + bool save(); + void exportcsvHistory(); + void print(); + void slotSelectionChanged(); + void contextMenuRequest( QListViewItem*, const QPoint&, int ); + void enableStopAll(); + void disableStopAll(); +// void timeLoggingChanged( bool on ); + + protected: + void startStatusBar(); + virtual void saveProperties( KConfig* ); + virtual void readProperties( KConfig* ); + void saveGeometry(); + void loadGeometry(); + bool queryClose(); + +}; + +#endif // KARM_MAIN_WINDOW_H diff --git a/karm/pics/Makefile.am b/karm/pics/Makefile.am new file mode 100644 index 000000000..c57c57a18 --- /dev/null +++ b/karm/pics/Makefile.am @@ -0,0 +1,10 @@ +pics_DATA = filedel.xpm clock.xpm clockedit.xpm empty-watch.xpm\ + watch-0.xpm watch-1.xpm watch-2.xpm watch-3.xpm \ + watch-4.xpm watch-5.xpm watch-6.xpm watch-7.xpm \ + active-icon-0.xpm active-icon-1.xpm active-icon-2.xpm \ + active-icon-3.xpm active-icon-4.xpm active-icon-5.xpm \ + active-icon-6.xpm active-icon-7.xpm \ + task-complete.xpm task-incomplete.xpm + +picsdir = $(kde_datadir)/karm/pics + diff --git a/karm/pics/active-icon-0.xpm b/karm/pics/active-icon-0.xpm new file mode 100644 index 000000000..e49a52eb4 --- /dev/null +++ b/karm/pics/active-icon-0.xpm @@ -0,0 +1,27 @@ +/* XPM */ +static char *hi16[]={ +"16 16 8 1", +". c None", +"# c #000000", +"b c #808080", +"f c #c00000", +"d c #c0c0c0", +"c c #c0c0ff", +"e c #ffc0c0", +"a c #ffffff", +".##############.", +"#aaaaaabaaaaaab#", +"#acaaaabaaaaacb#", +"#aaaaaabaaaaaab#", +"#adddd###addddb#", +"#acaaaabaa##acb#", +"#aaaaaded####db#", +"#aaaaadf#bd#b#a#", +"#aaaaaa#dba#bd##", +"#acaaa#bbaa#abb#", +"#aaaaa#daaa#aad#", +"#adddd#daaaaaad#", +"#acaaa#bbaaaabb#", +"#aaaaaa#dbaabdb#", +"#bbbbbbb#bddbbb#", +".##############."}; diff --git a/karm/pics/active-icon-1.xpm b/karm/pics/active-icon-1.xpm new file mode 100644 index 000000000..cb7375335 --- /dev/null +++ b/karm/pics/active-icon-1.xpm @@ -0,0 +1,27 @@ +/* XPM */ +static char *hi16[]={ +"16 16 8 1", +". c None", +"# c #000000", +"b c #808080", +"f c #c00000", +"d c #c0c0c0", +"c c #c0c0ff", +"e c #ffc0c0", +"a c #ffffff", +".##############.", +"#aaaaaabaaaaaab#", +"#acaaaabaaaaacb#", +"#aaaaaabaaaaaab#", +"#adddd###addddb#", +"#acaaaabaa##acb#", +"#aaaaaded####db#", +"#aaaaadf#bddb#a#", +"#aaaaaa#dbaab###", +"#acaaa#bbaaa#bb#", +"#aaaaa#daaa#aad#", +"#adddd#daaaaaad#", +"#acaaa#bbaaaabb#", +"#aaaaaa#dbaabdb#", +"#bbbbbbb#bddbbb#", +".##############."}; diff --git a/karm/pics/active-icon-2.xpm b/karm/pics/active-icon-2.xpm new file mode 100644 index 000000000..418d86b2d --- /dev/null +++ b/karm/pics/active-icon-2.xpm @@ -0,0 +1,27 @@ +/* XPM */ +static char *hi16[]={ +"16 16 8 1", +". c None", +"# c #000000", +"b c #808080", +"f c #c00000", +"d c #c0c0c0", +"c c #c0c0ff", +"e c #ffc0c0", +"a c #ffffff", +".##############.", +"#aaaaaabaaaaaab#", +"#acaaaabaaaaacb#", +"#aaaaaabaaaaaab#", +"#adddd###addddb#", +"#acaaaabaa##acb#", +"#aaaaaded####db#", +"#aaaaadf#bddb#a#", +"#aaaaaa#dbaabd##", +"#acaaa#bbaaaabb#", +"#aaaaa#daaa#####", +"#adddd#daaaaaad#", +"#acaaa#bbaaaabb#", +"#aaaaaa#dbaabdb#", +"#bbbbbbb#bddbbb#", +".##############."}; diff --git a/karm/pics/active-icon-3.xpm b/karm/pics/active-icon-3.xpm new file mode 100644 index 000000000..a18a95c63 --- /dev/null +++ b/karm/pics/active-icon-3.xpm @@ -0,0 +1,27 @@ +/* XPM */ +static char *hi16[]={ +"16 16 8 1", +". c None", +"# c #000000", +"b c #808080", +"f c #c00000", +"d c #c0c0c0", +"c c #c0c0ff", +"e c #ffc0c0", +"a c #ffffff", +".##############.", +"#aaaaaabaaaaaab#", +"#acaaaabaaaaacb#", +"#aaaaaabaaaaaab#", +"#adddd###addddb#", +"#acaaaabaa##acb#", +"#aaaaaded####db#", +"#aaaaadf#bddb#a#", +"#aaaaaa#dbaabd##", +"#acaaa#bbaaaabb#", +"#aaaaa#daaa#aad#", +"#adddd#daaaa#ad#", +"#acaaa#bbaaaa#b#", +"#aaaaaa#dbaabdb#", +"#bbbbbbb#bddbbb#", +".##############."}; diff --git a/karm/pics/active-icon-4.xpm b/karm/pics/active-icon-4.xpm new file mode 100644 index 000000000..5c6ba519d --- /dev/null +++ b/karm/pics/active-icon-4.xpm @@ -0,0 +1,27 @@ +/* XPM */ +static char *hi16[]={ +"16 16 8 1", +". c None", +"# c #000000", +"b c #808080", +"f c #c00000", +"d c #c0c0c0", +"c c #c0c0ff", +"e c #ffc0c0", +"a c #ffffff", +".##############.", +"#aaaaaabaaaaaab#", +"#acaaaabaaaaacb#", +"#aaaaaabaaaaaab#", +"#adddd###addddb#", +"#acaaaabaa##acb#", +"#aaaaaded####db#", +"#aaaaadf#bddb#a#", +"#aaaaaa#dbaabd##", +"#acaaa#bbaaaabb#", +"#aaaaa#daaa#aad#", +"#adddd#daaa#aad#", +"#acaaa#bbaa#abb#", +"#aaaaaa#dba#bdb#", +"#bbbbbbb#bddbbb#", +".##############."}; diff --git a/karm/pics/active-icon-5.xpm b/karm/pics/active-icon-5.xpm new file mode 100644 index 000000000..f86ce84b7 --- /dev/null +++ b/karm/pics/active-icon-5.xpm @@ -0,0 +1,27 @@ +/* XPM */ +static char *hi16[]={ +"16 16 8 1", +". c None", +"# c #000000", +"b c #808080", +"f c #c00000", +"d c #c0c0c0", +"c c #c0c0ff", +"e c #ffc0c0", +"a c #ffffff", +".##############.", +"#aaaaaabaaaaaab#", +"#acaaaabaaaaacb#", +"#aaaaaabaaaaaab#", +"#adddd###addddb#", +"#acaaaabaa##acb#", +"#aaaaaded####db#", +"#aaaaadf#bddb#a#", +"#aaaaaa#dbaabd##", +"#acaaa#bbaaaabb#", +"#aaaaa#daaa#aad#", +"#adddd#daa#aaad#", +"#acaaa#bb#aaabb#", +"#aaaaaa#dbaabdb#", +"#bbbbbbb#bddbbb#", +".##############."}; diff --git a/karm/pics/active-icon-6.xpm b/karm/pics/active-icon-6.xpm new file mode 100644 index 000000000..4c988350e --- /dev/null +++ b/karm/pics/active-icon-6.xpm @@ -0,0 +1,27 @@ +/* XPM */ +static char *hi16[]={ +"16 16 8 1", +". c None", +"# c #000000", +"b c #808080", +"f c #c00000", +"d c #c0c0c0", +"c c #c0c0ff", +"e c #ffc0c0", +"a c #ffffff", +".##############.", +"#aaaaaabaaaaaab#", +"#acaaaabaaaaacb#", +"#aaaaaabaaaaaab#", +"#adddd###addddb#", +"#acaaaabaa##acb#", +"#aaaaaded####db#", +"#aaaaadf#bddb#a#", +"#aaaaaa#dbaabd##", +"#acaaa#bbaaaabb#", +"#aaaaa#d####aad#", +"#adddd#daaaaaad#", +"#acaaa#bbaaaabb#", +"#aaaaaa#dbaabdb#", +"#bbbbbbb#bddbbb#", +".##############."}; diff --git a/karm/pics/active-icon-7.xpm b/karm/pics/active-icon-7.xpm new file mode 100644 index 000000000..c93242dfc --- /dev/null +++ b/karm/pics/active-icon-7.xpm @@ -0,0 +1,27 @@ +/* XPM */ +static char *hi16[]={ +"16 16 8 1", +". c None", +"# c #000000", +"b c #808080", +"f c #c00000", +"d c #c0c0c0", +"c c #c0c0ff", +"e c #ffc0c0", +"a c #ffffff", +".##############.", +"#aaaaaabaaaaaab#", +"#acaaaabaaaaacb#", +"#aaaaaabaaaaaab#", +"#adddd###addddb#", +"#acaaaabaa##acb#", +"#aaaaaded####db#", +"#aaaaadf#bddb#a#", +"#aaaaaa#d#aabd##", +"#acaaa#bba#aabb#", +"#aaaaa#daaa#aad#", +"#adddd#daaaaaad#", +"#acaaa#bbaaaabb#", +"#aaaaaa#dbaabdb#", +"#bbbbbbb#bddbbb#", +".##############."}; diff --git a/karm/pics/clock.xpm b/karm/pics/clock.xpm new file mode 100644 index 000000000..28b988564 --- /dev/null +++ b/karm/pics/clock.xpm @@ -0,0 +1,34 @@ +/* XPM */ +static char *magick[] = { +/* columns rows colors chars-per-pixel */ +"22 22 6 1", +" c #000000", +". c #008080", +"X c #808080", +"o c #c0c0c0", +"O c #ffffff", +"+ c None", +/* pixels */ +"++++++++++++++++++++++", +"+++++++++oooo+++++++++", +"++++++oooX..Xooo++++++", +"+++++oXXXO..OXXXo+++++", +"++++oXOOOO..OOOOXo++++", +"+++oXOOOOOOOOOOOOXo+++", +"++oXOOOOOOOOOOOOOOXo++", +"++oXOOOOOOOOOOOOOOXo++", +"++oXOOOOOOOOO.OOOOXo++", +"+oXOOOOOOOOO.OOOOOOXo+", +"+oXOOOOOOO..OOOOOOOXo+", +"+oXOOOOOOO..OOOOOOOXo+", +"+oXOOOOOOOOO.OOOOOOXo+", +"++oXOOOOOOOOO.OOOOXo++", +"++oXOOOOOOOOOO.OOOXo++", +"++oXOOOOOOOOOOO.OOXo++", +"+++oXOOOOOOOOOOOOXo+++", +"++++oXOOOOOOOOOOXo++++", +"+++++oXXXOOOOXXXo+++++", +"++++++oooXXXXooo++++++", +"+++++++++oooo+++++++++", +"++++++++++++++++++++++" +}; diff --git a/karm/pics/clockedit.xpm b/karm/pics/clockedit.xpm new file mode 100644 index 000000000..f24ee221d --- /dev/null +++ b/karm/pics/clockedit.xpm @@ -0,0 +1,37 @@ +/* XPM */ +static char *magick[] = { +/* columns rows colors chars-per-pixel */ +"22 22 9 1", +" c #000000", +". c #008080", +"X c #ff00ff", +"o c #808000", +"O c #ffff00", +"+ c #808080", +"@ c #c0c0c0", +"# c #ffffff", +"$ c None", +/* pixels */ +"$$$$$$$$$$$$$$$$$$$$$$", +"$$$$$$$$$@@@@$$$$$$$$$", +"$$$$$$@@@+..+@@@@@@$$$", +"$$$$$@++@@@@@@++@XXX$$", +"$$$$@+#@##..##@ooXXX@$", +"$$$@+@@######@ooooXX@$", +"$$@+#@######@ooOooo@$$", +"$$@+@######@oooOOoo@$$", +"$$@@######@ooOOOoo@@$$", +"$@+@#####@oooOOoo#@+@$", +"$@+@####@ooOOOoo##@+@$", +"$@+@###@oooOOoo###@+@$", +"$@+@##@ooOOOoo####@+@$", +"$$@@#@oooOOoo.####@@$$", +"$$@+@ooOOOoo##.##@+@$$", +"$$@+oooOOoo####.@#+@$$", +"$$$ooOOOoo#####@@+@$$$", +"$$ooOOOoo#####@#+@$$$$", +"$$ oOOoo@@@@@@++@$$$$$", +"$ ooooo@@++++@@@$$$$$$", +"$ oo o$$$@@@@$$$$$$$$$", +"$ $$$$$$$$$$$$$$$$$$" +}; diff --git a/karm/pics/empty-watch.xpm b/karm/pics/empty-watch.xpm new file mode 100644 index 000000000..8932686b1 --- /dev/null +++ b/karm/pics/empty-watch.xpm @@ -0,0 +1,19 @@ +/* XPM */ +static char * x_xpm[] = { +"15 15 1 1", +" c None", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/karm/pics/filedel.xpm b/karm/pics/filedel.xpm new file mode 100644 index 000000000..9c2b10138 --- /dev/null +++ b/karm/pics/filedel.xpm @@ -0,0 +1,31 @@ +/* XPM */ +static char * delete_xpm[] = { +/* width height num_colors chars_per_pixel */ +"22 22 3 1", +/* colors */ +" c white", +". c none", +"X c black", +/* pixels */ +"......................", +"......................", +"......................", +"......................", +"......................", +"....XX ........XX ....", +"....XXXX .....XX .....", +".....XXXX ...XX ......", +".......XXX .X ........", +"........XXXXX ........", +".........XXX .........", +"........XXXXX ........", +".......XXX .XX .......", +"......XXX ...XX ......", +".....XXX .....X ......", +".....XXX ......X .....", +"......X ..............", +"................X ....", +"......................", +"......................", +"......................", +"......................"}; diff --git a/karm/pics/task-complete.xpm b/karm/pics/task-complete.xpm new file mode 100644 index 000000000..a1fa6db82 --- /dev/null +++ b/karm/pics/task-complete.xpm @@ -0,0 +1,28 @@ +/* XPM */ +static char *task-complete[] = { +/* columns rows colors chars-per-pixel */ +"15 15 7 1", +" c black", +". c #004000", +"X c #008000", +"o c #00C000", +"O c green", +"+ c #C6C6C6", +"@ c None", +/* pixels */ +"@@@@@@@@@@@@@@@", +"@@@@@@@@@@@@Oo@", +"@@ OooX@", +"@@ @@@@@@ooXX.@", +"@@ @@@@@ooXX..@", +"@@ @@@@ooXX. @@", +"Oo @@@ooX.. @@", +"oXo@@ooX..@ @@", +".XXoooX..@@ @@", +"@.XXooX.@@@ @@", +"@@.XoX..@@@ @@", +"@@ XXX. @@", +"@@@.... @@", +"@@@@@@@@@@@@@@@", +"@@@@@@@@@@@@@@@" +}; diff --git a/karm/pics/task-incomplete.xpm b/karm/pics/task-incomplete.xpm new file mode 100644 index 000000000..c588039e9 --- /dev/null +++ b/karm/pics/task-incomplete.xpm @@ -0,0 +1,24 @@ +/* XPM */ +static char *task-incomplete[] = { +/* columns rows colors chars-per-pixel */ +"15 15 3 1", +" c black", +". c #C6C6C6", +"X c None", +/* pixels */ +"XXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXX", +"XX XXX", +"XX XXXXXXXX XX", +"XX XXXXXXXX XX", +"XX XXXXXXXX XX", +"XX XXXXXXXX XX", +"XX XXXXXXXX XX", +"XX XXXXXXXX XX", +"XX XXXXXXXX XX", +"XX XXXXXXXX XX", +"XX XX", +"XXX XX", +"XXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXX" +}; diff --git a/karm/pics/watch-0.xpm b/karm/pics/watch-0.xpm new file mode 100644 index 000000000..60f5d7e54 --- /dev/null +++ b/karm/pics/watch-0.xpm @@ -0,0 +1,25 @@ +/* XPM */ +static char * watch_0_xpm[] = { +"15 15 7 1", +" c None", +". c #000000", +"+ c #4A4A4A", +"@ c #ACACAC", +"# c #E9E9E9", +"$ c #989898", +"% c #FFFFFF", +" . ", +" +@#.#@+ ", +" $%%%.%%%$ ", +" $%%%%.%%%%$ ", +" +%%%%%.%%%%%+ ", +" @%%%%%.%%%%%@ ", +" #%%%%%.%%%%%# ", +".%%%%%%.%%%%%%.", +" #%%%%%%%%%%%# ", +" @%%%%%%%%%%%@ ", +" +%%%%%%%%%%%+ ", +" $%%%%%%%%%$ ", +" $%%%%%%%$ ", +" +@#%#@+ ", +" . "}; diff --git a/karm/pics/watch-1.xpm b/karm/pics/watch-1.xpm new file mode 100644 index 000000000..80450d54d --- /dev/null +++ b/karm/pics/watch-1.xpm @@ -0,0 +1,25 @@ +/* XPM */ +static char * watch_1_xpm[] = { +"15 15 7 1", +" c None", +". c #000000", +"+ c #4A4A4A", +"@ c #ACACAC", +"# c #E9E9E9", +"$ c #FFFFFF", +"% c #989898", +" . ", +" +@#$#@+ ", +" %$$$$$$$% ", +" %$$$$$$$$.% ", +" +$$$$$$$$.$$+ ", +" @$$$$$$$.$$$@ ", +" #$$$$$$.$$$$# ", +".$$$$$$.$$$$$$.", +" #$$$$$$$$$$$# ", +" @$$$$$$$$$$$@ ", +" +$$$$$$$$$$$+ ", +" %$$$$$$$$$% ", +" %$$$$$$$% ", +" +@#$#@+ ", +" . "}; diff --git a/karm/pics/watch-2.xpm b/karm/pics/watch-2.xpm new file mode 100644 index 000000000..5314d936b --- /dev/null +++ b/karm/pics/watch-2.xpm @@ -0,0 +1,25 @@ +/* XPM */ +static char * watch_2_xpm[] = { +"15 15 7 1", +" c None", +". c #000000", +"+ c #4A4A4A", +"@ c #ACACAC", +"# c #E9E9E9", +"$ c #FFFFFF", +"% c #989898", +" . ", +" +@#$#@+ ", +" %$$$$$$$% ", +" %$$$$$$$$$% ", +" +$$$$$$$$$$$+ ", +" @$$$$$$$$$$$@ ", +" #$$$$$$$$$$$# ", +".$$$$$$........", +" #$$$$$$$$$$$# ", +" @$$$$$$$$$$$@ ", +" +$$$$$$$$$$$+ ", +" %$$$$$$$$$% ", +" %$$$$$$$% ", +" +@#$#@+ ", +" . "}; diff --git a/karm/pics/watch-3.xpm b/karm/pics/watch-3.xpm new file mode 100644 index 000000000..0a6155647 --- /dev/null +++ b/karm/pics/watch-3.xpm @@ -0,0 +1,25 @@ +/* XPM */ +static char * watch_3_xpm[] = { +"15 15 7 1", +" c None", +". c #000000", +"+ c #4A4A4A", +"@ c #ACACAC", +"# c #E9E9E9", +"$ c #FFFFFF", +"% c #989898", +" . ", +" +@#$#@+ ", +" %$$$$$$$% ", +" %$$$$$$$$$% ", +" +$$$$$$$$$$$+ ", +" @$$$$$$$$$$$@ ", +" #$$$$$$$$$$$# ", +".$$$$$$.$$$$$$.", +" #$$$$$$.$$$$# ", +" @$$$$$$$.$$$@ ", +" +$$$$$$$$.$$+ ", +" %$$$$$$$$.% ", +" %$$$$$$$% ", +" +@#$#@+ ", +" . "}; diff --git a/karm/pics/watch-4.xpm b/karm/pics/watch-4.xpm new file mode 100644 index 000000000..6129b5f7a --- /dev/null +++ b/karm/pics/watch-4.xpm @@ -0,0 +1,25 @@ +/* XPM */ +static char * watch_4_xpm[] = { +"15 15 7 1", +" c None", +". c #000000", +"+ c #4A4A4A", +"@ c #ACACAC", +"# c #E9E9E9", +"$ c #FFFFFF", +"% c #989898", +" . ", +" +@#$#@+ ", +" %$$$$$$$% ", +" %$$$$$$$$$% ", +" +$$$$$$$$$$$+ ", +" @$$$$$$$$$$$@ ", +" #$$$$$$$$$$$# ", +".$$$$$$.$$$$$$.", +" #$$$$$.$$$$$# ", +" @$$$$$.$$$$$@ ", +" +$$$$$.$$$$$+ ", +" %$$$$.$$$$% ", +" %$$$.$$$% ", +" +@#.#@+ ", +" . "}; diff --git a/karm/pics/watch-5.xpm b/karm/pics/watch-5.xpm new file mode 100644 index 000000000..ce5797905 --- /dev/null +++ b/karm/pics/watch-5.xpm @@ -0,0 +1,25 @@ +/* XPM */ +static char * watch_5_xpm[] = { +"15 15 7 1", +" c None", +". c #000000", +"+ c #4A4A4A", +"@ c #ACACAC", +"# c #E9E9E9", +"$ c #FFFFFF", +"% c #989898", +" . ", +" +@#$#@+ ", +" %$$$$$$$% ", +" %$$$$$$$$$% ", +" +$$$$$$$$$$$+ ", +" @$$$$$$$$$$$@ ", +" #$$$$$$$$$$$# ", +".$$$$$$.$$$$$$.", +" #$$$$.$$$$$$# ", +" @$$$.$$$$$$$@ ", +" +$$.$$$$$$$$+ ", +" %.$$$$$$$$% ", +" %$$$$$$$% ", +" +@#$#@+ ", +" . "}; diff --git a/karm/pics/watch-6.xpm b/karm/pics/watch-6.xpm new file mode 100644 index 000000000..25323ec62 --- /dev/null +++ b/karm/pics/watch-6.xpm @@ -0,0 +1,25 @@ +/* XPM */ +static char * watch_6_xpm[] = { +"15 15 7 1", +" c None", +". c #000000", +"+ c #4A4A4A", +"@ c #ACACAC", +"# c #E9E9E9", +"$ c #FFFFFF", +"% c #989898", +" . ", +" +@#$#@+ ", +" %$$$$$$$% ", +" %$$$$$$$$$% ", +" +$$$$$$$$$$$+ ", +" @$$$$$$$$$$$@ ", +" #$$$$$$$$$$$# ", +"........$$$$$$.", +" #$$$$$$$$$$$# ", +" @$$$$$$$$$$$@ ", +" +$$$$$$$$$$$+ ", +" %$$$$$$$$$% ", +" %$$$$$$$% ", +" +@#$#@+ ", +" . "}; diff --git a/karm/pics/watch-7.xpm b/karm/pics/watch-7.xpm new file mode 100644 index 000000000..35d962dc7 --- /dev/null +++ b/karm/pics/watch-7.xpm @@ -0,0 +1,25 @@ +/* XPM */ +static char * watch_7_xpm[] = { +"15 15 7 1", +" c None", +". c #000000", +"+ c #4A4A4A", +"@ c #ACACAC", +"# c #E9E9E9", +"$ c #FFFFFF", +"% c #989898", +" . ", +" +@#$#@+ ", +" %$$$$$$$% ", +" %.$$$$$$$$% ", +" +$$.$$$$$$$$+ ", +" @$$$.$$$$$$$@ ", +" #$$$$.$$$$$$# ", +".$$$$$$.$$$$$$.", +" #$$$$$$$$$$$# ", +" @$$$$$$$$$$$@ ", +" +$$$$$$$$$$$+ ", +" %$$$$$$$$$% ", +" %$$$$$$$% ", +" +@#$#@+ ", +" . "}; diff --git a/karm/plannerparser.cpp b/karm/plannerparser.cpp new file mode 100644 index 000000000..21e0c4f76 --- /dev/null +++ b/karm/plannerparser.cpp @@ -0,0 +1,100 @@ +// +// C++ Implementation: plannerparser +// +// Description: +/* + +this class is here to import tasks from a planner project file to karm. +the import shall not be limited to karm (kPlaTo sends greetings) +it imports planner's top-level-tasks on the same level-depth as current_item. +if there is no current_item, planner's top-level-tasks will become top-level-tasks in karm. +it imports as well the level-depth of each task, as its name, as its percent-complete. +test cases: + - deleting all tasks away, then import! + - having started with an empty ics, import! + - with current_item being a top-level-task, import! + - with current_item being a subtask, import! + + + Author: Thorsten Staerk <[email protected]>, (C) 2004 + + Copyright: See COPYING file that comes with this distribution + + +*/ + +#include "plannerparser.h" + + + PlannerParser::PlannerParser(TaskView * tv) + // if there is a task one level above current_item, make it the father of all imported tasks. Set level accordingly. + // import as well if there a no task in the taskview as if there are. + // if there are, put the top-level tasks of planner on the same level as current_item. + // So you have the chance as well to have the planner tasks at top-level as at a whatever-so-deep sublevel. + { + kdDebug() << "entering constructor to import planner tasks" << endl; + _taskView=tv; + level=0; + if (_taskView->current_item()) if (_taskView->current_item()->parent()) + { + task = _taskView->current_item()->parent(); + level=1; + } + } + + bool PlannerParser::startDocument() + { + withInTasks=false; // becomes true as soon as parsing occurres <tasks> + return true; + } + + bool PlannerParser::startElement( const QString&, const QString&, const QString& qName, const QXmlAttributes& att ) + { + kdDebug() << "entering startElement" << endl; + QString taskName; + int taskComplete=0; + + // only <task>s within <tasks> are processed + if (qName == QString::fromLatin1("tasks")) withInTasks=true; + if ((qName == QString::fromLatin1("task")) && (withInTasks)) + { + + // find out name and percent-complete + for (int i=0; i<att.length(); i++) + { + if (att.qName(i) == QString::fromLatin1("name")) taskName=att.value(i); + if (att.qName(i)==QString::fromLatin1("percent-complete")) taskComplete=att.value(i).toInt(); + } + + // at the moment, task is still the old task or the old father task (if an endElement occurred) or not existing (if the + // new task is a top-level-task). Make task the parenttask, if existing. + DesktopList dl; + if (level++>0) + { + parentTask=task; + task = new Task(taskName, 0, 0, dl, parentTask); + task->setUid(_taskView->storage()->addTask(task, parentTask)); + } + else + { + task = new Task(taskName, 0, 0, dl, _taskView); + kdDebug() << "added" << taskName << endl; + task->setUid(_taskView->storage()->addTask(task, 0)); + } + + task->setPercentComplete(taskComplete, _taskView->storage()); + } + return true; + } + + bool PlannerParser::endElement( const QString&, const QString&, const QString& qName) + { + // only <task>s within <tasks> increased level, so only decrease for <task>s within <tasks> + if (withInTasks) + { + if (qName=="task") if (level-->=0) task=task->parent(); + if (qName=="tasks") withInTasks=false; + } + return true; + } + diff --git a/karm/plannerparser.h b/karm/plannerparser.h new file mode 100644 index 000000000..a626d6f03 --- /dev/null +++ b/karm/plannerparser.h @@ -0,0 +1,62 @@ +// +// C++ Interface: plannerparser +// +// Description: +// +// +// Author: Thorsten Staerk <[email protected]>, (C) 2004 +// +// Copyright: See COPYING file that comes with this distribution +// +// +#ifndef PLANNERPARSER_H +#define PLANNERPARSER_H + +/** +this class is here to import tasks from a planner project file to karm. +the import shall not be limited to karm (kPlaTo sends greetings) +it imports planner's top-level-tasks on the same level-depth as current_item. +if there is no current_item, planner's top-level-tasks will become top-level-tasks in karm. +it imports as well the level-depth of each task, as its name, as its percent-complete. +test cases: + - deleting all tasks away, then import! + - having started with an empty ics, import! + - with current_item being a top-level-task, import! + - with current_item being a subtask, import! + +@author Thorsten Staerk +*/ + +#include <qxml.h> +#include <klocale.h> +#include "taskview.h" +#include "task.h" +#include "karmstorage.h" +#include "kapplication.h" + +class PlannerParser : public QXmlDefaultHandler +{ +public: + + /** Stores the active TaskView in this parser. Returns error code (not always, hopefully) */ + PlannerParser(TaskView * tv); + + /** given by the framework from qxml. Called when parsing the xml-document starts. */ + bool startDocument(); + + /** given by the framework from qxml. Called when the reader occurs an open tag (e.g. \<b\> ) */ + bool startElement( const QString&, const QString&, const QString& qName, const QXmlAttributes& att ); + + /** given by the framework from qxml. Called when the reader occurs a closed tag (e.g. \</b\> )*/ + bool endElement( const QString&, const QString&, const QString& qName); + +private: + bool withInTasks; // within <tasks> ? + TaskView *_taskView; + Task *task; + Task *parentTask; + int level; // level=1: task is top-level-task +}; + + +#endif diff --git a/karm/preferences.cpp b/karm/preferences.cpp new file mode 100644 index 000000000..9316aa440 --- /dev/null +++ b/karm/preferences.cpp @@ -0,0 +1,335 @@ +#undef Unsorted // for --enable-final +#include <qcheckbox.h> +#include <qlabel.h> +#include <qstring.h> +#include <qspinbox.h> +#include <qlayout.h> + +#include <kapplication.h> // kapp +#include <kconfig.h> +#include <kdebug.h> +#include <kemailsettings.h> +#include <kiconloader.h> +#include <klineedit.h> // lineEdit() +#include <klocale.h> // i18n +#include <kstandarddirs.h> +#include <kurlrequester.h> + +#include "preferences.h" + +Preferences *Preferences::_instance = 0; + +Preferences::Preferences( const QString& icsFile ) + : KDialogBase( IconList, i18n("Preferences"), Ok|Cancel, Ok ) +{ + + setIconListAllVisible( true ); + + makeBehaviorPage(); + makeDisplayPage(); + makeStoragePage(); + + load(); + + // command-line option overrides what is stored in + if ( ! icsFile.isEmpty() ) _iCalFileV = icsFile; + +} + +Preferences *Preferences::instance( const QString &icsfile ) +{ + if (_instance == 0) { + _instance = new Preferences( icsfile ); + } + return _instance; +} + +void Preferences::makeBehaviorPage() +{ + QPixmap icon = SmallIcon( "kcmsystem", KIcon::SizeMedium); + QFrame* behaviorPage = addPage( i18n("Behavior"), i18n("Behavior Settings"), + icon ); + + QVBoxLayout* topLevel = new QVBoxLayout( behaviorPage, 0, spacingHint() ); + QGridLayout* layout = new QGridLayout( topLevel, 2, 2 ); + layout->setColStretch( 1, 1 ); + + _doIdleDetectionW = new QCheckBox + ( i18n("Detect desktop as idle after"), behaviorPage, "_doIdleDetectionW"); + _idleDetectValueW = new QSpinBox + (1,60*24, 1, behaviorPage, "_idleDetectValueW"); + _idleDetectValueW->setSuffix(i18n(" min")); + _promptDeleteW = new QCheckBox + ( i18n( "Prompt before deleting tasks" ), behaviorPage, "_promptDeleteW" ); + + layout->addWidget(_doIdleDetectionW, 0, 0 ); + layout->addWidget(_idleDetectValueW, 0, 1 ); + layout->addWidget(_promptDeleteW, 1, 0 ); + + topLevel->addStretch(); + + connect( _doIdleDetectionW, SIGNAL( clicked() ), this, + SLOT( idleDetectCheckBoxChanged() )); +} + +void Preferences::makeDisplayPage() +{ + QPixmap icon = SmallIcon( "viewmag", KIcon::SizeMedium ); + QFrame* displayPage = addPage( i18n("Display"), i18n("Display Settings"), + icon ); + + QVBoxLayout* topLevel = new QVBoxLayout( displayPage, 0, spacingHint() ); + QGridLayout* layout = new QGridLayout( topLevel, 5, 2 ); + layout->setColStretch( 1, 1 ); + + QLabel* _displayColumnsLabelW = new QLabel( i18n("Columns displayed:"), + displayPage ); + _displaySessionW = new QCheckBox ( i18n("Session time"), + displayPage, "_displaySessionW"); + _displayTimeW = new QCheckBox ( i18n("Cumulative task time"), + displayPage, "_displayTimeW"); + _displayTotalSessionW = new QCheckBox( i18n("Total session time"), + displayPage, "_displayTotalSessionW"); + _displayTotalTimeW = new QCheckBox ( i18n("Total task time"), + displayPage, "_displayTotalTimeW"); + + layout->addMultiCellWidget( _displayColumnsLabelW, 0, 0, 0, 1 ); + layout->addWidget(_displaySessionW, 1, 1 ); + layout->addWidget(_displayTimeW, 2, 1 ); + layout->addWidget(_displayTotalSessionW, 3, 1 ); + layout->addWidget(_displayTotalTimeW, 4, 1 ); + + topLevel->addStretch(); +} + +void Preferences::makeStoragePage() +{ + QPixmap icon = SmallIcon( "kfm", KIcon::SizeMedium ); + QFrame* storagePage = addPage( i18n("Storage"), i18n("Storage Settings"), + icon ); + + QVBoxLayout* topLevel = new QVBoxLayout( storagePage, 0, spacingHint() ); + QGridLayout* layout = new QGridLayout( topLevel, 4, 2 ); + layout->setColStretch( 1, 1 ); + + // autosave + _doAutoSaveW = new QCheckBox + ( i18n("Save tasks every"), storagePage, "_doAutoSaveW" ); + _autoSaveValueW = new QSpinBox(1, 60*24, 1, storagePage, "_autoSaveValueW"); + _autoSaveValueW->setSuffix(i18n(" min")); + + // iCalendar + QLabel* _iCalFileLabel = new QLabel( i18n("iCalendar file:"), storagePage); + _iCalFileW = new KURLRequester(storagePage, "_iCalFileW"); + _iCalFileW->setFilter(QString::fromLatin1("*.ics")); + _iCalFileW->setMode(KFile::File); + + // Log time? + _loggingW = new QCheckBox + ( i18n("Log history"), storagePage, "_loggingW" ); + + // add widgets to layout + layout->addWidget(_doAutoSaveW, 0, 0); + layout->addWidget(_autoSaveValueW, 0, 1); + layout->addWidget(_iCalFileLabel, 1, 0 ); + layout->addWidget(_iCalFileW, 1, 1 ); + layout->addWidget(_loggingW, 2, 0 ); + + topLevel->addStretch(); + + // checkboxes disable file selection controls + connect( _doAutoSaveW, SIGNAL( clicked() ), + this, SLOT( autoSaveCheckBoxChanged() )); +} + +void Preferences::disableIdleDetection() +{ + _doIdleDetectionW->setEnabled(false); +} + + +//--------------------------------------------------------------------------- +// SLOTS +//--------------------------------------------------------------------------- + +void Preferences::showDialog() +{ + + // set all widgets + _iCalFileW->lineEdit()->setText(_iCalFileV); + + _doIdleDetectionW->setChecked(_doIdleDetectionV); + _idleDetectValueW->setValue(_idleDetectValueV); + + _doAutoSaveW->setChecked(_doAutoSaveV); + _autoSaveValueW->setValue(_autoSaveValueV); + _loggingW->setChecked(_loggingV); + + _promptDeleteW->setChecked(_promptDeleteV); + + _displaySessionW->setChecked(_displayColumnV[0]); + _displayTimeW->setChecked(_displayColumnV[1]); + _displayTotalSessionW->setChecked(_displayColumnV[2]); + _displayTotalTimeW->setChecked(_displayColumnV[3]); + + // adapt visibility of preference items according + // to settings + idleDetectCheckBoxChanged(); + autoSaveCheckBoxChanged(); + + show(); +} + +void Preferences::slotOk() +{ + kdDebug(5970) << "Entering Preferences::slotOk" << endl; + // storage + _iCalFileV = _iCalFileW->lineEdit()->text(); + + _doIdleDetectionV = _doIdleDetectionW->isChecked(); + _idleDetectValueV = _idleDetectValueW->value(); + + _doAutoSaveV = _doAutoSaveW->isChecked(); + _autoSaveValueV = _autoSaveValueW->value(); + _loggingV = _loggingW->isChecked(); + + // behavior + _promptDeleteV = _promptDeleteW->isChecked(); + + // display + _displayColumnV[0] = _displaySessionW->isChecked(); + _displayColumnV[1] = _displayTimeW->isChecked(); + _displayColumnV[2] = _displayTotalSessionW->isChecked(); + _displayColumnV[3] = _displayTotalTimeW->isChecked(); + + emitSignals(); + save(); + KDialogBase::slotOk(); +} + +void Preferences::slotCancel() +{ + kdDebug(5970) << "Entering Preferences::slotCancel" << endl; + KDialogBase::slotCancel(); +} + +void Preferences::idleDetectCheckBoxChanged() +{ + _idleDetectValueW->setEnabled(_doIdleDetectionW->isChecked()); +} + +void Preferences::autoSaveCheckBoxChanged() +{ + _autoSaveValueW->setEnabled(_doAutoSaveW->isChecked()); +} + +void Preferences::emitSignals() +{ + kdDebug(5970) << "Entering Preferences::emitSignals" << endl; + emit iCalFile( _iCalFileV ); + emit detectIdleness( _doIdleDetectionV ); + emit idlenessTimeout( _idleDetectValueV ); + emit autoSave( _doAutoSaveV ); + emit autoSavePeriod( _autoSaveValueV ); + emit setupChanged(); +} + +QString Preferences::iCalFile() const { return _iCalFileV; } +QString Preferences::activeCalendarFile() const { return _iCalFileV; } +bool Preferences::detectIdleness() const { return _doIdleDetectionV; } +int Preferences::idlenessTimeout() const { return _idleDetectValueV; } +bool Preferences::autoSave() const { return _doAutoSaveV; } +int Preferences::autoSavePeriod() const { return _autoSaveValueV; } +bool Preferences::logging() const { return _loggingV; } +bool Preferences::promptDelete() const { return _promptDeleteV; } +QString Preferences::setPromptDelete(bool prompt) { _promptDeleteV=prompt; return ""; } +bool Preferences::displayColumn(int n) const { return _displayColumnV[n]; } +QString Preferences::userRealName() const { return _userRealName; } + +//--------------------------------------------------------------------------- +// Load and Save +//--------------------------------------------------------------------------- +void Preferences::load() +{ + KConfig &config = *kapp->config(); + + config.setGroup( QString::fromLatin1("Idle detection") ); + _doIdleDetectionV = config.readBoolEntry( QString::fromLatin1("enabled"), + true ); + _idleDetectValueV = config.readNumEntry(QString::fromLatin1("period"), 15); + + config.setGroup( QString::fromLatin1("Saving") ); + _iCalFileV = config.readPathEntry + ( QString::fromLatin1("ical file"), + locateLocal( "appdata", QString::fromLatin1( "karm.ics"))); + _doAutoSaveV = config.readBoolEntry + ( QString::fromLatin1("auto save"), true); + _autoSaveValueV = config.readNumEntry + ( QString::fromLatin1("auto save period"), 5); + _promptDeleteV = config.readBoolEntry + ( QString::fromLatin1("prompt delete"), true); + _loggingV = config.readBoolEntry + ( QString::fromLatin1("logging"), true); + + _displayColumnV[0] = config.readBoolEntry + ( QString::fromLatin1("display session time"), true); + _displayColumnV[1] = config.readBoolEntry + ( QString::fromLatin1("display time"), true); + _displayColumnV[2] = config.readBoolEntry + ( QString::fromLatin1("display total session time"), true); + _displayColumnV[3] = config.readBoolEntry + ( QString::fromLatin1("display total time"), true); + + KEMailSettings settings; + _userRealName = settings.getSetting( KEMailSettings::RealName ); +} + +void Preferences::save() +{ + KConfig &config = *KGlobal::config(); + + config.setGroup( QString::fromLatin1("Idle detection")); + config.writeEntry( QString::fromLatin1("enabled"), _doIdleDetectionV); + config.writeEntry( QString::fromLatin1("period"), _idleDetectValueV); + + config.setGroup( QString::fromLatin1("Saving")); + config.writePathEntry( QString::fromLatin1("ical file"), _iCalFileV); + config.writeEntry( QString::fromLatin1("auto save"), _doAutoSaveV); + config.writeEntry( QString::fromLatin1("logging"), _loggingV); + config.writeEntry( QString::fromLatin1("auto save period"), _autoSaveValueV); + config.writeEntry( QString::fromLatin1("prompt delete"), _promptDeleteV); + + config.writeEntry( QString::fromLatin1("display session time"), + _displayColumnV[0]); + config.writeEntry( QString::fromLatin1("display time"), + _displayColumnV[1]); + config.writeEntry( QString::fromLatin1("display total session time"), + _displayColumnV[2]); + config.writeEntry( QString::fromLatin1("display total time"), + _displayColumnV[3]); + + config.sync(); +} + +// HACK: this entire config dialog should be upgraded to KConfigXT +bool Preferences::readBoolEntry( const QString& key ) +{ + KConfig &config = *KGlobal::config(); + return config.readBoolEntry ( key, true ); +} + +void Preferences::writeEntry( const QString &key, bool value) +{ + KConfig &config = *KGlobal::config(); + config.writeEntry( key, value ); + config.sync(); +} + +void Preferences::deleteEntry( const QString &key ) +{ + KConfig &config = *KGlobal::config(); + config.deleteEntry( key ); + config.sync(); +} + +#include "preferences.moc" diff --git a/karm/preferences.h b/karm/preferences.h new file mode 100644 index 000000000..e917d70bb --- /dev/null +++ b/karm/preferences.h @@ -0,0 +1,90 @@ +#ifndef KARM_PREFERENCES_H +#define KARM_PREFERENCES_H + +#include <kdialogbase.h> + +class QCheckBox; +class QLabel; +class QSpinBox; +class QString; +class KURLRequester; + +/** + * Provide an interface to the configuration options for the program. + */ + +class Preferences :public KDialogBase +{ + Q_OBJECT + + public: + static Preferences *instance( const QString& icsfile = "" ); + void disableIdleDetection(); + + // Retrive information about settings + bool detectIdleness() const; + int idlenessTimeout() const; + QString iCalFile() const; + QString activeCalendarFile() const; + bool autoSave() const; + bool logging() const; + int autoSavePeriod() const; + bool promptDelete() const; + QString setPromptDelete( bool prompt ); + bool displayColumn(int n) const; + QString userRealName() const; + + void emitSignals(); + bool readBoolEntry( const QString& uid ); + void writeEntry( const QString &key, bool value ); + void deleteEntry( const QString &key ); + + public slots: + void showDialog(); + void load(); + void save(); + + signals: + void detectIdleness(bool on); + void idlenessTimeout(int minutes); + void iCalFile(QString); + void autoSave(bool on); + void autoSavePeriod(int minutes); + void setupChanged(); + + protected slots: + virtual void slotOk(); + virtual void slotCancel(); + void idleDetectCheckBoxChanged(); + void autoSaveCheckBoxChanged(); + + private: + void makeDisplayPage(); + void makeBehaviorPage(); + void makeStoragePage(); + + Preferences( const QString& icsfile = "" ); + static Preferences *_instance; + bool _unsavedChanges; + + // Widgets + QCheckBox *_doIdleDetectionW, *_doAutoSaveW, *_promptDeleteW; + QCheckBox *_displayTimeW, *_displaySessionW, + *_displayTotalTimeW, *_displayTotalSessionW; + QCheckBox *_loggingW; + QLabel *_idleDetectLabelW, *_displayColumnsLabelW; + QSpinBox *_idleDetectValueW, *_autoSaveValueW; + KURLRequester *_iCalFileW ; + + // Values + bool _doIdleDetectionV, _doAutoSaveV, _promptDeleteV, _loggingV; + bool _displayColumnV[4]; + int _idleDetectValueV, _autoSaveValueV; + QString _iCalFileV; + + /** real name of the user, used during ICAL saving */ + QString _userRealName; +}; + +#endif // KARM_PREFERENCES_H + diff --git a/karm/print.cpp b/karm/print.cpp new file mode 100644 index 000000000..ff00a3bc3 --- /dev/null +++ b/karm/print.cpp @@ -0,0 +1,166 @@ +// #include <iostream> + +#include <qdatetime.h> +#include <qpaintdevicemetrics.h> +#include <qpainter.h> + +#include <kglobal.h> +#include <klocale.h> // i18n + +#include "karmutility.h" // formatTime() +#include "print.h" +#include "task.h" +#include "taskview.h" + +const int levelIndent = 10; + +MyPrinter::MyPrinter(const TaskView *taskView) +{ + _taskView = taskView; +} + +void MyPrinter::print() +{ + // FIXME: make a better caption for the printingdialog + if (setup(0L, i18n("Print Times"))) { + // setup + QPainter painter(this); + QPaintDeviceMetrics deviceMetrics(this); + QFontMetrics metrics = painter.fontMetrics(); + pageHeight = deviceMetrics.height(); + int pageWidth = deviceMetrics.width(); + xMargin = margins().width(); + yMargin = margins().height(); + yoff = yMargin; + lineHeight = metrics.height(); + + // Calculate the totals + // Note the totals are only calculated at the top most levels, as the + // totals are increased together with its children. + int totalTotal = 0; + int sessionTotal = 0; + for (Task* task = _taskView->first_child(); + task; + task = static_cast<Task *>(task->nextSibling())) { + totalTotal += task->totalTime(); + sessionTotal += task->totalSessionTime(); + } + + // Calculate the needed width for each of the fields + timeWidth = QMAX(metrics.width(i18n("Total")), + metrics.width(formatTime(totalTotal))); + sessionTimeWidth = QMAX(metrics.width(i18n("Session")), + metrics.width(formatTime(sessionTotal))); + + nameFieldWidth = pageWidth - xMargin - timeWidth - sessionTimeWidth - 2*5; + + int maxReqNameFieldWidth= metrics.width(i18n("Task Name ")); + + for ( Task* task = _taskView->first_child(); + task; + task = static_cast<Task *>(task->nextSibling())) + { + int width = calculateReqNameWidth(task, metrics, 0); + maxReqNameFieldWidth = QMAX(maxReqNameFieldWidth, width); + } + nameFieldWidth = QMIN(nameFieldWidth, maxReqNameFieldWidth); + + int realPageWidth = nameFieldWidth + timeWidth + sessionTimeWidth + 2*5; + + // Print the header + QFont origFont, newFont; + origFont = painter.font(); + newFont = origFont; + newFont.setPixelSize( static_cast<int>(origFont.pixelSize() * 1.5) ); + painter.setFont(newFont); + + int height = metrics.height(); + QString now = KGlobal::locale()->formatDateTime(QDateTime::currentDateTime()); + + painter.drawText(xMargin, yoff, pageWidth, height, + QPainter::AlignCenter, + i18n("KArm - %1").arg(now)); + + painter.setFont(origFont); + yoff += height + 10; + + // Print the second header. + printLine(i18n("Total"), i18n("Session"), i18n("Task Name"), painter, 0); + + yoff += 4; + painter.drawLine(xMargin, yoff, xMargin + realPageWidth, yoff); + yoff += 2; + + // Now print the actual content + for ( Task* task = _taskView->first_child(); + task; + task = static_cast<Task *>(task->nextSibling()) ) + { + printTask(task, painter, 0); + } + + yoff += 4; + painter.drawLine(xMargin, yoff, xMargin + realPageWidth, yoff); + yoff += 2; + + // Print the Totals + printLine( formatTime( totalTotal ), + formatTime( sessionTotal ), + QString(), painter, 0); + } +} + +int MyPrinter::calculateReqNameWidth( Task* task, + QFontMetrics &metrics, + int level) +{ + int width = metrics.width(task->name()) + level * levelIndent; + + for ( Task* subTask = task->firstChild(); + subTask; + subTask = subTask->nextSibling() ) { + int subTaskWidth = calculateReqNameWidth(subTask, metrics, level+1); + width = QMAX(width, subTaskWidth); + } + return width; +} + +void MyPrinter::printTask(Task *task, QPainter &painter, int level) +{ + QString time = formatTime(task->totalTime()); + QString sessionTime = formatTime(task->totalSessionTime()); + QString name = task->name(); + printLine(time, sessionTime, name, painter, level); + + for ( Task* subTask = task->firstChild(); + subTask; + subTask = subTask->nextSibling()) + { + printTask(subTask, painter, level+1); + } +} + +void MyPrinter::printLine( QString total, QString session, QString name, + QPainter &painter, int level ) +{ + int xoff = xMargin + 10 * level; + + painter.drawText( xoff, yoff, nameFieldWidth, lineHeight, + QPainter::AlignLeft, name); + xoff = xMargin + nameFieldWidth; + + painter.drawText( xoff, yoff, sessionTimeWidth, lineHeight, + QPainter::AlignRight, session); + xoff += sessionTimeWidth+ 5; + + painter.drawText( xoff, yoff, timeWidth, lineHeight, + QPainter::AlignRight, total); + xoff += timeWidth+5; + + yoff += lineHeight; + + if (yoff + 2* lineHeight > pageHeight) { + newPage(); + yoff = yMargin; + } +} diff --git a/karm/print.h b/karm/print.h new file mode 100644 index 000000000..5c51a2c45 --- /dev/null +++ b/karm/print.h @@ -0,0 +1,41 @@ +#ifndef KARM_PRINT_H +#define KARM_PRINT_H + +#undef Color // X11 headers +#undef GrayScale // X11 headers +#include <kprinter.h> + +class QPainter; +class QString; + +class Task; +class TaskView; + +/** + * Provide printing capabilities. + */ + +class MyPrinter : public KPrinter +{ + public: + MyPrinter( const TaskView *taskView ); + void print(); + void printLine( QString total, QString session, QString name, QPainter &, + int ); + void printTask( Task *task, QPainter &, int level ); + int calculateReqNameWidth( Task *task, QFontMetrics &metrics, + int level); + + private: + const TaskView *_taskView; + + int xMargin, yMargin; + int yoff; + int timeWidth; + int sessionTimeWidth; + int nameFieldWidth; + int lineHeight; + int pageHeight; +}; + +#endif // KARM_PRINT_H diff --git a/karm/printdialog.cpp b/karm/printdialog.cpp new file mode 100644 index 000000000..b2e4635d9 --- /dev/null +++ b/karm/printdialog.cpp @@ -0,0 +1,117 @@ +/* + * This file only: + * Copyright (C) 2003 Mark Bucciarelli <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ + +#include <qbuttongroup.h> +#include <qcheckbox.h> +#include <qhbox.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qlineedit.h> +#include <qpixmap.h> +#include <qpushbutton.h> +#include <qstring.h> +#include <qwidget.h> +#include <qwhatsthis.h> + +#include <kiconloader.h> +#include <klocale.h> // i18n +#include <kwinmodule.h> + +#include "printdialog.h" +#include <libkdepim/kdateedit.h> + + +PrintDialog::PrintDialog() + : KDialogBase(0, "PrintDialog", true, i18n("Print Dialog"), Ok|Cancel, + Ok, true ) +{ + QWidget *page = new QWidget( this ); + setMainWidget(page); + int year, month; + + QVBoxLayout *layout = new QVBoxLayout(page, KDialog::spacingHint()); + layout->addSpacing(10); + layout->addStretch(1); + + // Date Range + QGroupBox *rangeGroup = new QGroupBox(1, Horizontal, i18n("Date Range"), + page); + layout->addWidget(rangeGroup); + + QWidget *rangeWidget = new QWidget(rangeGroup); + QHBoxLayout *rangeLayout = new QHBoxLayout(rangeWidget, 0, spacingHint()); + + rangeLayout->addWidget(new QLabel(i18n("From:"), rangeWidget)); + _from = new KDateEdit(rangeWidget); + + // Default from date to beginning of the month + year = QDate::currentDate().year(); + month = QDate::currentDate().month(); + _from->setDate(QDate(year, month, 1)); + rangeLayout->addWidget(_from); + rangeLayout->addWidget(new QLabel(i18n("To:"), rangeWidget)); + _to = new KDateEdit(rangeWidget); + rangeLayout->addWidget(_to); + + layout->addSpacing(10); + layout->addStretch(1); + + _allTasks = new QComboBox( page ); + _allTasks->insertItem( i18n( "Selected Task" ) ); + _allTasks->insertItem( i18n( "All Tasks" ) ); + layout->addWidget( _allTasks ); + + _perWeek = new QCheckBox( i18n( "Summarize per week" ), page ); + layout->addWidget( _perWeek ); + _totalsOnly = new QCheckBox( i18n( "Totals only" ), page ); + layout->addWidget( _totalsOnly ); + + layout->addSpacing(10); + layout->addStretch(1); +} + +QDate PrintDialog::from() const +{ + return _from->date(); +} + +QDate PrintDialog::to() const +{ + return _to->date(); +} + +bool PrintDialog::perWeek() const +{ + return _perWeek->isChecked(); +} + +bool PrintDialog::allTasks() const +{ + return _allTasks->currentItem() == 1; +} + +bool PrintDialog::totalsOnly() const +{ + return _totalsOnly->isChecked(); +} + +#include "printdialog.moc" diff --git a/karm/printdialog.h b/karm/printdialog.h new file mode 100644 index 000000000..6c2c4c581 --- /dev/null +++ b/karm/printdialog.h @@ -0,0 +1,61 @@ +/* + * This file only: + * Copyright (C) 2003 Mark Bucciarelli <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ +#ifndef KARM_PRINT_DIALOG_H +#define KARM_PRINT_DIALOG_H + +#include <kdialogbase.h> +#include <libkdepim/kdateedit.h> + +class QCheckBox; +class KDateEdit; + +class PrintDialog : public KDialogBase +{ + Q_OBJECT + + public: + PrintDialog(); + + /* Return the from date entered. */ + QDate from() const; + + /* Return the to date entered. */ + QDate to() const; + + /* Whether to summarize per week */ + bool perWeek() const; + + /* Whether to print all tasks */ + bool allTasks() const; + + /* Whether to print totals only, instead of per-day columns */ + bool totalsOnly() const; + +private: + KDateEdit *_from, *_to; + QCheckBox *_perWeek; + QComboBox *_allTasks; + QCheckBox *_totalsOnly; +}; + +#endif // KARM_PRINT_DIALOG_H + diff --git a/karm/reportcriteria.h b/karm/reportcriteria.h new file mode 100644 index 000000000..66b5b8a02 --- /dev/null +++ b/karm/reportcriteria.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2004 Mark Bucciarelli <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ +#ifndef REPORTCRITERIA_H +#define REPORTCRITERIA_H + +#include <qdatetime.h> +#include <kurl.h> +class QString; + +/** + Stores entries from export dialog. + + Keeps details (like CSV export dialog control names) out of the TaskView + class, which contains the slot triggered by the export action. + + The dialog and the report logic can change all they want and the TaskView + logic can stay the same. + */ + +class ReportCriteria +{ + public: + + /** + The different report types. + */ + enum REPORTTYPE { CSVTotalsExport = 0, CSVHistoryExport = 1 }; + + /** + The type of report we are running. + */ + REPORTTYPE reportType; + + /** + For reports that write to a file, the filename to write to. + */ + KURL url; + + /** + For history reports, the lower bound of the date range to report on. + */ + QDate from; + + /** + For history reports, the upper bound of the date range to report on. + */ + QDate to; + + /** + True if the report should contain all tasks in Karm. + + Defaults to true. + */ + bool allTasks; + + /** + True if the durations should be output in decimal hours. Otherwise, + output durations as HH24:MI + */ + bool decimalMinutes; + + /** + The delimiter to use when outputting comma-seperated value reports. + */ + QString delimiter; + + /** + The quote to use for text fields when outputting comma-seperated reports. + */ + QString quote; +}; + +#endif diff --git a/karm/support/Makefile.am b/karm/support/Makefile.am new file mode 100644 index 000000000..3d74a3484 --- /dev/null +++ b/karm/support/Makefile.am @@ -0,0 +1,3 @@ +EXTRA_DIST = karm.desktop newtask.dlg + +xdg_apps_DATA = karm.desktop diff --git a/karm/support/karm.desktop b/karm/support/karm.desktop new file mode 100644 index 000000000..66b016af2 --- /dev/null +++ b/karm/support/karm.desktop @@ -0,0 +1,79 @@ +[Desktop Entry] +Name=KArm +Name[af]=Karm +Name[hi]=के-आर्म +Name[sv]=Karm +Name[ta]=Kஅம் +Name[xh]=KAlrm +Name[zh_TW]=KArm 個人時程紀錄 +GenericName=Personal Time Tracker +GenericName[af]=Persoonlike Tyd Volger +GenericName[az]=Şəxsi Saat İzləyici +GenericName[bg]=Отчитане на времето +GenericName[br]=Roudenner amzer personel +GenericName[bs]=Osobni mjerač vremena +GenericName[ca]=Cronòmetre personal +GenericName[cs]=Osobní měřič času +GenericName[cy]=Dilynnydd Amser Personol +GenericName[da]=Personlig tidsoversigt +GenericName[de]=Persönliche Zeiterfassung +GenericName[el]=Προσωπικός καταγραφέας χρόνου +GenericName[eo]=Tempomezurilo por viaj aktivecoj +GenericName[es]=Cronómetro personal +GenericName[et]=Personaalne ajaarvestus +GenericName[eu]=Kronometro pertsonala +GenericName[fa]=ردیاب شخصی زمان +GenericName[fi]=Henkilökohtainen ajanhallintaohjelma +GenericName[fr]=Chronomètre individuel de tâches +GenericName[fy]=Persoanlike tiidregistraasje +GenericName[gl]=Xestor Persoal de Proxectos +GenericName[he]=מנהל זמן אישי +GenericName[hr]=Osobni mjerač vremena +GenericName[hu]=Időfelhasználás-figyelő +GenericName[id]=Tracker Waktu Pribadi +GenericName[is]=Fylgjast með í hvað tíminn fer +GenericName[it]=Segnatempo personale +GenericName[ka]=პერსონალური დროის აღმრიცხველი +GenericName[kk]=Дербес уақыт қадағалағышы +GenericName[km]=កម្មវិធីតាមដានពេលវេលាផ្ទាល់ខ្លួន +GenericName[ko]=일하는 시간을 잴 수 있습니다 +GenericName[lt]=Asmeninis laiko sekėjas +GenericName[lv]=Personālais Laika Atsekotājs +GenericName[mk]=Следење на личното време +GenericName[ms]=Penjejak Waktu Peribadi +GenericName[mt]=Żomm il-ħin personali +GenericName[nb]=Personlig tidsoversikt +GenericName[nds]=Persöönlich Tietlogbook +GenericName[ne]=व्यक्तिगत समय ट्र्याकर +GenericName[nl]=Tijdsregistratie +GenericName[nn]=Personleg tidsmålar +GenericName[nso]=Seswaranako sa Nako ya Botho +GenericName[pl]=Osobisty Czasomierz +GenericName[pt]=Gestor Pessoal de Tempo +GenericName[pt_BR]=Gerenciador pessoal de tempo +GenericName[ro]=Organizator timp personal +GenericName[ru]=Учёт рабочего времени +GenericName[sk]=Osobný merač času +GenericName[sl]=Osebni merilec časa +GenericName[sr]=Лични пратилац времена +GenericName[sr@Latn]=Lični pratilac vremena +GenericName[sv]=Personlig tidmätare +GenericName[ta]=தனிப்பயன் நேரம் பின்பற்றி +GenericName[tg]=Баҳисобгирии вақти корӣ +GenericName[tr]=Kişisel Saat İzleyici +GenericName[uk]=Персональний лічильник часу +GenericName[ven]=Tshisedzulusi tsha tshifhinga tsha vhune +GenericName[vi]=Trình đo thời gian +GenericName[xh]=Umfumani Wexesha Lobuqu +GenericName[zh_CN]=个人时间记录 +GenericName[zh_TW]=個人時程記錄 +GenericName[zu]=Umgcini Wesikhathi Esemfihlo +Exec=karm -caption "%c" %i %m +Icon=karm +Path= +DocPath=karm/index.html +Type=Application +Terminal=false +X-KDE-StartupNotify=true +X-DCOP-ServiceType=Multi +Categories=Qt;KDE;X-KDE-Utilities-PIM;Office;Calendar; diff --git a/karm/task.cpp b/karm/task.cpp new file mode 100644 index 000000000..07e934e53 --- /dev/null +++ b/karm/task.cpp @@ -0,0 +1,465 @@ +#include <qcstring.h> +#include <qdatetime.h> +#include <qstring.h> +#include <qtimer.h> + +#include <kiconloader.h> + +#include "kapplication.h" // kapp +#include "kdebug.h" + +#include "event.h" + +#include "karmutility.h" +#include "task.h" +#include "taskview.h" +#include "preferences.h" + + +const int gSecondsPerMinute = 60; + + +QPtrVector<QPixmap> *Task::icons = 0; + +Task::Task( const QString& taskName, long minutes, long sessionTime, + DesktopList desktops, TaskView *parent) + : QObject(), QListViewItem(parent) +{ + init(taskName, minutes, sessionTime, desktops, 0); +} + +Task::Task( const QString& taskName, long minutes, long sessionTime, + DesktopList desktops, Task *parent) + : QObject(), QListViewItem(parent) +{ + init(taskName, minutes, sessionTime, desktops, 0); +} + +Task::Task( KCal::Todo* todo, TaskView* parent ) + : QObject(), QListViewItem( parent ) +{ + long minutes = 0; + QString name; + long sessionTime = 0; + int percent_complete = 0; + DesktopList desktops; + + parseIncidence(todo, minutes, sessionTime, name, desktops, percent_complete); + init(name, minutes, sessionTime, desktops, percent_complete); +} + +void Task::init( const QString& taskName, long minutes, long sessionTime, + DesktopList desktops, int percent_complete) +{ + // If our parent is the taskview then connect our totalTimesChanged + // signal to its receiver + if ( ! parent() ) + connect( this, SIGNAL( totalTimesChanged ( long, long ) ), + listView(), SLOT( taskTotalTimesChanged( long, long) )); + + connect( this, SIGNAL( deletingTask( Task* ) ), + listView(), SLOT( deletingTask( Task* ) )); + + if (icons == 0) { + icons = new QPtrVector<QPixmap>(8); + KIconLoader kil("karm"); // always load icons from the KArm application + for (int i=0; i<8; i++) + { + QPixmap *icon = new QPixmap(); + QString name; + name.sprintf("watch-%d.xpm",i); + *icon = kil.loadIcon( name, KIcon::User ); + icons->insert(i,icon); + } + } + + _removing = false; + _name = taskName.stripWhiteSpace(); + _lastStart = QDateTime::currentDateTime(); + _totalTime = _time = minutes; + _totalSessionTime = _sessionTime = sessionTime; + _timer = new QTimer(this); + _desktops = desktops; + connect(_timer, SIGNAL(timeout()), this, SLOT(updateActiveIcon())); + setPixmap(1, UserIcon(QString::fromLatin1("empty-watch.xpm"))); + _currentPic = 0; + _percentcomplete = percent_complete; + + update(); + changeParentTotalTimes( _sessionTime, _time); +} + +Task::~Task() { + emit deletingTask(this); + delete _timer; +} + +void Task::setRunning( bool on, KarmStorage* storage, QDateTime whenStarted, QDateTime whenStopped ) +// Sets a task running or stopped. If the task is to be stopped, whenStarted is not evaluated. +// on=true if the task shall be started on=false if the task shall be stopped +// This is the back-end, the front-end is StartTimerFor() +{ + kdDebug(5970) << "Entering Task::setRunning " << "on=" << on << "whenStarted=" << whenStarted << " whenStopped=" << whenStopped << endl; + if ( on ) + { + if (!_timer->isActive()) + { + _timer->start(1000); + storage->startTimer(this); + _currentPic=7; + _lastStart = whenStarted; + updateActiveIcon(); + } + } + else + { + if (_timer->isActive()) + { + _timer->stop(); + if ( ! _removing ) + { + storage->stopTimer(this, whenStopped); + setPixmap(1, UserIcon(QString::fromLatin1("empty-watch.xpm"))); + } + } + } +} + +void Task::setUid(QString uid) { + _uid = uid; +} + +bool Task::isRunning() const +{ + return _timer->isActive(); +} + +void Task::setName( const QString& name, KarmStorage* storage ) +{ + kdDebug(5970) << "Task:setName: " << name << endl; + + QString oldname = _name; + if ( oldname != name ) { + _name = name; + storage->setName(this, oldname); + update(); + } +} + +void Task::setPercentComplete(const int percent, KarmStorage *storage) +{ + kdDebug(5970) << "Task::setPercentComplete(" << percent << ", storage): " + << _uid << endl; + + if (!percent) + _percentcomplete = 0; + else if (percent > 100) + _percentcomplete = 100; + else if (percent < 0) + _percentcomplete = 0; + else + _percentcomplete = percent; + + if (isRunning() && _percentcomplete==100) taskView()->stopTimerFor(this); + + setPixmapProgress(); + + // When parent marked as complete, mark all children as complete as well. + // Complete tasks are not displayed in the task view, so if a parent is + // marked as complete and some of the children are not, then we get an error + // message. KArm actually keep chugging along in this case and displays the + // child tasks just fine, so an alternative solution is to remove that error + // message (from KarmStorage::load). But I think it makes more sense that + // if you mark a parent task as complete, then all children should be + // complete as well. + // + // This behavior is consistent with KOrganizer (as of 2003-09-24). + if (_percentcomplete == 100) + { + for (Task* child= this->firstChild(); child; child = child->nextSibling()) + child->setPercentComplete(_percentcomplete, storage); + } +} + +void Task::setPixmapProgress() +{ + QPixmap* icon = new QPixmap(); + if (_percentcomplete >= 100) + *icon = UserIcon("task-complete.xpm"); + else + *icon = UserIcon("task-incomplete.xpm"); + setPixmap(0, *icon); +} + +bool Task::isComplete() { return _percentcomplete == 100; } + +void Task::removeFromView() +{ + while ( Task* child = firstChild() ) + child->removeFromView(); + delete this; +} + +void Task::setDesktopList ( DesktopList desktopList ) +{ + _desktops = desktopList; +} + +void Task::changeTime( long minutes, KarmStorage* storage ) +{ + changeTimes( minutes, minutes, storage); +} + +void Task::changeTimes( long minutesSession, long minutes, KarmStorage* storage) +{ + if( minutesSession != 0 || minutes != 0) + { + _sessionTime += minutesSession; + _time += minutes; + if ( storage ) storage->changeTime(this, minutes * gSecondsPerMinute); + changeTotalTimes( minutesSession, minutes ); + } +} + +void Task::changeTotalTimes( long minutesSession, long minutes ) +{ + kdDebug(5970) + << "Task::changeTotalTimes(" << minutesSession << ", " + << minutes << ") for " << name() << endl; + + _totalSessionTime += minutesSession; + _totalTime += minutes; + update(); + changeParentTotalTimes( minutesSession, minutes ); +} + +void Task::resetTimes() +{ + _totalSessionTime -= _sessionTime; + _totalTime -= _time; + changeParentTotalTimes( -_sessionTime, -_time); + _sessionTime = 0; + _time = 0; + update(); +} + +void Task::changeParentTotalTimes( long minutesSession, long minutes ) +{ + //kdDebug(5970) + // << "Task::changeParentTotalTimes(" << minutesSession << ", " + // << minutes << ") for " << name() << endl; + + if ( isRoot() ) + emit totalTimesChanged( minutesSession, minutes ); + else + parent()->changeTotalTimes( minutesSession, minutes ); +} + +bool Task::remove( QPtrList<Task>& activeTasks, KarmStorage* storage) +{ + kdDebug(5970) << "Task::remove: " << _name << endl; + + bool ok = true; + + _removing = true; + storage->removeTask(this); + if( isRunning() ) setRunning( false, storage ); + + for (Task* child = this->firstChild(); child; child = child->nextSibling()) + { + if (child->isRunning()) + child->setRunning(false, storage); + child->remove(activeTasks, storage); + } + + changeParentTotalTimes( -_sessionTime, -_time); + + _removing = false; + + return ok; +} + +void Task::updateActiveIcon() +{ + _currentPic = (_currentPic+1) % 8; + setPixmap(1, *(*icons)[_currentPic]); +} + +QString Task::fullName() const +{ + if (isRoot()) + return name(); + else + return parent()->fullName() + QString::fromLatin1("/") + name(); +} + +KCal::Todo* Task::asTodo(KCal::Todo* todo) const +{ + + Q_ASSERT( todo != NULL ); + + kdDebug(5970) << "Task::asTodo: name() = '" << name() << "'" << endl; + todo->setSummary( name() ); + + // Note: if the date start is empty, the KOrganizer GUI will have the + // checkbox blank, but will prefill the todo's starting datetime to the + // time the file is opened. + // todo->setDtStart( current ); + + todo->setCustomProperty( kapp->instanceName(), + QCString( "totalTaskTime" ), QString::number( _time ) ); + todo->setCustomProperty( kapp->instanceName(), + QCString( "totalSessionTime" ), QString::number( _sessionTime) ); + + if (getDesktopStr().isEmpty()) + todo->removeCustomProperty(kapp->instanceName(), QCString("desktopList")); + else + todo->setCustomProperty( kapp->instanceName(), + QCString( "desktopList" ), getDesktopStr() ); + + todo->setOrganizer( Preferences::instance()->userRealName() ); + + todo->setPercentComplete(_percentcomplete); + + return todo; +} + +bool Task::parseIncidence( KCal::Incidence* incident, long& minutes, + long& sessionMinutes, QString& name, DesktopList& desktops, + int& percent_complete ) +{ + bool ok; + + name = incident->summary(); + _uid = incident->uid(); + + _comment = incident->description(); + + ok = false; + minutes = incident->customProperty( kapp->instanceName(), + QCString( "totalTaskTime" )).toInt( &ok ); + if ( !ok ) + minutes = 0; + + ok = false; + sessionMinutes = incident->customProperty( kapp->instanceName(), + QCString( "totalSessionTime" )).toInt( &ok ); + if ( !ok ) + sessionMinutes = 0; + + QString desktopList = incident->customProperty( kapp->instanceName(), + QCString( "desktopList" ) ); + QStringList desktopStrList = QStringList::split( QString::fromLatin1(","), + desktopList ); + desktops.clear(); + + for ( QStringList::iterator iter = desktopStrList.begin(); + iter != desktopStrList.end(); + ++iter ) { + int desktopInt = (*iter).toInt( &ok ); + if ( ok ) { + desktops.push_back( desktopInt ); + } + } + + percent_complete = static_cast<KCal::Todo*>(incident)->percentComplete(); + + //kdDebug(5970) << "Task::parseIncidence: " + // << name << ", Minutes: " << minutes + // << ", desktop: " << desktopList << endl; + + return true; +} + +QString Task::getDesktopStr() const +{ + if ( _desktops.empty() ) + return QString(); + + QString desktopstr; + for ( DesktopList::const_iterator iter = _desktops.begin(); + iter != _desktops.end(); + ++iter ) { + desktopstr += QString::number( *iter ) + QString::fromLatin1( "," ); + } + desktopstr.remove( desktopstr.length() - 1, 1 ); + return desktopstr; +} + +void Task::cut() +{ + //kdDebug(5970) << "Task::cut - " << name() << endl; + changeParentTotalTimes( -_totalSessionTime, -_totalTime); + if ( ! parent()) + listView()->takeItem(this); + else + parent()->takeItem(this); +} + +void Task::move(Task* destination) +{ + cut(); + paste(destination); +} + +void Task::paste(Task* destination) +{ + destination->insertItem(this); + changeParentTotalTimes( _totalSessionTime, _totalTime); +} + +void Task::update() +{ + setText(0, _name); + setText(1, formatTime(_sessionTime)); + setText(2, formatTime(_time)); + setText(3, formatTime(_totalSessionTime)); + setText(4, formatTime(_totalTime)); +} + +void Task::addComment( QString comment, KarmStorage* storage ) +{ + _comment = _comment + QString::fromLatin1("\n") + comment; + storage->addComment(this, comment); +} + +QString Task::comment() const +{ + return _comment; +} + +int Task::compare ( QListViewItem * i, int col, bool ascending ) const +{ + long thistime = 0; + long thattime = 0; + Task *task = static_cast<Task*>(i); + + switch ( col ) + { + case 1: + thistime = _sessionTime; + thattime = task->sessionTime(); + break; + case 2: + thistime = _time; + thattime = task->time(); + break; + case 3: + thistime = _totalSessionTime; + thattime = task->totalSessionTime(); + break; + case 4: + thistime = _totalTime; + thattime = task->totalTime(); + break; + default: + return key(col, ascending).localeAwareCompare( i->key(col, ascending) ); + } + + if ( thistime < thattime ) return -1; + if ( thistime > thattime ) return 1; + return 0; + +} + +#include "task.moc" diff --git a/karm/task.h b/karm/task.h new file mode 100644 index 000000000..8a9ca7833 --- /dev/null +++ b/karm/task.h @@ -0,0 +1,298 @@ +#ifndef KARM_TASK_H +#define KARM_TASK_H + +// Required b/c QPtrList is a struct, not a class. +#include <qptrlist.h> + +// Requred b/c/ QPtrVector is a template (?) +#include <qptrvector.h> + +#include <qdatetime.h> + +// Required b/c DesktopList is a typedef not a class. +#include "desktoplist.h" + +// Required b/c of static cast below? (How else can compiler know that a +// TaskView is a subclass or QListView?) +#include "taskview.h" + +class QFile; +class QString; +class KarmStorage; + +class QTimer; +class KCal::Incidence; +class KCal::Todo; +class QObject; +class QPixmap; + +/// \class Task +/** \brief A class representing a task + * + * A "Task" object stores information about a task such as it's name, + * total and session times. + * + * It can log when the task is started, stoped or deleted. + * + * If a task is associated with some desktop's activity it can remember that + * too. + * + * It can also contain subtasks - these are managed using the + * QListViewItem class. + */ +class Task : public QObject, public QListViewItem +{ + Q_OBJECT + + public: + //@{ constructors + Task( const QString& taskame, long minutes, long sessionTime, + DesktopList desktops, TaskView* parent = 0); + Task( const QString& taskame, long minutes, long sessionTime, + DesktopList desktops, Task* parent = 0); + Task( KCal::Todo* incident, TaskView* parent ); + //@} + /* destructor */ + ~Task(); + + /** return parent Task or null in case of TaskView. + * same as QListViewItem::parent() + */ + Task* firstChild() const { return (Task*)QListViewItem::firstChild(); } + Task* nextSibling() const { return (Task*)QListViewItem::nextSibling(); } + Task* parent() const { return (Task*)QListViewItem::parent(); } + + /** Return task view for this task */ + TaskView* taskView() const { + return static_cast<TaskView *>( listView() ); + } + + /** Return unique iCalendar Todo ID for this task. */ + QString uid() const { return _uid; } + + /** + * Set unique id for the task. + * + * The uid is the key used to update the storage. + * + * @param uid The new unique id. + */ + void setUid(const QString uid); + + /** cut Task out of parent Task or the TaskView */ + void cut(); + /** cut Task out of parent Task or the TaskView and into the + * destination Task */ + void move(Task* destination); + /** insert Task into the destination Task */ + void paste(Task* destination); + + /** Sort times numerically, not alphabetically. */ + int compare ( QListViewItem * i, int col, bool ascending ) const; + + //@{ timing related functions + + /** + * Change task time. Adds minutes to both total time and session time. + * + * @param minutes minutes to add to - may be negative + * @param storage Pointer to KarmStorage instance. + * If zero, don't save changes. + */ + void changeTime( long minutes, KarmStorage* storage ); + + /** + * Add minutes to time and session time, and write to storage. + * + * @param minutesSession minutes to add to task session time + * @param minutes minutes to add to task time + * @param storage Pointer to KarmStorage instance. + * If zero, don't save changes. + */ + void changeTimes + ( long minutesSession, long minutes, KarmStorage* storage=0); + + /** adds minutes to total and session time + * + * @param minutesSession minutes to add to task total session time + * @param minutes minutes to add to task total time + */ + void changeTotalTimes( long minutesSession, long minutes ); + + /** + * Reset all times to 0 + */ + void resetTimes(); + + /*@{ returns the times accumulated by the task + * @return total time in minutes + */ + long time() const { return _time; }; + long totalTime() const { return _totalTime; }; + long sessionTime() const { return _sessionTime; }; + long totalSessionTime() const { return _totalSessionTime; }; + + /** + * Return time the task was started. + */ + QDateTime startTime() const { return _lastStart; }; + + /** sets session time to zero. */ + void startNewSession() { changeTimes( -_sessionTime, 0 ); }; + //@} + + //@{ desktop related functions + + void setDesktopList ( DesktopList dl ); + DesktopList getDesktops() const { return _desktops;} + + QString getDesktopStr() const; + //@} + + //@{ name related functions + + /** sets the name of the task + * @param name a pointer to the name. A deep copy will be made. + * @param storage a pointer to a KarmStorage object. + */ + void setName( const QString& name, KarmStorage* storage ); + + /** returns the name of this task. + * @return a pointer to the name. + */ + QString name() const { return _name; }; + + /** + * Returns that task name, prefixed by parent tree up to root. + * + * Task names are seperated by a forward slash: / + */ + QString fullName() const; + //@} + + /** Update the display of the task (all columns) in the UI. */ + void update(); + + //@{ the state of a Task - stopped, running + + /** starts or stops a task + * @param on true or false for starting or stopping a task + * @param storage a pointer to a KarmStorage object. + * @param whenStarted time when the task was started. Normally + QDateTime::currentDateTime, but if calendar has + been changed by another program and being reloaded + the task is set to running with another start date + */ + void setRunning( bool on, KarmStorage* storage, QDateTime whenStarted=QDateTime::currentDateTime(), QDateTime whenStopped=QDateTime::currentDateTime()); + + /** return the state of a task - if it's running or not + * @return true or false depending on whether the task is running + */ + bool isRunning() const; + //@} + + bool parseIncidence(KCal::Incidence*, long& minutes, + long& sessionMinutes, QString& name, DesktopList& desktops, + int& percent_complete); + + /** + * Load the todo passed in with this tasks info. + */ + KCal::Todo* asTodo(KCal::Todo* calendar) const; + + /** Add a comment to this task. */ + void addComment( QString comment, KarmStorage* storage ); + + /** Retrieve the entire comment for the task. */ + QString comment() const; + + /** tells you whether this task is the root of the task tree */ + bool isRoot() const { return parent() == 0; } + + /** remove Task with all it's children + * @param activeTasks - list of aktive tasks + * @param storage a pointer to a KarmStorage object. + */ + bool remove( QPtrList<Task>& activeTasks, KarmStorage* storage ); + + /** + * Update percent complete for this task. + * + * Tasks that are complete (i.e., percent = 100) do not show up in + * taskview. If percent NULL, set to zero. If greater than 100, set to + * 100. If less than zero, set to zero. + */ + void setPercentComplete(const int percent, KarmStorage *storage); + + + /** Sets an appropriate icon for this task based on its level of + * completion */ + void setPixmapProgress(); + + /** Return true if task is complete (percent complete equals 100). */ + bool isComplete(); + + /** Remove current task and all it's children from the view. */ + void removeFromView(); + + /** delivers when the task was started last */ + QDateTime lastStart() { return _lastStart; } + + protected: + void changeParentTotalTimes( long minutesSession, long minutes ); + + signals: + void totalTimesChanged( long minutesSession, long minutes); + /** signal that we're about to delete a task */ + void deletingTask(Task* thisTask); + + protected slots: + /** animate the active icon */ + void updateActiveIcon(); + + private: + + /** The iCal unique ID of the Todo for this task. */ + QString _uid; + + /** The comment associated with this Task. */ + QString _comment; + + int _percentcomplete; + + long totalTimeInSeconds() const { return _totalTime * 60; } + + /** if the time or session time is negative set them to zero */ + void noNegativeTimes(); + + /** initialize a task */ + void init( const QString& taskame, long minutes, long sessionTime, + DesktopList desktops, int percent_complete); + + + /** task name */ + QString _name; + + /** Last time this task was started. */ + QDateTime _lastStart; + + //@{ totals of the whole subtree including self + long _totalTime; + long _totalSessionTime; + //@} + + //@{ times spend on the task itself + long _time; + long _sessionTime; + //@} + DesktopList _desktops; + QTimer *_timer; + int _currentPic; + static QPtrVector<QPixmap> *icons; + + /** Don't need to update storage when deleting task from list. */ + bool _removing; + +}; + +#endif // KARM_TASK_H diff --git a/karm/taskview.cpp b/karm/taskview.cpp new file mode 100644 index 000000000..590744408 --- /dev/null +++ b/karm/taskview.cpp @@ -0,0 +1,871 @@ +#include <qclipboard.h> +#include <qfile.h> +#include <qlayout.h> +#include <qlistbox.h> +#include <qlistview.h> +#include <qptrlist.h> +#include <qptrstack.h> +#include <qstring.h> +#include <qtextstream.h> +#include <qtimer.h> +#include <qxml.h> + +#include "kapplication.h" // kapp +#include <kconfig.h> +#include <kdebug.h> +#include <kfiledialog.h> +#include <klocale.h> // i18n +#include <kmessagebox.h> +#include <kurlrequester.h> + +#include "csvexportdialog.h" +#include "desktoptracker.h" +#include "edittaskdialog.h" +#include "idletimedetector.h" +#include "karmstorage.h" +#include "plannerparser.h" +#include "preferences.h" +#include "printdialog.h" +#include "reportcriteria.h" +#include "task.h" +#include "taskview.h" +#include "timekard.h" +#include "taskviewwhatsthis.h" + +#define T_LINESIZE 1023 +#define HIDDEN_COLUMN -10 + +class DesktopTracker; + +TaskView::TaskView(QWidget *parent, const char *name, const QString &icsfile ):KListView(parent,name) +{ + _preferences = Preferences::instance( icsfile ); + _storage = KarmStorage::instance(); + + connect( this, SIGNAL( expanded( QListViewItem * ) ), + this, SLOT( itemStateChanged( QListViewItem * ) ) ); + connect( this, SIGNAL( collapsed( QListViewItem * ) ), + this, SLOT( itemStateChanged( QListViewItem * ) ) ); + + // setup default values + previousColumnWidths[0] = previousColumnWidths[1] + = previousColumnWidths[2] = previousColumnWidths[3] = HIDDEN_COLUMN; + + addColumn( i18n("Task Name") ); + addColumn( i18n("Session Time") ); + addColumn( i18n("Time") ); + addColumn( i18n("Total Session Time") ); + addColumn( i18n("Total Time") ); + setColumnAlignment( 1, Qt::AlignRight ); + setColumnAlignment( 2, Qt::AlignRight ); + setColumnAlignment( 3, Qt::AlignRight ); + setColumnAlignment( 4, Qt::AlignRight ); + adaptColumns(); + setAllColumnsShowFocus( true ); + + // set up the minuteTimer + _minuteTimer = new QTimer(this); + connect( _minuteTimer, SIGNAL( timeout() ), this, SLOT( minuteUpdate() )); + _minuteTimer->start(1000 * secsPerMinute); + + // React when user changes iCalFile + connect(_preferences, SIGNAL(iCalFile(QString)), + this, SLOT(iCalFileChanged(QString))); + + // resize columns when config is changed + connect(_preferences, SIGNAL( setupChanged() ), this,SLOT( adaptColumns() )); + + _minuteTimer->start(1000 * secsPerMinute); + + // Set up the idle detection. + _idleTimeDetector = new IdleTimeDetector( _preferences->idlenessTimeout() ); + connect( _idleTimeDetector, SIGNAL( extractTime(int) ), + this, SLOT( extractTime(int) )); + connect( _idleTimeDetector, SIGNAL( stopAllTimersAt(QDateTime) ), + this, SLOT( stopAllTimersAt(QDateTime) )); + connect( _preferences, SIGNAL( idlenessTimeout(int) ), + _idleTimeDetector, SLOT( setMaxIdle(int) )); + connect( _preferences, SIGNAL( detectIdleness(bool) ), + _idleTimeDetector, SLOT( toggleOverAllIdleDetection(bool) )); + if (!_idleTimeDetector->isIdleDetectionPossible()) + _preferences->disableIdleDetection(); + + // Setup auto save timer + _autoSaveTimer = new QTimer(this); + connect( _preferences, SIGNAL( autoSave(bool) ), + this, SLOT( autoSaveChanged(bool) )); + connect( _preferences, SIGNAL( autoSavePeriod(int) ), + this, SLOT( autoSavePeriodChanged(int) )); + connect( _autoSaveTimer, SIGNAL( timeout() ), this, SLOT( save() )); + + // Setup manual save timer (to save changes a little while after they happen) + _manualSaveTimer = new QTimer(this); + connect( _manualSaveTimer, SIGNAL( timeout() ), this, SLOT( save() )); + + // Connect desktop tracker events to task starting/stopping + _desktopTracker = new DesktopTracker(); + connect( _desktopTracker, SIGNAL( reachedtActiveDesktop( Task* ) ), + this, SLOT( startTimerFor(Task*) )); + connect( _desktopTracker, SIGNAL( leftActiveDesktop( Task* ) ), + this, SLOT( stopTimerFor(Task*) )); + new TaskViewWhatsThis( this ); +} + +KarmStorage* TaskView::storage() +{ + return _storage; +} + +void TaskView::contentsMousePressEvent ( QMouseEvent * e ) +{ + kdDebug(5970) << "entering contentsMousePressEvent" << endl; + KListView::contentsMousePressEvent(e); + Task* task = current_item(); + // This checks that there has been a click onto an item, + // not into an empty part of the KListView. + if ( task != 0 && // zero can happen if there is no task + e->pos().y() >= current_item()->itemPos() && + e->pos().y() < current_item()->itemPos()+current_item()->height() ) + { + // see if the click was on the completed icon + int leftborder = treeStepSize() * ( task->depth() + ( rootIsDecorated() ? 1 : 0)) + itemMargin(); + if ((leftborder < e->x()) && (e->x() < 19 + leftborder )) + { + if ( e->button() == LeftButton ) + if ( task->isComplete() ) task->setPercentComplete( 0, _storage ); + else task->setPercentComplete( 100, _storage ); + } + emit updateButtons(); + } +} + +void TaskView::contentsMouseDoubleClickEvent ( QMouseEvent * e ) +// if the user double-clicks onto a tasks, he says "I am now working exclusively +// on that task". That means, on a doubleclick, we check if it occurs on an item +// not in the blank space, if yes, stop all other tasks and start the new timer. +{ + kdDebug(5970) << "entering contentsMouseDoubleClickEvent" << endl; + KListView::contentsMouseDoubleClickEvent(e); + + Task *task = current_item(); + + if ( task != 0 ) // current_item() exists + { + if ( e->pos().y() >= task->itemPos() && // doubleclick was onto current_item() + e->pos().y() < task->itemPos()+task->height() ) + { + if ( activeTasks.findRef(task) == -1 ) // task is active + { + stopAllTimers(); + startCurrentTimer(); + } + else stopCurrentTimer(); + } + } +} + +TaskView::~TaskView() +{ + _preferences->save(); +} + +Task* TaskView::first_child() const +{ + return static_cast<Task*>(firstChild()); +} + +Task* TaskView::current_item() const +{ + return static_cast<Task*>(currentItem()); +} + +Task* TaskView::item_at_index(int i) +{ + return static_cast<Task*>(itemAtIndex(i)); +} + +void TaskView::load( QString fileName ) +{ + // if the program is used as an embedded plugin for konqueror, there may be a need + // to load from a file without touching the preferences. + _isloading = true; + QString err = _storage->load(this, _preferences, fileName); + + if (!err.isEmpty()) + { + KMessageBox::error(this, err); + _isloading = false; + return; + } + + // Register tasks with desktop tracker + int i = 0; + for ( Task* t = item_at_index(i); t; t = item_at_index(++i) ) + _desktopTracker->registerForDesktops( t, t->getDesktops() ); + + restoreItemState( first_child() ); + + setSelected(first_child(), true); + setCurrentItem(first_child()); + if ( _desktopTracker->startTracking() != QString() ) + KMessageBox::error( 0, i18n("You are on a too high logical desktop, desktop tracking will not work") ); + _isloading = false; + refresh(); +} + +void TaskView::restoreItemState( QListViewItem *item ) +{ + while( item ) + { + Task *t = (Task *)item; + t->setOpen( _preferences->readBoolEntry( t->uid() ) ); + if( item->childCount() > 0 ) restoreItemState( item->firstChild() ); + item = item->nextSibling(); + } +} + +void TaskView::itemStateChanged( QListViewItem *item ) +{ + if ( !item || _isloading ) return; + Task *t = (Task *)item; + kdDebug(5970) << "TaskView::itemStateChanged()" + << " uid=" << t->uid() << " state=" << t->isOpen() + << endl; + if( _preferences ) _preferences->writeEntry( t->uid(), t->isOpen() ); +} + +void TaskView::closeStorage() { _storage->closeStorage( this ); } + +void TaskView::iCalFileModified(ResourceCalendar *rc) +{ + kdDebug(5970) << "entering iCalFileModified" << endl; + kdDebug(5970) << rc->infoText() << endl; + rc->dump(); + _storage->buildTaskView(rc,this); + kdDebug(5970) << "exiting iCalFileModified" << endl; +} + +void TaskView::refresh() +{ + kdDebug(5970) << "entering TaskView::refresh()" << endl; + this->setRootIsDecorated(true); + int i = 0; + for ( Task* t = item_at_index(i); t; t = item_at_index(++i) ) + { + t->setPixmapProgress(); + } + + // remove root decoration if there is no more children. + bool anyChilds = false; + for(Task* child = first_child(); + child; + child = child->nextSibling()) { + if (child->childCount() != 0) { + anyChilds = true; + break; + } + } + if (!anyChilds) { + setRootIsDecorated(false); + } + emit updateButtons(); + kdDebug(5970) << "exiting TaskView::refresh()" << endl; +} + +void TaskView::loadFromFlatFile() +{ + kdDebug(5970) << "TaskView::loadFromFlatFile()" << endl; + + //KFileDialog::getSaveFileName("icalout.ics",i18n("*.ics|ICalendars"),this); + + QString fileName(KFileDialog::getOpenFileName(QString::null, QString::null, + 0)); + if (!fileName.isEmpty()) { + QString err = _storage->loadFromFlatFile(this, fileName); + if (!err.isEmpty()) + { + KMessageBox::error(this, err); + return; + } + // Register tasks with desktop tracker + int task_idx = 0; + Task* task = item_at_index(task_idx++); + while (task) + { + // item_at_index returns 0 where no more items. + _desktopTracker->registerForDesktops( task, task->getDesktops() ); + task = item_at_index(task_idx++); + } + + setSelected(first_child(), true); + setCurrentItem(first_child()); + + if ( _desktopTracker->startTracking() != QString() ) + KMessageBox::error(0, i18n("You are on a too high logical desktop, desktop tracking will not work") ); + } +} + +QString TaskView::importPlanner(QString fileName) +{ + kdDebug(5970) << "entering importPlanner" << endl; + PlannerParser* handler=new PlannerParser(this); + if (fileName.isEmpty()) fileName=KFileDialog::getOpenFileName(QString::null, QString::null, 0); + QFile xmlFile( fileName ); + QXmlInputSource source( xmlFile ); + QXmlSimpleReader reader; + reader.setContentHandler( handler ); + reader.parse( source ); + refresh(); + return ""; +} + +QString TaskView::report( const ReportCriteria& rc ) +{ + return _storage->report( this, rc ); +} + +void TaskView::exportcsvFile() +{ + kdDebug(5970) << "TaskView::exportcsvFile()" << endl; + + CSVExportDialog dialog( ReportCriteria::CSVTotalsExport, this ); + if ( current_item() && current_item()->isRoot() ) + dialog.enableTasksToExportQuestion(); + dialog.urlExportTo->KURLRequester::setMode(KFile::File); + if ( dialog.exec() ) { + QString err = _storage->report( this, dialog.reportCriteria() ); + if ( !err.isEmpty() ) KMessageBox::error( this, i18n(err.ascii()) ); + } +} + +QString TaskView::exportcsvHistory() +{ + kdDebug(5970) << "TaskView::exportcsvHistory()" << endl; + QString err; + + CSVExportDialog dialog( ReportCriteria::CSVHistoryExport, this ); + if ( current_item() && current_item()->isRoot() ) + dialog.enableTasksToExportQuestion(); + dialog.urlExportTo->KURLRequester::setMode(KFile::File); + if ( dialog.exec() ) { + err = _storage->report( this, dialog.reportCriteria() ); + } + return err; +} + +void TaskView::scheduleSave() +{ + kdDebug(5970) << "Entering TaskView::scheduleSave" << endl; + // save changes a little while after they happen + _manualSaveTimer->start( 10, true /*single-shot*/ ); +} + +Preferences* TaskView::preferences() { return _preferences; } + +QString TaskView::save() +// This saves the tasks. If they do not yet have an endDate, their startDate is also not saved. +{ + kdDebug(5970) << "Entering TaskView::save" << endl; + QString err = _storage->save(this); + emit(setStatusBar(err)); + return err; +} + +void TaskView::startCurrentTimer() +{ + startTimerFor( current_item() ); +} + +long TaskView::count() +{ + long n = 0; + for (Task* t = item_at_index(n); t; t=item_at_index(++n)); + return n; +} + +void TaskView::startTimerFor(Task* task, QDateTime startTime ) +{ + kdDebug(5970) << "Entering TaskView::startTimerFor" << endl; + if (save()==QString()) + { + if (task != 0 && activeTasks.findRef(task) == -1) + { + _idleTimeDetector->startIdleDetection(); + if (!task->isComplete()) + { + task->setRunning(true, _storage, startTime); + activeTasks.append(task); + emit updateButtons(); + if ( activeTasks.count() == 1 ) + emit timersActive(); + emit tasksChanged( activeTasks); + } + } + } + else KMessageBox::error(0,i18n("Saving is impossible, so timing is useless. \nSaving problems may result from a full harddisk, a directory name instead of a file name, or stale locks. Check that your harddisk has enough space, that your calendar file exists and is a file and remove stale locks, typically from ~/.kde/share/apps/kabc/lock.")); +} + +void TaskView::clearActiveTasks() +{ + activeTasks.clear(); +} + +void TaskView::stopAllTimers() +{ + kdDebug(5970) << "Entering TaskView::stopAllTimers()" << endl; + for ( unsigned int i = 0; i < activeTasks.count(); i++ ) + activeTasks.at(i)->setRunning(false, _storage); + + _idleTimeDetector->stopIdleDetection(); + activeTasks.clear(); + emit updateButtons(); + emit timersInactive(); + emit tasksChanged( activeTasks ); +} + +void TaskView::stopAllTimersAt(QDateTime qdt) +// stops all timers for the time qdt. This makes sense, if the idletimedetector detected +// the last work has been done 50 minutes ago. +{ + kdDebug(5970) << "Entering TaskView::stopAllTimersAt " << qdt << endl; + for ( unsigned int i = 0; i < activeTasks.count(); i++ ) + { + activeTasks.at(i)->setRunning(false, _storage, qdt, qdt); + kdDebug() << activeTasks.at(i)->name() << endl; + } + + _idleTimeDetector->stopIdleDetection(); + activeTasks.clear(); + emit updateButtons(); + emit timersInactive(); + emit tasksChanged( activeTasks ); +} + +void TaskView::startNewSession() +{ + QListViewItemIterator item( first_child()); + for ( ; item.current(); ++item ) { + Task * task = (Task *) item.current(); + task->startNewSession(); + } +} + +void TaskView::resetTimeForAllTasks() +{ + QListViewItemIterator item( first_child()); + for ( ; item.current(); ++item ) { + Task * task = (Task *) item.current(); + task->resetTimes(); + } +} + +void TaskView::stopTimerFor(Task* task) +{ + kdDebug(5970) << "Entering stopTimerFor. task = " << task->name() << endl; + if ( task != 0 && activeTasks.findRef(task) != -1 ) { + activeTasks.removeRef(task); + task->setRunning(false, _storage); + if ( activeTasks.count() == 0 ) { + _idleTimeDetector->stopIdleDetection(); + emit timersInactive(); + } + emit updateButtons(); + } + emit tasksChanged( activeTasks); +} + +void TaskView::stopCurrentTimer() +{ + stopTimerFor( current_item()); +} + +void TaskView::minuteUpdate() +{ + addTimeToActiveTasks(1, false); +} + +void TaskView::addTimeToActiveTasks(int minutes, bool save_data) +{ + for( unsigned int i = 0; i < activeTasks.count(); i++ ) + activeTasks.at(i)->changeTime(minutes, ( save_data ? _storage : 0 ) ); +} + +void TaskView::newTask() +{ + newTask(i18n("New Task"), 0); +} + +void TaskView::newTask(QString caption, Task *parent) +{ + EditTaskDialog *dialog = new EditTaskDialog(caption, false); + long total, totalDiff, session, sessionDiff; + DesktopList desktopList; + + int result = dialog->exec(); + if ( result == QDialog::Accepted ) { + QString taskName = i18n( "Unnamed Task" ); + if ( !dialog->taskName().isEmpty()) taskName = dialog->taskName(); + + total = totalDiff = session = sessionDiff = 0; + dialog->status( &total, &totalDiff, &session, &sessionDiff, &desktopList ); + + // If all available desktops are checked, disable auto tracking, + // since it makes no sense to track for every desktop. + if ( desktopList.size() == ( unsigned int ) _desktopTracker->desktopCount() ) + desktopList.clear(); + + QString uid = addTask( taskName, total, session, desktopList, parent ); + if ( uid.isNull() ) + { + KMessageBox::error( 0, i18n( + "Error storing new task. Your changes were not saved. Make sure you can edit your iCalendar file. Also quit all applications using this file and remove any lock file related to its name from ~/.kde/share/apps/kabc/lock/ " ) ); + } + + delete dialog; + } +} + +QString TaskView::addTask +( const QString& taskname, long total, long session, + const DesktopList& desktops, Task* parent ) +{ + Task *task; + kdDebug(5970) << "TaskView::addTask: taskname = " << taskname << endl; + + if ( parent ) task = new Task( taskname, total, session, desktops, parent ); + else task = new Task( taskname, total, session, desktops, this ); + + task->setUid( _storage->addTask( task, parent ) ); + QString taskuid=task->uid(); + if ( ! taskuid.isNull() ) + { + _desktopTracker->registerForDesktops( task, desktops ); + setCurrentItem( task ); + setSelected( task, true ); + task->setPixmapProgress(); + save(); + } + else + { + delete task; + } + return taskuid; +} + +void TaskView::newSubTask() +{ + Task* task = current_item(); + if(!task) + return; + newTask(i18n("New Sub Task"), task); + task->setOpen(true); + refresh(); +} + +void TaskView::editTask() +{ + Task *task = current_item(); + if (!task) + return; + + DesktopList desktopList = task->getDesktops(); + EditTaskDialog *dialog = new EditTaskDialog(i18n("Edit Task"), true, &desktopList); + dialog->setTask( task->name(), + task->time(), + task->sessionTime() ); + int result = dialog->exec(); + if (result == QDialog::Accepted) { + QString taskName = i18n("Unnamed Task"); + if (!dialog->taskName().isEmpty()) { + taskName = dialog->taskName(); + } + // setName only does something if the new name is different + task->setName(taskName, _storage); + + // update session time as well if the time was changed + long total, session, totalDiff, sessionDiff; + total = totalDiff = session = sessionDiff = 0; + DesktopList desktopList; + dialog->status( &total, &totalDiff, &session, &sessionDiff, &desktopList); + + if( totalDiff != 0 || sessionDiff != 0) + task->changeTimes( sessionDiff, totalDiff, _storage ); + + // If all available desktops are checked, disable auto tracking, + // since it makes no sense to track for every desktop. + if (desktopList.size() == (unsigned int)_desktopTracker->desktopCount()) + desktopList.clear(); + + task->setDesktopList(desktopList); + + _desktopTracker->registerForDesktops( task, desktopList ); + + emit updateButtons(); + } + delete dialog; +} + +//void TaskView::addCommentToTask() +//{ +// Task *task = current_item(); +// if (!task) +// return; + +// bool ok; +// QString comment = KLineEditDlg::getText(i18n("Comment"), +// i18n("Log comment for task '%1':").arg(task->name()), +// QString(), &ok, this); +// if ( ok ) +// task->addComment( comment, _storage ); +//} + +void TaskView::reinstateTask(int completion) +{ + Task* task = current_item(); + if (task == 0) { + KMessageBox::information(0,i18n("No task selected.")); + return; + } + + if (completion<0) completion=0; + if (completion<100) + { + task->setPercentComplete(completion, _storage); + task->setPixmapProgress(); + save(); + emit updateButtons(); + } +} + +void TaskView::deleteTask(bool markingascomplete) +{ + Task *task = current_item(); + if (task == 0) { + KMessageBox::information(0,i18n("No task selected.")); + return; + } + + int response = KMessageBox::Continue; + if (!markingascomplete && _preferences->promptDelete()) { + if (task->childCount() == 0) { + response = KMessageBox::warningContinueCancel( 0, + i18n( "Are you sure you want to delete " + "the task named\n\"%1\" and its entire history?") + .arg(task->name()), + i18n( "Deleting Task"), KStdGuiItem::del()); + } + else { + response = KMessageBox::warningContinueCancel( 0, + i18n( "Are you sure you want to delete the task named" + "\n\"%1\" and its entire history?\n" + "NOTE: all its subtasks and their history will also " + "be deleted.").arg(task->name()), + i18n( "Deleting Task"), KStdGuiItem::del()); + } + } + + if (response == KMessageBox::Continue) + { + if (markingascomplete) + { + task->setPercentComplete(100, _storage); + task->setPixmapProgress(); + save(); + emit updateButtons(); + + // Have to remove after saving, as the save routine only affects tasks + // that are in the view. Otherwise, the new percent complete does not + // get saved. (No longer remove when marked as complete.) + //task->removeFromView(); + + } + else + { + QString uid=task->uid(); + task->remove(activeTasks, _storage); + task->removeFromView(); + if( _preferences ) _preferences->deleteEntry( uid ); // forget if the item was expanded or collapsed + save(); + } + + // remove root decoration if there is no more children. + refresh(); + + // Stop idle detection if no more counters are running + if (activeTasks.count() == 0) { + _idleTimeDetector->stopIdleDetection(); + emit timersInactive(); + } + + emit tasksChanged( activeTasks ); + } +} + +void TaskView::extractTime(int minutes) +// This procedure subtracts ''minutes'' from the active task's time in the memory. +// It is called by the idletimedetector class. +// When the desktop has been idle for the past 20 minutes, the past 20 minutes have +// already been added to the task's time in order for the time to be displayed correctly. +// That is why idletimedetector needs to subtract this time first. +{ + kdDebug(5970) << "Entering extractTime" << endl; + addTimeToActiveTasks(-minutes,false); // subtract minutes, but do not store it +} + +void TaskView::autoSaveChanged(bool on) +{ + if (on) _autoSaveTimer->start(_preferences->autoSavePeriod()*1000*secsPerMinute); + else if (_autoSaveTimer->isActive()) _autoSaveTimer->stop(); +} + +void TaskView::autoSavePeriodChanged(int /*minutes*/) +{ + autoSaveChanged(_preferences->autoSave()); +} + +void TaskView::adaptColumns() +{ + // to hide a column X we set it's width to 0 + // at that moment we'll remember the original column within + // previousColumnWidths[X] + // + // When unhiding a previously hidden column + // (previousColumnWidths[X] != HIDDEN_COLUMN !) + // we restore it's width from the saved value and set + // previousColumnWidths[X] to HIDDEN_COLUMN + + for( int x=1; x <= 4; x++) { + // the column was invisible before and were switching it on now + if( _preferences->displayColumn(x-1) + && previousColumnWidths[x-1] != HIDDEN_COLUMN ) + { + setColumnWidth( x, previousColumnWidths[x-1] ); + previousColumnWidths[x-1] = HIDDEN_COLUMN; + setColumnWidthMode( x, QListView::Maximum ); + } + // the column was visible before and were switching it off now + else + if( ! _preferences->displayColumn(x-1) + && previousColumnWidths[x-1] == HIDDEN_COLUMN ) + { + setColumnWidthMode( x, QListView::Manual ); // we don't want update() + // to resize/unhide the col + previousColumnWidths[x-1] = columnWidth( x ); + setColumnWidth( x, 0 ); + } + } +} + +void TaskView::deletingTask(Task* deletedTask) +{ + DesktopList desktopList; + + _desktopTracker->registerForDesktops( deletedTask, desktopList ); + activeTasks.removeRef( deletedTask ); + + emit tasksChanged( activeTasks); +} + +void TaskView::iCalFileChanged(QString file) +// User might have picked a new file in the preferences dialog. +// This is not iCalFileModified. +{ + kdDebug(5970) << "TaskView:iCalFileChanged: " << file << endl; + if (_storage->icalfile() != file) + { + stopAllTimers(); + _storage->save(this); + load(); + } +} + +QValueList<HistoryEvent> TaskView::getHistory(const QDate& from, + const QDate& to) const +{ + return _storage->getHistory(from, to); +} + +void TaskView::markTaskAsComplete() +{ + if (current_item()) + kdDebug(5970) << "TaskView::markTaskAsComplete: " + << current_item()->uid() << endl; + else + kdDebug(5970) << "TaskView::markTaskAsComplete: null current_item()" << endl; + + bool markingascomplete = true; + deleteTask(markingascomplete); +} + +void TaskView::markTaskAsIncomplete() +{ + if (current_item()) + kdDebug(5970) << "TaskView::markTaskAsComplete: " + << current_item()->uid() << endl; + else + kdDebug(5970) << "TaskView::markTaskAsComplete: null current_item()" << endl; + + reinstateTask(50); // if it has been reopened, assume half-done +} + + +void TaskView::clipTotals() +{ + TimeKard t; + if (current_item() && current_item()->isRoot()) + { + int response = KMessageBox::questionYesNo( 0, + i18n("Copy totals for just this task and its subtasks, or copy totals for all tasks?"), + i18n("Copy Totals to Clipboard"), + i18n("Copy This Task"), i18n("Copy All Tasks") ); + if (response == KMessageBox::Yes) // This task only + { + KApplication::clipboard()->setText(t.totalsAsText(this, true, TimeKard::TotalTime)); + } + else // All tasks + { + KApplication::clipboard()->setText(t.totalsAsText(this, false, TimeKard::TotalTime)); + } + } + else + { + KApplication::clipboard()->setText(t.totalsAsText(this, true, TimeKard::TotalTime)); + } +} + +void TaskView::clipSession() +{ + TimeKard t; + if (current_item() && current_item()->isRoot()) + { + int response = KMessageBox::questionYesNo( 0, + i18n("Copy session time for just this task and its subtasks, or copy session time for all tasks?"), + i18n("Copy Session Time to Clipboard"), + i18n("Copy This Task"), i18n("Copy All Tasks") ); + if (response == KMessageBox::Yes) // this task only + { + KApplication::clipboard()->setText(t.totalsAsText(this, true, TimeKard::SessionTime)); + } + else // only task + { + KApplication::clipboard()->setText(t.totalsAsText(this, false, TimeKard::SessionTime)); + } + } + else + { + KApplication::clipboard()->setText(t.totalsAsText(this, true, TimeKard::SessionTime)); + } +} + +void TaskView::clipHistory() +{ + PrintDialog dialog; + if (dialog.exec()== QDialog::Accepted) + { + TimeKard t; + KApplication::clipboard()-> + setText( t.historyAsText(this, dialog.from(), dialog.to(), !dialog.allTasks(), dialog.perWeek(), dialog.totalsOnly() ) ); + } +} + +#include "taskview.moc" diff --git a/karm/taskview.h b/karm/taskview.h new file mode 100644 index 000000000..f1cf78078 --- /dev/null +++ b/karm/taskview.h @@ -0,0 +1,238 @@ +#ifndef KARM_TASK_VIEW_H +#define KARM_TASK_VIEW_H + +#include <qdict.h> +#include <qptrlist.h> +#include <qptrstack.h> + +#include <klistview.h> + +#include "desktoplist.h" +#include "resourcecalendar.h" +#include "karmstorage.h" +#include "mainwindow.h" +#include "reportcriteria.h" +#include <qtimer.h> +//#include "desktoptracker.h" + +//#include "karmutility.h" + +class QListBox; +class QString; +class QTextStream; +class QTimer; + +class KMenuBar; +class KToolBar; + +class DesktopTracker; +class EditTaskDialog; +class IdleTimeDetector; +class Preferences; +class Task; +class KarmStorage; +class HistoryEvent; + +using namespace KCal; + +/** + * Container and interface for the tasks. + */ + +class TaskView : public KListView +{ + Q_OBJECT + + public: + TaskView( QWidget *parent = 0, const char *name = 0, const QString &icsfile = "" ); + virtual ~TaskView(); + + /** Return the first item in the view, cast to a Task pointer. */ + Task* first_child() const; + + /** Return the current item in the view, cast to a Task pointer. */ + Task* current_item() const; + + /** Return the i'th item (zero-based), cast to a Task pointer. */ + Task* item_at_index(int i); + + /** Load the view from storage. */ + void load( QString filename="" ); + + /** Close the storage and release lock. */ + void closeStorage(); + + /** Reset session time to zero for all tasks. */ + void startNewSession(); + + /** Reset session and total time to zero for all tasks. */ + void resetTimeForAllTasks(); + + /** Return the total number if items in the view. */ + long count(); + + /** Return list of start/stop events for given date range. */ + QValueList<HistoryEvent> getHistory(const QDate& from, const QDate& to) const; + + /** Schedule that we should save very soon */ + void scheduleSave(); + + /** Return preferences user selected on settings dialog. **/ + Preferences *preferences(); + + /** Add a task to view and storage. */ + QString addTask( const QString& taskame, long total, long session, const DesktopList& desktops, + Task* parent = 0 ); + + public slots: + /** Save to persistent storage. */ + QString save(); + + /** Start the timer on the current item (task) in view. */ + void startCurrentTimer(); + + /** Stop the timer for the current item in the view. */ + void stopCurrentTimer(); + + /** Stop all running timers. */ + void stopAllTimers(); + + /** Stop all running timers as if it was qdt */ + void stopAllTimersAt(QDateTime qdt); + + /** Calls newTask dialog with caption "New Task". */ + void newTask(); + + /** Display edit task dialog and create a new task with results. */ + void newTask( QString caption, Task* parent ); + + /** Used to refresh (e.g. after import) */ + void refresh(); + + /** Used to import a legacy file format. */ + void loadFromFlatFile(); + + /** used to import tasks from imendio planner */ + QString importPlanner( QString fileName="" ); + + /** call export function for csv totals or history */ + QString report( const ReportCriteria &rc ); + + /** Export comma separated values format for task time totals. */ + void exportcsvFile(); + + /** Export comma-separated values format for task history. */ + QString exportcsvHistory(); + + /** Calls newTask dialog with caption "New Sub Task". */ + void newSubTask(); + + void editTask(); + + /** + * Returns a pointer to storage object. + * + * This is poor object oriented design--the task view should + * expose wrappers around the storage methods we want to access instead of + * giving clients full access to objects that we own. + * + * Hopefully, this will be redesigned as part of the Qt4 migration. + */ + KarmStorage* storage(); + + /** + * Delete task (and children) from view. + * + * @param markingascomplete If false (the default), deletes history for + * current task and all children. If markingascomplete is true, then sets + * percent complete to 100 and removes task and all it's children from the + * list view. + */ + void deleteTask(bool markingascomplete=false); + + /** Reinstates the current task as incomplete. + * @param completion The percentage complete to mark the task as. */ + void reinstateTask(int completion); +// void addCommentToTask(); + void markTaskAsComplete(); + void markTaskAsIncomplete(); + + /** Subtracts time from all active tasks, and does not log event. + * The time is stored in memory and in X-KDE-karm-duration. It is + * increased automatically every minute to display the right duration. + */ + void extractTime( int minutes ); + void taskTotalTimesChanged( long session, long total) + { emit totalTimesChanged( session, total); }; + void adaptColumns(); + /** receiving signal that a task is being deleted */ + void deletingTask(Task* deletedTask); + + /** starts timer for task. + * @param task task to start timer of + * @param startTime if taskview has been modified by another program, we + have to set the starting time to not-now. */ + void startTimerFor( Task* task, QDateTime startTime = QDateTime::currentDateTime() ); + void stopTimerFor( Task* task ); + + /** clears all active tasks. Needed e.g. if iCal file was modified by + another program and taskview is cleared without stopping tasks + IF YOU DO NOT KNOW WHAT YOU ARE DOING, CALL stopAllTimers INSTEAD */ + void clearActiveTasks(); + + /** User might have picked a new iCalendar file on preferences screen. + Verify the file is not the same as before and load the new one. + This is not iCalFileModified. */ + void iCalFileChanged(QString file); + + /** Copy totals for current and all sub tasks to clipboard. */ + void clipTotals(); + + /** Copy session times for current and all sub tasks to clipboard */ + void clipSession(); + + /** Copy history for current and all sub tasks to clipboard. */ + void clipHistory(); + + signals: + void totalTimesChanged( long session, long total ); + void updateButtons(); + void timersActive(); + void timersInactive(); + void tasksChanged( QPtrList<Task> activeTasks ); + void setStatusBar( QString ); + + private: // member variables + IdleTimeDetector *_idleTimeDetector; + QTimer *_minuteTimer; + QTimer *_autoSaveTimer; + QTimer *_manualSaveTimer; + Preferences *_preferences; + QPtrList<Task> activeTasks; + int previousColumnWidths[4]; + DesktopTracker* _desktopTracker; + bool _isloading; + + //KCal::CalendarLocal _calendar; + KarmStorage * _storage; + + private: + void contentsMousePressEvent ( QMouseEvent * e ); + void contentsMouseDoubleClickEvent ( QMouseEvent * e ); + void updateParents( Task* task, long totalDiff, long sesssionDiff); + void deleteChildTasks( Task *item ); + void addTimeToActiveTasks( int minutes, bool save_data = true ); + /** item state stores if a task is expanded so you can see the subtasks */ + void restoreItemState( QListViewItem *item ); + + protected slots: + void autoSaveChanged( bool ); + void autoSavePeriodChanged( int period ); + void minuteUpdate(); + /** item state stores if a task is expanded so you can see the subtasks */ + void itemStateChanged( QListViewItem *item ); + /** React on another process having modified the iCal file we rely on. */ + void iCalFileModified(ResourceCalendar *); +}; + +#endif // KARM_TASK_VIEW diff --git a/karm/taskviewwhatsthis.cpp b/karm/taskviewwhatsthis.cpp new file mode 100644 index 000000000..9beb163df --- /dev/null +++ b/karm/taskviewwhatsthis.cpp @@ -0,0 +1,41 @@ +// +// C++ Implementation: taskviewwhatsthis +// +// Description: +// This is a subclass of QWhatsThis, specially adapted for karm's taskview. +// +// Author: Thorsten Staerk <[email protected]>, (C) 2005 +// +// Copyright: See COPYING file that comes with this distribution +// +// +#include "taskviewwhatsthis.h" +#include <kdebug.h> +#include <klistview.h> +#include <klocale.h> + +TaskViewWhatsThis::TaskViewWhatsThis( QWidget* qw ) + : QWhatsThis( qw ) +{ + _listView=(KListView *) qw; +} + +TaskViewWhatsThis::~TaskViewWhatsThis() +{ +} + +QString TaskViewWhatsThis::text ( const QPoint & pos ) +{ + QString desc = QString::null; + kdDebug(5970) << "entering TaskViewWhatsThis::text" << endl; + kdDebug(5970) << "x-pos:" << pos.x() << endl; + if ( pos.x() < _listView->columnWidth( 0 ) ) + { + desc=i18n("Task Name shows the name of a task or subtask you are working on."); + } + else + { + desc=i18n("Session time: Time for this task since you chose \"Start New Session\".\nTotal Session time: Time for this task and all its subtasks since you chose \"Start New Session\".\nTime: Overall time for this task.\nTotal Time: Overall time for this task and all its subtasks."); + } + return desc; +} diff --git a/karm/taskviewwhatsthis.h b/karm/taskviewwhatsthis.h new file mode 100644 index 000000000..af8a82969 --- /dev/null +++ b/karm/taskviewwhatsthis.h @@ -0,0 +1,32 @@ +// +// C++ Interface: taskviewwhatsthis +// +// +// Author: Thorsten Staerk <[email protected]>, (C) 2005 +// +// Copyright: See COPYING file that comes with this distribution +// +// +#ifndef TASKVIEWWHATSTHIS_H +#define TASKVIEWWHATSTHIS_H + +#include <qwhatsthis.h> +#include <klistview.h> + +/** +this is the karm-taskview-specific implementation of qwhatsthis + +@author Thorsten Staerk +*/ +class TaskViewWhatsThis : public QWhatsThis +{ +public: + TaskViewWhatsThis( QWidget* ); + ~TaskViewWhatsThis(); + + QString text ( const QPoint & ); + +private: + KListView* _listView; // stores the associated listview for column widths +}; +#endif diff --git a/karm/test/Makefile.am b/karm/test/Makefile.am new file mode 100644 index 000000000..7205a5995 --- /dev/null +++ b/karm/test/Makefile.am @@ -0,0 +1,18 @@ +METASOURCES = AUTO + +check_PROGRAMS = runscripts locking + +runscripts_SOURCES = script.cpp runscripts.cpp +runscripts_LDFLAGS = $(all_libraries) $(KDE_RPATH) +runscripts_LDADD = $(LIB_QT) $(LIB_KDECORE) + +locking_SOURCES = locking.cpp lockerthread.cpp +locking_LDFLAGS = $(all_libraries) $(KDE_RPATH) +locking_LDADD = $(LIB_QT) $(top_builddir)/libkcal/libkcal.la + +KDE_CXXFLAGS = $(USE_EXCEPTIONS) + +INCLUDES = -I$(srcdir) -I$(top_srcdir) -I$(top_srcdir)/libkcal $(all_includes) + +#TESTS = runscripts locking +TESTS = locking diff --git a/karm/test/README b/karm/test/README new file mode 100644 index 000000000..31dbc8d24 --- /dev/null +++ b/karm/test/README @@ -0,0 +1,80 @@ +This directory holds automated tests for karm. + +It's in very rough shape. + + +How you start: + + (1) get and install kdepim including karm + + (2) get and install xautomation from http://hoopajoo.net/projects/xautomation.html + + (3) get and install Net::DAV::Server with CPAN + + (4) get and install Net::Virtual::Plain with CPAN + + (5) get and install File::Find::Rule::Filesys::Virtual with CPAN + + (6) start the automated tests with the command make check + + +Here are some of the issues: + + (1) The tests require KDE to be running in English. + + The automated XTests use shortcut keys to drive the app. These key + combinations are language specific. + + (2) The tests require that you "make install" first. + + The XAutomation tests and tests that use DCOP run karm from the bash + prompt. + + (3) The tests are destructive. + + If you have an already running instance of karm, the tests will kill + that instance. So you cannot, for example, record time spent running + karm automated tests. + + The tests are smart enough to use test iCalendar files. But they will + alter the karm storage settings for the ics file name. + + (4) The runscripts program does not kill scripts that never return. + + You have to monitor the progress and press Control-C if you think a + script is hung. + + (5) No attempt is made to check for installed script interpreters. + + In addition to required interpreters (Python 2.2, for example), the bash + scripts use xte, which on Debian is in the xautomation package. + + If a required library is not found, runscripts will consider this a + script failure and stop. + + (6) I have only tested this with Bash on GNU/Linux. + + (7) When a test fails, it is really hard to figure out why. There is too + much stuff mixed together on the console output, and the tests + themselves do not give much info when they fail. + +That having been said, I find it so useful that I will keep working on this so +it should get better shortly. + + +Some notes on runscripts.cpp: + + Parses this directory for script files (Python, PHP, Perl and Bash). It + identifies a script file by the extension. + + It runs any script files it finds. Within each script type, it runs the + scripts in alphabetical order. Scripts that start with a double underscore + are skipped. + + When a script fails runscripts stops. + + Script files should return a non-zero exit code to indicate a failure. + +-- +Mark Bucciarelli <[email protected]> +December 6, 2004 diff --git a/karm/test/__httpd.py b/karm/test/__httpd.py new file mode 100644 index 000000000..ba87113be --- /dev/null +++ b/karm/test/__httpd.py @@ -0,0 +1,39 @@ +'''Runs an HTTP server on port 8000 (or the first command line argument).''' + +import BaseHTTPServer +import SimpleHTTPServer +import sys +import os.path + +class MyHandler( SimpleHTTPServer.SimpleHTTPRequestHandler ): + + def do_PUT( self ): + '''Just enough to work with karm.''' + path = self.translate_path(self.path) + + rval = 200 + if not os.path.exists( path ): rval = 201 + + f = file( path, "w" ) + lines = [] + while 1: + line = self.rfile.readline() + lines.append( line ) + if line == '\r\n' or line == '\n' or line == '': + break + f.writelines( lines ) + self.send_response( rval ) + + +DEFAULT_PORT = 8000 + +if sys.argv[1:]: port = int(sys.argv[1]) +else: port = DEFAULT_PORT +server_address = ('', port) + +SimpleHTTPServer.SimpleHTTPRequestHandler.protocol_version = "HTTP/1.0" +httpd = BaseHTTPServer.HTTPServer(server_address, MyHandler ) + +sa = httpd.socket.getsockname() +print "Serving HTTP on", sa[0], "port", sa[1], "..." +httpd.serve_forever() diff --git a/karm/test/__korg.sh b/karm/test/__korg.sh new file mode 100755 index 000000000..e7fa9629f --- /dev/null +++ b/karm/test/__korg.sh @@ -0,0 +1,134 @@ +#!/bin/bash + +# Example of how to use xautomation. +# +# Notes: +# +# - This test fails for korg, as it opens a modal dialog box, even +# after changes are saved (ref bug #94537). +# +# - The xte str command is broken (it types to fast). To be safe, +# put in characters one-by-one using the key command. Writing a +# bash function that does this for a given string would be helpful. +# +# - This script uses hardcoded English short cut keys. To make generic: +# +# 1. Get two char language code from OS (or KDE?) +# +# 2. Source bash script for language (e.g. source __shortcuts.en) +# +# 3. Call functions from that script (e.g. open_file $FILENAME) +# +# - Using killall isn't great. karm has a quit() dcop function for clean +# shutdown. Need to check if korgac or korganizer provide this interface. + + +# kill any running processes +killall korganizer +killall korgac + +# start korganizer +korganizer& + +# make sure it's done opening +sleep 10 + +# open file in korganizer. +# Note: this opens a second korg instance +xte 'keydown Alt_L' +xte 'key F' +xte 'keyup Alt_L' +xte 'key O' + +# wait for open file dialog to come up +sleep 1 + +# put focus on file name control +xte 'keydown Alt_L' +xte 'key L' +xte 'keyup Alt_L' +xte 'key ~' +xte 'key /' +xte 'key t' +xte 'key e' +xte 'key s' +xte 'key t' +xte 'key .' +xte 'key i' +xte 'key c' +xte 'key s' + +# save new storage file +xte 'keydown Alt_L' +xte 'key O' +xte 'keyup Alt_L' + +sleep 1 + +# open new to-do dialog +xte 'keydown Alt_L' +xte 'key A' +xte 'keup Alt_L' +xte 'key v' + +sleep 1 + +# set focus to title +xte 'keydown Alt_L' +xte 'key I' +xte 'keyup Alt_L' + +# type in test task name (you have to type slowly for xte) +xte 'key T' +xte 'key e' +xte 'key s' +xte 'key t' +xte 'key -' +xte 'key t' +xte 'key a' +xte 'key s' +xte 'key k' +xte 'key -' +xte 'key 1' + +sleep 1 + +# save new todo +xte 'keydown Alt_L' +xte 'key O' +xte 'keyup Alt_L' + +sleep 1 + +# save changes to file +xte 'keydown Alt_L' +xte 'key F' +xte 'keyup Alt_L' +xte 'key S' + +sleep 1 + +# Quit below fails. +# +# korg pops up a dialog that says: +# +# The calendar contains unsaved changes. +# Do you want to save them before exiting? + +# quit korganizer +xte 'keydown Control_L' +xte 'key q' +xte 'keyup Control_L' + +# quit first korganizer view +xte 'keydown Control_L' +xte 'key q' +xte 'keyup Control_L' + +sleep 1 + +killall korgac + +# 1. cleanup +rm "~/test.ics" + diff --git a/karm/test/__lib.sh b/karm/test/__lib.sh new file mode 100644 index 000000000..06fd9df62 --- /dev/null +++ b/karm/test/__lib.sh @@ -0,0 +1,79 @@ + +# Expects karm test file in $TESTFILE +# Returns dcop id in $DCOP_ID +function set_up() +{ + DCOPID=`dcop 2>/dev/null | grep karm` + + if [ -n "$DCOPID" ]; then dcop $DCOPID KarmDCOPIface quit; fi; + + if [ "x$SKIP_TESTFILE_DELETE" != "xtrue" ]; then + if [ -e "$TESTFILE" ]; then rm $TESTFILE; fi + fi + + #echo "__lib.sh - starting karm with $TESTFILE" + karm "$TESTFILE" & + + # Make sure karm is up and running + limit=10 + idx=0 + DCOPID="" + while [ "$idx" -lt "$limit" ] + do + #echo "__lib.sh: dcop 2>/dev/null | grep karm" + DCOPID=`dcop 2>/dev/null | grep karm` + if [ -n "$DCOPID" ] + then + break + else + let "idx += 1" + fi + sleep 1 + done + + # It's not enough to get the dcop id, as this is available almost + # immediately. We need to make sure karm (and fam) is done loading data. + limit=20 + idx=0 + KARM_VERSION="" + while [ "$idx" -lt "$limit" ] + do + #echo "__lib.sh: dcop $DCOPID KarmDCOPIface version 2>/dev/null" + KARM_VERSION=`dcop $DCOPID KarmDCOPIface version 2>/dev/null` + if [ -n "$KARM_VERSION" ] + then + break + else + let "idx += 1" + fi + sleep 1 + done + + if [ "x$DCOPID" = x ] + then + echo "__lib.sh set_up error: could not start karm--no dcop id." + exit 1 + else + echo "__lib.sh: DCOPID = $DCOPID, KARM_VERSION = $KARM_VERSION" + fi + + if [ "x$KARM_VERSION" = x ] + then + echo "__lib.sh set_up error: karm did not return a version string." + exit 1 + fi +} + +function test_func() +{ + echo "Yep, that works." +} + +function tear_down() +{ + if [ -n "$DCOPID" ]; then dcop "$DCOPID" KarmDCOPIface quit; fi; + + if [ "x$SKIP_TESTFILE_DELETE" != "xtrue" ]; then + if [ -e "$TESTFILE" ]; then rm "$TESTFILE"; fi + fi +} diff --git a/karm/test/__webdav.pl b/karm/test/__webdav.pl new file mode 100755 index 000000000..b09875755 --- /dev/null +++ b/karm/test/__webdav.pl @@ -0,0 +1,57 @@ +#!/usr/bin/perl -w + +# This script requires the following Perl modules: +# +# $ perl -MCPAN -e 'shell' +# cpan> install Net::DAV::Server +# cpan> install Filesys::Virtual::Plain +# cpan> install File::Find::Rule::Filesys::Virtual +# cpan> install XML::LibXML +# +# The last Perl module needs the libxml2 development libraries installed +# (the libxml2-dev package on Debian). + +use Net::DAV::Server; +use Filesys::Virtual::Plain; +use HTTP::Daemon; + +# If 1, output request and response headers +my $DEBUG=0; + +my $filesys = Filesys::Virtual::Plain->new(); +$filesys->root_path('/tmp'); +$filesys->cwd('/tmp'); +#print foreach ($filesys->list('/')); + +my $webdav = Net::DAV::Server->new(); +$webdav->filesys($filesys); + +my $d = new HTTP::Daemon + LocalAddr => 'localhost', + LocalPort => 4242, + ReuseAddr => 1 || die; +print "Please contact me at: ", $d->url, "\n"; +while (my $c = $d->accept) { + while (my $request = $c->get_request) { + if ( $DEBUG ) { + print qq|------------------------------------------------------------ +REQUEST +------------------------------------------------------------\n|; + while ( ($k,$v) = each %{$request} ) { + print " $k => $v\n"; + } + } + my $response = $webdav->run($request); + if ( $DEBUG ) { + print qq|------------------------------------------------------------ +RESPONSE +------------------------------------------------------------\n|; + while ( ($k,$v) = each %{$response} ) { + print " $k => $v\n"; + } + } + $c->send_response ($response); + } + $c->close; + undef($c); +} diff --git a/karm/test/booktime-baddate.sh b/karm/test/booktime-baddate.sh new file mode 100755 index 000000000..d89e54cb1 --- /dev/null +++ b/karm/test/booktime-baddate.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +TESTFILE="/tmp/testkarm1.ics" + +source __lib.sh + +set_up + +dcop $DCOPID KarmDCOPIface addTask Task1 2>/dev/null +TASKID=`dcop $DCOPID KarmDCOPIface taskIdFromName Task1 2>/dev/null` +RVAL=`dcop $DCOPID KarmDCOPIface bookTime $TASKID notadate 360 2>/dev/null` + +tear_down + +EXPECTED=5 +if [ "$RVAL" == "$EXPECTED" ]; then + echo "PASS $0" + exit 0; +else + echo "FAIL $0: got /$RVAL/, expected /$EXPECTED/" + exit 1; +fi diff --git a/karm/test/booktime-badduration.sh b/karm/test/booktime-badduration.sh new file mode 100755 index 000000000..2b1e8c96b --- /dev/null +++ b/karm/test/booktime-badduration.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +TESTFILE="/tmp/testkarm1.ics" + +source __lib.sh + +set_up + +dcop $DCOPID KarmDCOPIface addTask Task1 2>/dev/null +TASKID=`dcop $DCOPID KarmDCOPIface taskIdFromName Task1 2>/dev/null` +RVAL=`dcop $DCOPID KarmDCOPIface bookTime $TASKID 20050619 notanumber 2>/dev/null` + +tear_down + +EXPECTED=7 +if [ "$RVAL" == "$EXPECTED" ]; then + echo "PASS $0" + exit 0; +else + echo "FAIL $0: got /$RVAL/, expected /$EXPECTED/" + exit 1; +fi diff --git a/karm/test/booktime-badtime.sh b/karm/test/booktime-badtime.sh new file mode 100755 index 000000000..1bbaa3d53 --- /dev/null +++ b/karm/test/booktime-badtime.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +TESTFILE="/tmp/testkarm1.ics" + +source __lib.sh + +set_up + +dcop $DCOPID KarmDCOPIface addTask Task1 2>/dev/null +TASKID=`dcop $DCOPID KarmDCOPIface taskIdFromName Task1 2>/dev/null` +RVAL=`dcop $DCOPID KarmDCOPIface bookTime $TASKID 20050619Tabc 360 2>/dev/null` + +tear_down + +EXPECTED=5 +if [ "$RVAL" == "$EXPECTED" ]; then + echo "PASS $0" + exit 0; +else + echo "FAIL $0: got /$RVAL/, expected /$EXPECTED/" + exit 1; +fi diff --git a/karm/test/booktime-baduid.sh b/karm/test/booktime-baduid.sh new file mode 100755 index 000000000..8cb064244 --- /dev/null +++ b/karm/test/booktime-baduid.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# First test, just check version. + +TESTFILE="/tmp/testkarm1.ics" + +source __lib.sh + +set_up + +RVAL=`dcop $DCOPID KarmDCOPIface bookTime bad-uid 20050619 360 2>/dev/null` + +tear_down + +EXPECTED=4 +if [ "$RVAL" == "$EXPECTED" ]; then + echo "PASS $0" + exit 0; +else + echo "FAIL $0: got /$RVAL/, expected /$EXPECTED/" + exit 1; +fi diff --git a/karm/test/booktime-works.sh b/karm/test/booktime-works.sh new file mode 100755 index 000000000..280e1231b --- /dev/null +++ b/karm/test/booktime-works.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +TESTFILE="/tmp/testkarm1.ics" + +source __lib.sh + +set_up + +DURATION=12 +dcop $DCOPID KarmDCOPIface addTask Task1 2>/dev/null +TASKID=`dcop $DCOPID KarmDCOPIface taskIdFromName Task1 2>/dev/null` +RVAL=`dcop $DCOPID KarmDCOPIface bookTime $TASKID 2005-06-19 $DURATION 2>/dev/null` + +if [ "x$RVAL" != "x0" ]; then + echo "FAIL $0: got /$RVAL/, expected /$EXPECTED/" + exit 1; +fi + +RVAL=`dcop $DCOPID KarmDCOPIface totalMinutesForTaskId $TASKID 2>/dev/null` + +SKIP_TESTFILE_DELETE=true +tear_down + +if [ "x$RVAL" == "x$DURATION" ]; then + echo "PASS $0" + exit 0; +else + echo "FAIL $0: got /$RVAL/, expected /$DURATION/" + exit 1; +fi diff --git a/karm/test/bug94447.sh b/karm/test/bug94447.sh new file mode 100755 index 000000000..fa163735b --- /dev/null +++ b/karm/test/bug94447.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# Create files relative to current directory if no "/" prefix +# in file name given on command line + +exec >>check.log 2>&1 + +TESTFILE="testkarm.ics" +TESTTODO="testtodo" + +source __lib.sh + +set_up + +# make karm create the file. +dcop $DCOPID KarmDCOPIface addtodo "$TESTTODO" + +RVAL=1 +if [ -e $TESTFILE ]; then RVAL=0; fi + +tear_down + +if [ $RVAL -eq 0 ] +then + echo "PASS $0" + exit 0 +else + echo "FAIL $0" + exit 1 +fi diff --git a/karm/test/delete.sh b/karm/test/delete.sh new file mode 100755 index 000000000..78f04a7de --- /dev/null +++ b/karm/test/delete.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# check if deleting a task works + +exec >> check.log 2>&1 + +TESTFILE="/tmp/testkarm1.ics" +rm $TESTFILE + +source __lib.sh + +set_up + +# do not prompt on deleting a task +GPOD=`dcop $DCOPID KarmDCOPIface getpromptdelete 2>/dev/null` +RVAL=`dcop $DCOPID KarmDCOPIface setpromptdelete 0 2>/dev/null` + +RVAL=`dcop $DCOPID KarmDCOPIface addTask todo1 2>/dev/null` +RVAL=`dcop $DCOPID KarmDCOPIface addTask todo2 2>/dev/null` +RVAL=`dcop $DCOPID KarmDCOPIface deletetodo 2>/dev/null` +RVAL=`dcop $DCOPID KarmDCOPIface deletetodo 2>/dev/null` +RVAL=`dcop $DCOPID KarmDCOPIface setpromptdelete $GPOD 2>/dev/null` +RVAL=`dcop $DCOPID KarmDCOPIface version 2>/dev/null` + +tear_down + +if [ "$RVAL" == "" ]; then + echo "FAIL $0: got no version." + exit 1; +else + echo "PASS $0" + exit 0; +fi diff --git a/karm/test/example.ics b/karm/test/example.ics new file mode 100644 index 000000000..8cfe93525 --- /dev/null +++ b/karm/test/example.ics @@ -0,0 +1,173 @@ +BEGIN:VCALENDAR +PRODID:-//K Desktop Environment//NONSGML libkcal 3.5//EN +VERSION:2.0 +BEGIN:VTODO +DTSTAMP:20061125T204432Z +ORGANIZER;CN=root:MAILTO: +X-KDE-karm-totalSessionTime:0 +X-KDE-karm-totalTaskTime:0 +CREATED:20061118T183926Z +UID:libkcal-1156600660.458 +SEQUENCE:0 +LAST-MODIFIED:20061125T204432Z +SUMMARY:customer BÄR +CLASS:PUBLIC +PRIORITY:3 +PERCENT-COMPLETE:0 +END:VTODO + +BEGIN:VTODO +DTSTAMP:20061125T204432Z +ORGANIZER;CN=root:MAILTO: +X-KDE-karm-totalSessionTime:0 +X-KDE-karm-totalTaskTime:0 +CREATED:20061118T183934Z +UID:libkcal-1923273586.967 +SEQUENCE:0 +LAST-MODIFIED:20061125T204432Z +SUMMARY:documenting +CLASS:PUBLIC +PRIORITY:3 +RELATED-TO:libkcal-1156600660.458 +PERCENT-COMPLETE:0 +END:VTODO + +BEGIN:VTODO +DTSTAMP:20061125T204432Z +ORGANIZER;CN=root:MAILTO: +X-KDE-karm-totalSessionTime:90 +X-KDE-karm-totalTaskTime:90 +CREATED:20061118T184032Z +UID:libkcal-61950406.193 +SEQUENCE:0 +LAST-MODIFIED:20061125T204432Z +SUMMARY:analysis +CLASS:PUBLIC +PRIORITY:3 +RELATED-TO:libkcal-1156600660.458 +PERCENT-COMPLETE:0 +END:VTODO + +BEGIN:VTODO +DTSTAMP:20061125T204432Z +ORGANIZER;CN=root:MAILTO: +X-KDE-karm-totalSessionTime:184 +X-KDE-karm-totalTaskTime:184 +CREATED:20061118T184044Z +UID:libkcal-1755408865.833 +SEQUENCE:0 +LAST-MODIFIED:20061125T204432Z +SUMMARY:programming +CLASS:PUBLIC +PRIORITY:3 +RELATED-TO:libkcal-1156600660.458 +PERCENT-COMPLETE:0 +END:VTODO + +BEGIN:VTODO +DTSTAMP:20061125T204432Z +ORGANIZER;CN=root:MAILTO: +X-KDE-karm-totalSessionTime:156 +X-KDE-karm-totalTaskTime:156 +CREATED:20061118T184054Z +UID:libkcal-1177224330.526 +SEQUENCE:0 +LAST-MODIFIED:20061125T204432Z +SUMMARY:phone with mother +CLASS:PUBLIC +PRIORITY:3 +PERCENT-COMPLETE:0 +END:VTODO + +BEGIN:VEVENT +DTSTAMP:20061125T204432Z +ORGANIZER:MAILTO: +X-KDE-karm-duration:9360 +CREATED:20061118T184102Z +UID:libkcal-733807033.405 +SEQUENCE:0 +LAST-MODIFIED:20061118T184102Z +SUMMARY:phone with mother +CLASS:PUBLIC +PRIORITY:3 +CATEGORIES:KArm +RELATED-TO:libkcal-1177224330.526 +DTSTART:20061118T184054Z +DTEND:20061118T211654Z +TRANSP:OPAQUE +END:VEVENT + +BEGIN:VEVENT +DTSTAMP:20061125T204432Z +ORGANIZER:MAILTO: +X-KDE-karm-duration:5400 +CREATED:20061118T184114Z +UID:libkcal-552065854.752 +SEQUENCE:0 +LAST-MODIFIED:20061118T184114Z +SUMMARY:analysis +CLASS:PUBLIC +PRIORITY:3 +CATEGORIES:KArm +RELATED-TO:libkcal-61950406.193 +DTSTART:20061118T184032Z +DTEND:20061118T201032Z +TRANSP:OPAQUE +END:VEVENT + +BEGIN:VEVENT +DTSTAMP:20061125T204432Z +ORGANIZER:MAILTO: +X-KDE-karm-duration:10920 +CREATED:20061118T184125Z +UID:libkcal-691043213.294 +SEQUENCE:0 +LAST-MODIFIED:20061118T184125Z +SUMMARY:programming +CLASS:PUBLIC +PRIORITY:3 +CATEGORIES:KArm +RELATED-TO:libkcal-1755408865.833 +DTSTART:20061118T184044Z +DTEND:20061118T214244Z +TRANSP:OPAQUE +END:VEVENT + +BEGIN:VEVENT +DTSTAMP:20061125T204432Z +ORGANIZER:MAILTO: +X-KDE-karm-duration:2 +CREATED:20061118T183930Z +UID:libkcal-251324967.864 +SEQUENCE:0 +LAST-MODIFIED:20061118T183930Z +SUMMARY:cr +CLASS:PUBLIC +PRIORITY:3 +CATEGORIES:KArm +RELATED-TO:libkcal-1156600660.458 +DTSTART:20061118T183928Z +DTEND:20061118T183930Z +TRANSP:OPAQUE +END:VEVENT + +BEGIN:VEVENT +DTSTAMP:20061125T204432Z +ORGANIZER:MAILTO: +X-KDE-karm-duration:95 +CREATED:20061118T184306Z +UID:libkcal-729762004.89 +SEQUENCE:0 +LAST-MODIFIED:20061118T184306Z +SUMMARY:programming +CLASS:PUBLIC +PRIORITY:3 +CATEGORIES:KArm +RELATED-TO:libkcal-1755408865.833 +DTSTART:20061118T184130Z +DTEND:20061118T184305Z +TRANSP:OPAQUE +END:VEVENT + +END:VCALENDAR + diff --git a/karm/test/lifetest.php b/karm/test/lifetest.php new file mode 100644 index 000000000..7a24bd3d7 --- /dev/null +++ b/karm/test/lifetest.php @@ -0,0 +1,221 @@ +#!/usr/bin/php +<? + +// Description: +// This program starts karm and simulates keypresses to do a real life-test of karm. +// This program returns zero if all tests went ok, else an error code. +// You need a US or DE keyboard to run this. + +// for those who do not know php: +// for a tutorial about php, check out www.usegroup.de +// for a reference about php, surf to www.php.net + +// TODO +// prepare Windows-port + +function createplannerexample() +{ +$handle=fopen("/tmp/example.planner","w"); +fwrite($handle, +'<?xml version="1.0"?> +<project name="" company="" manager="" phase="" project-start="20041101T000000Z" mrproject-version="2" calendar="1"> + <properties> + <property name="cost" type="cost" owner="resource" label="Cost" description="standard cost for a resource"/> + </properties> + <phases/> + <calendars> + <day-types> + <day-type id="0" name="Working" description="Ein Vorgabe-Arbeitstag"/> + <day-type id="1" name="Nonworking" description="Ein Vorgabetag, an dem nicht gearbeitet wird"/> + <day-type id="2" name="Basis verwenden" description="Use day from base calendar"/> + </day-types> + <calendar id="1" name="Vorgabe"> + <default-week mon="0" tue="0" wed="0" thu="0" fri="0" sat="1" sun="1"/> + <overridden-day-types> + <overridden-day-type id="0"> + <interval start="0800" end="1200"/> + <interval start="1300" end="1700"/> + </overridden-day-type> + </overridden-day-types> + <days/> + </calendar> + </calendars> + <tasks> + <task id="1" name="task 1" note="" work="28800" start="20041101T000000Z" end="20041101T170000Z" percent-complete="0" priority="0" type="normal" scheduling="fixed-work"/> + <task id="2" name="task 2" note="" work="28800" start="20041101T000000Z" end="20041101T170000Z" percent-complete="0" priority="0" type="normal" scheduling="fixed-work"> + <task id="3" name="subtask 1-1" note="" work="28800" start="20041101T000000Z" end="20041101T170000Z" percent-complete="0" priority="0" type="normal" scheduling="fixed-work"/> + <task id="4" name="subtask 1-2" note="" work="28800" start="20041101T000000Z" end="20041101T170000Z" percent-complete="0" priority="0" type="normal" scheduling="fixed-work"/> + </task> + </tasks> + <resource-groups/> + <resources/> + <allocations/> +</project> +'); +fclose($handle); +}; + +function simkey($s) +// This function simulates keypresses that form the string $s, e.g. for $s==hallo, it simulates the keypress of h, then a, then l and so on. +// find a useful list of keycodes under /usr/include/X11/keysymdef.h +{ + for ($i=0; $i<strlen($s); $i++) + { + usleep(10000); # this is just for the user to see what happens + if ($s[$i]=="/") system("xte 'key KP_Divide'"); + else system("xte 'key ".$s[$i]."'"); + usleep(10000); + } +} + +function keysim($s) +// remove everything that makes you have to think twice!! +{ + simkey($s); +} + +function funkeysim($s, $count=1) +// same as keysim, but interprets $s as function key name to be used by xte and expects a $count to indicate how often key is to be pressed +{ + for ($i=1; $i<=$count; $i++) + { + usleep(10000); + $rc=exec("xte 'key $s'"); + usleep(10000); + } + return $rc; +} + +// int main() +if ($argv[1]!="--batch") +{ + echo "\nThis is lifetest.php, a program to test karm by starting it and simulating keypresses.\n"; + echo "It is intended for developers who do changes to karm's sourcecode.\n"; + echo "Before publishing these changes, they should\n"; + echo "(a) resolve all conflicts with the latest karm sourcecode (cvs up)\n"; + echo "(b) make sure the code still builds (make)\n"; + echo "(c) run automated test routines like this (make check)\n\n"; + + echo "This program simulates keypresses, so please leave the keyboard alone during the test. Please use a us or de keyboardlayout (setxkbmap us). This must be run in X environment.\n + You must have XAutomation installed to run this."; + system("xte -h 2&>/dev/null",$rc); + if ($rc==0) echo " You have.\n"; + if ($rc==127) echo " You do not have, please get it from http://hoopajoo.net/projects/xautomation.html .\n"; + echo "This program will test karm by issueing karm, so, make sure, this calls the version you want to test (make install).\n\n"; + + echo "This program will now stop unless you give the parameter --batch (confirming that you do not touch the keyboard)\n"; + + $err=""; + $exit=0; +} +else +{ + if ( system( "which xte 2> /dev/null" ) == "" ) { + echo "xte not found\n"; + exit(0); + } + switch (funkeysim("Alt_L")) + { + case 1: + $err.="this must be run in an X environment\n"; + break; + case 127: + $err.="you do not have XAutomation installed, get it from http://hoopajoo.net/projects/xautomation.html\n"; + break; + } + // the following is the same as 'if file_exist(...) unlink(...)', but atomic + @unlink ("/tmp/karmtest.ics"); + @unlink ("/tmp/example.planner"); + if ($err=="") + { + // start and wait till mainwindow is up + // the mouse can be in the way, so, move it out. This here even works with "focus strictly under mouse". + system("xte 'mousemove 1 1'"); + echo "\nStarting karm"; + $process=popen("karm --geometry 200x100+0+0 /tmp/karmtest.ics >/dev/null 2>&1", 'w'); + $rc=1; + while ($rc==1) system("dcop `dcop 2>/dev/null | grep karm` KarmDCOPIface version",$rc); + echo "mainwindow is ready"; + sleep (1); + + funkeysim("Alt_L"); + + funkeysim("Right",3); + funkeysim("Down",2); + funkeysim("Return"); + sleep (1); + funkeysim("Down",2); + funkeysim("Tab",5); + simkey("/tmp/karmtest.ics"); + sleep (1); + funkeysim("Return"); + sleep (1); + funkeysim("Return"); + sleep (1); + + # add a new task + funkeysim("Alt_L"); + funkeysim("Right",2); + funkeysim("Down"); + sleep (1); + funkeysim("Return"); + sleep (1); + simkey("example 1"); + funkeysim("Return"); + sleep (1); + + echo "\nCreating a planner project file..."; + createplannerexample(); + + # import planner project file + funkeysim("Alt_L"); + funkeysim("Down",5); + funkeysim("Right"); + funkeysim("Down"); + funkeysim("Return"); + sleep (2); + keysim("/tmp/example.planner"); + sleep (1); + funkeysim("Return"); + sleep (1); + + # export to CSV file + funkeysim("Alt_L"); + funkeysim("Down",5); + funkeysim("Right"); + funkeysim("Down",2); + funkeysim("Return"); + sleep(2); + keysim("/tmp/exporttest.csv"); + sleep(1); + funkeysim("Tab",6); + system ("xte 'keydown Alt_L'"); + system ("xte 'key m'"); + system ("xte 'keyup Alt_L'"); + sleep(1); + funkeysim("Return"); + + # send CTRL_Q + sleep (2); + echo "\nsending CTRL_Q...\n"; + system ("xte 'keydown Control_L'"); + system ("xte 'key Q'"); + system ("xte 'keyup Control_L'"); + + $content=file_get_contents("/tmp/karmtest.ics"); + $lines=explode("\n",$content); + if (!preg_match("/DTSTAMP:[0-9]{1,8}T[0-9]{1,6}Z/", $lines[4])) $err.="iCal file: wrong dtstamp"; + if ($lines[12]<>"SUMMARY:example 1") $err.="iCal file: wrong task, should be example 1, but is $lines[12]"; + if ($lines[16]<>"END:VTODO") $err.="iCal file: wrong end of vtodo"; + $content=file_get_contents("/tmp/exporttest.csv"); + $lines=explode("\n",$content); + if (!preg_match("/\"example 1\",,0[,|.]00,0[,|.]00,0[,|.]00,0[,|.]00/", $lines[0])) $err.="csv export is wrong"; + pclose($process); + if ($err == "") @unlink ("/tmp/karmtest.ics"); + @unlink ("/tmp/example.planner"); + if ($err == "") @unlink ("/tmp/exporttest.csv"); + } +} + echo $err; + if ($err!="") exit(1); +?>
\ No newline at end of file diff --git a/karm/test/lockerthread.cpp b/karm/test/lockerthread.cpp new file mode 100644 index 000000000..6ec6e7ba2 --- /dev/null +++ b/karm/test/lockerthread.cpp @@ -0,0 +1,44 @@ +#include <qthread.h> +#include <qstring.h> + +#include <resourcecalendar.h> +#include <resourcelocal.h> +#include <calendarresources.h> + +#include "lockerthread.h" + +LockerThread::LockerThread( const QString &icsfile ) +{ + m_gotlock = false; + m_icsfile = icsfile; +} + +/* +void LockerThread::setIcsFile( const QString &filename ) +{ + m_icsfile = filename; +} +*/ + +void LockerThread::run() +{ + KCal::CalendarResources *calendars = 0; + KCal::ResourceCalendar *calendar = 0; + KCal::CalendarResources::Ticket *lock = 0; + + calendars = new KCal::CalendarResources( QString::fromLatin1( "UTC" ) ); + calendar = new KCal::ResourceLocal( m_icsfile ); + lock = calendars->requestSaveTicket( calendar ); + if ( lock ) + { + m_gotlock = true; + calendars->releaseSaveTicket( lock ); + } + else + { + m_gotlock = false; + } + + delete calendar; + delete calendars; +} diff --git a/karm/test/lockerthread.h b/karm/test/lockerthread.h new file mode 100644 index 000000000..d40f234ad --- /dev/null +++ b/karm/test/lockerthread.h @@ -0,0 +1,21 @@ +#include <qthread.h> + +class QString; + +/** + * Attempt to open and lock a calendar resource in a seperate thread. + * + * Result of attempt returned by gotlock(). + */ +class LockerThread : public QThread +{ + public: + LockerThread( const QString &filename ); + //void setIcsFile( const QString &filename ); + void run(); + bool gotlock() const { return m_gotlock; }; + + private: + QString m_icsfile; + bool m_gotlock; +}; diff --git a/karm/test/locking.cpp b/karm/test/locking.cpp new file mode 100644 index 000000000..49c7637d3 --- /dev/null +++ b/karm/test/locking.cpp @@ -0,0 +1,151 @@ +#include <assert.h> + +#include <qstring.h> +#include <qfile.h> +#include <qdir.h> +#include <kcmdlineargs.h> +#include <kapplication.h> + +#include <resourcecalendar.h> +#include <resourcelocal.h> +#include <calendarresources.h> + +#include "lockerthread.h" + +const QString icalfilename = "karmtest.ics"; + +// If one thread has the file is locked, the other cannot get the lock. +short test1() +{ + short rval = 0; + + KCal::CalendarResources *calendars = 0; + KCal::ResourceCalendar *calendar = 0; + KCal::CalendarResources::Ticket *lock = 0; + + calendars = new KCal::CalendarResources( QString::fromLatin1( "UTC" ) ); + calendar = new KCal::ResourceLocal( icalfilename ); + lock = calendars->requestSaveTicket( calendar ); + + if ( !lock ) + { + kdDebug( 5970 ) << "test1(): failed to lock " << icalfilename << endl; + rval = 1; + } + + if ( !rval ) + { + LockerThread thread( icalfilename ); + thread.run(); + if ( thread.gotlock() ) + { + kdDebug( 5970 ) << "test1(): second thread was able to lock " << icalfilename << endl; + rval = 1; + } + } + + // This frees the lock memory. + calendars->releaseSaveTicket( lock ); + + delete calendar; + delete calendars; + + return rval; +} + +// First thread opens but doesn't lock, second should get lock. +short test2() +{ + short rval = 0; + + KCal::CalendarResources *calendars = 0; + KCal::ResourceCalendar *calendar = 0; + KCal::CalendarResources::Ticket *lock = 0; + + calendars = new KCal::CalendarResources( QString::fromLatin1( "UTC" ) ); + calendar = new KCal::ResourceLocal( icalfilename ); + + LockerThread thread( icalfilename ); + thread.run(); + if ( !thread.gotlock() ) + { + kdDebug(5970) << "test2(): second thread was not able to lock " << icalfilename << endl; + rval = 1; + } + + delete calendar; + delete calendars; + + return rval; +} + +// First thread locks, then unlocks--second should get lock. +short test3() +{ + short rval = 0; + + KCal::CalendarResources *calendars = 0; + KCal::ResourceCalendar *calendar = 0; + KCal::CalendarResources::Ticket *lock = 0; + + calendars = new KCal::CalendarResources( QString::fromLatin1( "UTC" ) ); + calendar = new KCal::ResourceLocal( icalfilename ); + + // lock then unlock + lock = calendars->requestSaveTicket( calendar ); + if ( !lock ) + { + kdDebug( 5970 ) << "test1(): failed to lock " << icalfilename << endl; + rval = 1; + } + calendars->releaseSaveTicket( lock ); + + // second thread should get lock + if ( !rval ) + { + LockerThread thread( icalfilename ); + thread.run(); + if ( !thread.gotlock() ) + { + kdDebug( 5970 ) << "test1(): second thread was not able to lock " << icalfilename << endl; + rval = 1; + } + } + + + delete calendar; + delete calendars; + + return rval; +} + +// TODO: +// If one thread changes the file, the other is notified. +// What happens if we lock one incident and try to change another? + +int main( int argc, char *argv[] ) +{ + short rval = 0; + + // Use another directory than the real one, just to keep things clean + // KDEHOME needs to be writable though, for a ksycoca database + // FIXME: Delete this directory when done with test. + setenv( "KDEHOME", QFile::encodeName( QDir::homeDirPath() + "/.kde-testresource" ), true ); + + // Copied from Till's test in libkcal. Not sure what this is for. + setenv( "KDE_FORK_SLAVES", "yes", true ); // simpler, for the final cleanup + + // Copied from Till's test in libkcal. Not sure what this is for. + KApplication::disableAutoDcopRegistration(); + + KCmdLineArgs::init(argc,argv,"testresourcelocking", 0, 0, 0, 0); + + KApplication app( false, false ); + + // basic libkcal locking stuff + if ( !rval ) rval = test1(); + if ( !rval ) rval = test2(); + if ( !rval ) rval = test3(); + + return rval; +} diff --git a/karm/test/refresh_on_change.sh b/karm/test/refresh_on_change.sh new file mode 100755 index 000000000..6253c63b6 --- /dev/null +++ b/karm/test/refresh_on_change.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +# I cannot get this test to work reliably! +# I suspect the culprit is FAM. +# -- Mark + +exec >>check.log 2>&1 + +source __lib.sh + +TESTFILE="/tmp/testkarm.ics" + +set_up + +TODO_NAME=$0 +TODO_UID=abc-123 +TODO_TIME=`date +%Y%m%dT%H%M%SZ` + +sleep 1 # make sure kdirstat can recognize the change in last change date + +cat >> $TESTFILE << endl +BEGIN:VCALENDAR +PRODID:-//K Desktop Environment//NONSGML KArm Test Scripts//EN +VERSION:2.0 + +BEGIN:VTODO +DTSTAMP:$TODO_TIME +ORGANIZER;CN=Anonymous:MAILTO:nobody@nowhere +CREATED:$TODO_TIME +UID:$TODO_UID +SEQUENCE:0 +LAST-MODIFIED:$TODO_TIME +SUMMARY:$TODO_NAME +CLASS:PUBLIC +PRIORITY:5 +PERCENT-COMPLETE:0 +END:VTODO + +END:VCALENDAR +endl + +# wait so FAM and KDirWatcher tell karm and karm refreshes view +sleep 2 + +RVAL=`dcop $DCOPID KarmDCOPIface taskIdFromName $TODO_NAME` +#echo "RVAL = $RVAL" + +tear_down + +# check that todo was found +if [ "$RVAL" == "$TODO_UID" ]; then + echo "PASS $0" + exit 0; +else + echo "FAIL $0: got /$RVAL/, expected /$TODO_UID/" + exit 1; +fi diff --git a/karm/test/remote_storage.sh b/karm/test/remote_storage.sh new file mode 100755 index 000000000..a4335b6c1 --- /dev/null +++ b/karm/test/remote_storage.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +# Karm can read and write to an FTP server for storage file. + +exec >>check.log 2>&1 + +PORT=8000 +TESTFILE="http://localhost:$PORT/testkarm.ics" +TESTFILE_LOCAL="testkarm.ics" +TESTTODO="testtodo" + +# Start with clean environment +# If runscripts sees output on stderr, it thinks script failed. +DCOPID=`dcop | grep karm 2>/dev/null` +if [ -n $DCOPID ]; then dcop $DCOPID KarmDCOPIface quit; fi; +if [ -e $TESTFILE_LOCAL ]; then rm $TESTFILE_LOCAL; fi + +# if the file does not exist, kresources pops up a modal dialog box +# telling us that a file does not exist. + +touch $TESTFILE_LOCAL + +python __httpd.py $PORT & + +sleep 2 + +HTTPD_PID=`ps -C "python __httpd.py" -o pid=` + +karm $TESTFILE & + +sleep 2 + +DCOPID=`dcop | grep karm` + +# karm does not write file until data is saved. + +echo "DEBUG: dcop $DCOPID KarmDCOPIface addtodo \"$TESTTODO\"" +dcop $DCOPID KarmDCOPIface addtodo "$TESTTODO" + +sleep 1 + +RVAL=1 +if [ -e $TESTFILE_LOCAL ]; then RVAL=0; fi + +# clean up +if [ -n $DCOPID ]; then dcop $DCOPID KarmDCOPIface quit; fi; +if [ -e $TESTFILE_LOCAL ]; then rm $TESTFILE_LOCAL; fi +if [ -n $HTTPD_PID ]; then kill $HTTPD_PID; fi + +# return 0 on success, 1 on failure +if [ $RVAL -eq 0 ] +then + echo "PASS $0" + exit 0 +else + echo "FAIL $0" + exit 1 +fi diff --git a/karm/test/runscripts.cpp b/karm/test/runscripts.cpp new file mode 100644 index 000000000..aa5449541 --- /dev/null +++ b/karm/test/runscripts.cpp @@ -0,0 +1,130 @@ +/* This file is part of the KDE project + Copyright (C) 2004 Mark Bucciarelli <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include <kdebug.h> +#include <qdir.h> +#include <qfile.h> +#include <qstring.h> +#include <qstringlist.h> +#include <qtextstream.h> + +#include "script.h" + +static QString srcdir(); +static int runscripts +( const QString &interpreter, const QString &extension, const QString &path ); + +const QString dots = ".................................................."; +const QString not_a_test_filename_prefix = "__"; + +// Read srcdir from Makefile (for builddir != srcdir). +QString srcdir() +{ + bool found = false; + QString dir; + + QFile file( "Makefile" ); + if ( !file.open( IO_ReadOnly | IO_Translate ) ) return ""; + + QTextStream in( &file ); + QString line; + while ( !found && !in.atEnd() ) + { + line = in.readLine(); + if ( line.startsWith( "srcdir = " ) ) + { + dir = line.mid( 9 ); + found = true; + } + } + + if ( !found ) dir = ""; + + return dir; +} + +int runscripts +( const QString &interpreter, const QString &extension, const QString &path ) +{ + int rval = 0; + int oneBadApple = 0; + QStringList files; + + QDir dir( path ); + + Script* s = new Script( dir ); + + dir.setNameFilter( extension ); + dir.setFilter( QDir::Files ); + dir.setSorting( QDir::Name | QDir::IgnoreCase ); + const QFileInfoList *list = dir.entryInfoList(); + QFileInfoListIterator it( *list ); + QFileInfo *fi; + while ( !rval && ( fi = it.current() ) != 0 ) + { + // Don't run scripts that are shared routines. + if ( ! fi->fileName().startsWith( not_a_test_filename_prefix ) ) + { + s->addArgument( interpreter ); + s->addArgument( path + QDir::separator() + fi->fileName().latin1() ); + + // Thorsten's xautomation tests run with user interaction by default. + if ( interpreter == "sh" ) s->addArgument( "--batch" ); + if ( interpreter == "php" ) s->addArgument( "--batch" ); + + rval = s->run(); + + kdDebug() << "runscripts: " << fi->fileName() + << " " << dots.left( dots.length() - fi->fileName().length() ) + << " " << ( ! rval ? "PASS" : "FAIL" ) << endl; + + // Don't abort if one test files--run them all + if ( rval ) + { + oneBadApple = 1; + rval = 0; + } + + delete s; + s = new Script( dir ); + } + ++it; + } + delete s; + s = 0; + + return oneBadApple; +} + +int main( int, char** ) +{ + int rval = 0; + + QString path = srcdir(); + + if ( !rval ) rval = runscripts( "python", "*.py *.Py *.PY *.pY", path ); + + if ( !rval ) rval = runscripts( "sh", "*.sh *.Sh *.SH *.sH", path ); + + if ( !rval ) rval = runscripts( "perl", "*.pl *.Pl *.PL *.pL", path ); + + if ( !rval ) rval = runscripts( "php", "*.php *.php3 *.php4 *.php5", path ); + + return rval; +} diff --git a/karm/test/script.cpp b/karm/test/script.cpp new file mode 100644 index 000000000..37fe058d3 --- /dev/null +++ b/karm/test/script.cpp @@ -0,0 +1,115 @@ +/* This file is part of the KDE project + Copyright (C) 2002 Anders Lund <[email protected]> + Copyright (C) 2004 Mark Bucciarelli <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +#include <qdir.h> +#include <qprocess.h> +#include <qstring.h> +#include <qstringlist.h> +#include <qtimer.h> + +#include <kdebug.h> + +#include "script.h" + +/* + n.b. Do not use kdDebug statements in this file. + + With qt-copy 3_3_BRANCH, they cause a Valgrind error. + Ref: KDE bug #95237. +*/ + +// Wait for terminate() attempt to return before using kill() +// kill() doesn't let script interpreter try to clean up. +const int NICE_KILL_TIMEOUT_IN_SECS = 5; + +Script::Script( const QDir& workingDirectory ) +{ + m_status = 0; + m_stderr = false; + m_timeoutInSeconds = 5; + + m_proc = new QProcess( this ); + m_proc->setWorkingDirectory( workingDirectory ); + + connect ( m_proc, SIGNAL( readyReadStdout() ), + this , SLOT ( stdout() ) + ); + connect ( m_proc, SIGNAL( readyReadStderr() ), + this , SLOT ( stderr() ) + ); + connect ( m_proc, SIGNAL( processExited() ), + this , SLOT ( exit() ) + ); +} + +Script::~Script() +{ + delete m_proc; + m_proc = 0; +} + +void Script::addArgument( const QString &arg ) +{ + m_proc->addArgument( arg ); +} + +void Script::setTimeout( int seconds ) +{ + if ( seconds <= 0 ) return; + m_timeoutInSeconds = seconds; +} + +int Script::run() +{ + m_proc->start(); + // This didn't work. But Ctrl-C does. :P + //QTimer::singleShot( m_timeoutInSeconds * 1000, m_proc, SLOT( kill() ) ); + //while ( ! m_proc->normalExit() ); + while ( m_proc->isRunning() ); + return m_status; +} + +void Script::terminate() +{ + // These both trigger processExited, so exit() will run. + m_proc->tryTerminate(); + QTimer::singleShot( NICE_KILL_TIMEOUT_IN_SECS*1000, m_proc, SLOT( kill() ) ); +} + +void Script::exit() +{ + m_status = m_proc->exitStatus(); + delete m_proc; + m_proc = 0; +} + +void Script::stderr() +{ + // Treat any output to std err as a script failure + m_status = 1; + QString data = QString( m_proc->readStderr() ); + m_stderr= true; +} + +void Script::stdout() +{ + QString data = QString( m_proc->readStdout() ); +} + +#include "script.moc" diff --git a/karm/test/script.h b/karm/test/script.h new file mode 100644 index 000000000..f5ae1e227 --- /dev/null +++ b/karm/test/script.h @@ -0,0 +1,52 @@ +/* This file is part of the KDE project + Copyright (C) 2004 Mark Bucciarelli <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef _script_h_ +#define _script_h_ + +//#include <qvariant.h> +#include <qobject.h> + +class QDir; +class QProcess; +class QString; +class QStringList; + +class Script : public QObject +{ + Q_OBJECT +public: + Script( const QDir& workingDirectory ); + virtual ~Script(); + void addArgument( const QString &arg ); + void setTimeout( int seconds ); + int run(); +private slots: + void exit(); + void stderr(); + void stdout(); + void terminate(); +private: + QProcess *m_proc; + int m_status; + bool m_stderr; + int m_timeoutInSeconds; +}; + +#endif // _script_h_ diff --git a/karm/test/version.sh b/karm/test/version.sh new file mode 100755 index 000000000..64fbc8fb0 --- /dev/null +++ b/karm/test/version.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# First test, just check version. + +exec >>check.log 2>&1 + +TESTFILE="/tmp/testkarm1.ics" +VERSION="1.6.0" + +source __lib.sh + +set_up + +RVAL=`dcop $DCOPID KarmDCOPIface version 2>/dev/null` + +tear_down + +if [ "$RVAL" == "$VERSION" ]; then + echo "PASS $0" + exit 0; +else + echo "FAIL $0: got /$RVAL/, expected /$VERSION/" + exit 1; +fi diff --git a/karm/test/webdav.sh b/karm/test/webdav.sh new file mode 100755 index 000000000..8d51db92c --- /dev/null +++ b/karm/test/webdav.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +# Add a todo to an iCal file stored on a webdav server. + +exec >>check.log 2>&1 + +source __lib.sh + +# check for required perl stuff +perl -e "use Net::DAV::Server;" > /dev/null 2>&1 +if ! [ $? = 0 ]; then + echo "PASS" + exit 0 +fi + +# Start webdav server +perl __webdav.pl & +sleep 2 +WEBDAV_PID=`ps -C "perl __webdav.pl" -o pid=` + +# Start karm +TESTFILE="http://localhost:4242/testkarm.ics" +TESTFILE_LOCAL="/tmp/testkarm.ics" +TESTTODO="testtodo" +SKIP_TESTFILE_DELETE=true +# Need this or karm complains there is no file +rm -f $TESTFILE_LOCAL +touch $TESTFILE_LOCAL +set_up +#wait till download is ready +sleep 3 + +# add a todo +dcop $DCOPID KarmDCOPIface addTask "$TESTTODO" +sleep 1 +dcop $DCOPID KarmDCOPIface save + +sleep 1 + +if grep $TESTTODO $TESTFILE_LOCAL + then RVAL=0 + else RVAL=1 +fi + +# clean up +tear_down +#if [ -e $TESTFILE_LOCAL ]; then rm $TESTFILE_LOCAL; fi +if [ -n $WEBDAV_PID ]; then kill $WEBDAV_PID; fi + +# return 0 on success, 1 on failure +if [ $RVAL -eq 0 ] +then + echo "PASS $0" + exit 0 +else + echo "FAIL $0" + exit 1 +fi diff --git a/karm/timekard.cpp b/karm/timekard.cpp new file mode 100644 index 000000000..b7ff21f0e --- /dev/null +++ b/karm/timekard.cpp @@ -0,0 +1,382 @@ +/* + * This file only: + * Copyright (C) 2003 Mark Bucciarelli <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ + +// #include <iostream> + +#include <qdatetime.h> +#include <qpaintdevicemetrics.h> +#include <qpainter.h> +#include <qmap.h> + +#include <kglobal.h> +#include <kdebug.h> +#include <klocale.h> // i18n +#include <event.h> + +#include "karmutility.h" // formatTime() +#include "timekard.h" +#include "task.h" +#include "taskview.h" +#include <assert.h> + +const int taskWidth = 40; +const int timeWidth = 6; +const int totalTimeWidth = 7; +const int reportWidth = taskWidth + timeWidth; + +const QString cr = QString::fromLatin1("\n"); + +QString TimeKard::totalsAsText(TaskView* taskview, bool justThisTask, WhichTime which) +// Print the total Times as text. If justThisTask, use activeTask, else, all tasks +{ + kdDebug(5970) << "Entering TimeKard::totalsAsText" << endl; + QString retval; + QString line; + QString buf; + long sum; + + line.fill('-', reportWidth); + line += cr; + + // header + retval += i18n("Task Totals") + cr; + retval += KGlobal::locale()->formatDateTime(QDateTime::currentDateTime()); + retval += cr + cr; + retval += QString(QString::fromLatin1("%1 %2")) + .arg(i18n("Time"), timeWidth) + .arg(i18n("Task")); + retval += cr; + retval += line; + + // tasks + if (taskview->current_item()) + { + if (justThisTask) + { + // a task's total time includes the sum of all subtask times + sum = which == TotalTime ? taskview->current_item()->totalTime() : taskview->current_item()->sessionTime(); + printTask(taskview->current_item(), retval, 0, which); + } + else + { + sum = 0; + for (Task* task= taskview->item_at_index(0); task; + task= task->nextSibling()) + { + kdDebug(5970) << "Copying task " << task->name() << endl; + int time = which == TotalTime ? task->totalTime() : task->totalSessionTime(); + sum += time; + if ( time || task->firstChild() ) + printTask(task, retval, 0, which); + } + } + + // total + buf.fill('-', reportWidth); + retval += QString(QString::fromLatin1("%1")).arg(buf, timeWidth) + cr; + retval += QString(QString::fromLatin1("%1 %2")) + .arg(formatTime(sum),timeWidth) + .arg(i18n("Total")); + } + else + retval += i18n("No tasks."); + + return retval; +} + +// Print out "<indent for level> <task total> <task>", for task and subtasks. Used by totalsAsText. +void TimeKard::printTask(Task *task, QString &s, int level, WhichTime which) +{ + QString buf; + + s += buf.fill(' ', level); + s += QString(QString::fromLatin1("%1 %2")) + .arg(formatTime(which == TotalTime?task->totalTime():task->totalSessionTime()), timeWidth) + .arg(task->name()); + s += cr; + + for (Task* subTask = task->firstChild(); + subTask; + subTask = subTask->nextSibling()) + { + int time = which == TotalTime ? subTask->totalTime() : subTask->totalSessionTime(); + if (time) + printTask(subTask, s, level+1, which); + } +} + +void TimeKard::printTaskHistory(const Task *task, + const QMap<QString,long>& taskdaytotals, + QMap<QString,long>& daytotals, + const QDate& from, + const QDate& to, + const int level, QString& s, bool totalsOnly) +{ + long sectionsum = 0; + for ( QDate day = from; day <= to; day = day.addDays(1) ) + { + QString daykey = day.toString(QString::fromLatin1("yyyyMMdd")); + QString daytaskkey = QString::fromLatin1("%1_%2") + .arg(daykey) + .arg(task->uid()); + + if (taskdaytotals.contains(daytaskkey)) + { + if ( !totalsOnly ) + { + s += QString::fromLatin1("%1") + .arg(formatTime(taskdaytotals[daytaskkey]/60), timeWidth); + } + sectionsum += taskdaytotals[daytaskkey]; // in seconds + + if (daytotals.contains(daykey)) + daytotals.replace(daykey, daytotals[daykey] + taskdaytotals[daytaskkey]); + else + daytotals.insert(daykey, taskdaytotals[daytaskkey]); + } + else if ( !totalsOnly ) + { + QString buf; + buf.fill(' ', timeWidth); + s += buf; + } + } + + // Total for task this section (e.g. week) + s += QString::fromLatin1("%1").arg(formatTime(sectionsum/60), totalTimeWidth); + + // Task name + QString buf; + s += buf.fill(' ', level + 1); + s += QString::fromLatin1("%1").arg(task->name()); + s += cr; + + for (Task* subTask = task->firstChild(); + subTask; + subTask = subTask->nextSibling()) + { + // recursive + printTaskHistory(subTask, taskdaytotals, daytotals, from, to, level+1, s, totalsOnly); + } +} + +QString TimeKard::sectionHistoryAsText( + TaskView* taskview, + const QDate& sectionFrom, const QDate& sectionTo, + const QDate& from, const QDate& to, + const QString& name, + bool justThisTask, bool totalsOnly) +{ + + const int sectionReportWidth = taskWidth + ( totalsOnly ? 0 : sectionFrom.daysTo(sectionTo) * timeWidth ) + totalTimeWidth; + assert( sectionReportWidth > 0 ); + QString line; + line.fill('-', sectionReportWidth); + line += cr; + + QValueList<HistoryEvent> events; + if ( sectionFrom < from && sectionTo > to) + { + events = taskview->getHistory(from, to); + } + else if ( sectionFrom < from ) + { + events = taskview->getHistory(from, sectionTo); + } + else if ( sectionTo > to) + { + events = taskview->getHistory(sectionFrom, to); + } + else + { + events = taskview->getHistory(sectionFrom, sectionTo); + } + + QMap<QString, long> taskdaytotals; + QMap<QString, long> daytotals; + + // Build lookup dictionary used to output data in table cells. keys are + // in this format: YYYYMMDD_NNNNNN, where Y = year, M = month, d = day and + // NNNNN = the VTODO uid. The value is the total seconds logged against + // that task on that day. Note the UID is the todo id, not the event id, + // so times are accumulated for each task. + for (QValueList<HistoryEvent>::iterator event = events.begin(); event != events.end(); ++event) + { + QString daykey = (*event).start().date().toString(QString::fromLatin1("yyyyMMdd")); + QString daytaskkey = QString::fromLatin1("%1_%2") + .arg(daykey) + .arg((*event).todoUid()); + + if (taskdaytotals.contains(daytaskkey)) + taskdaytotals.replace(daytaskkey, + taskdaytotals[daytaskkey] + (*event).duration()); + else + taskdaytotals.insert(daytaskkey, (*event).duration()); + } + + QString retval; + // section name (e.g. week name) + retval += cr + cr; + QString buf; + if ( name.length() < (unsigned int)sectionReportWidth ) + buf.fill(' ', int((sectionReportWidth - name.length()) / 2)); + retval += buf + name + cr; + + if ( !totalsOnly ) + { + // day headings + for (QDate day = sectionFrom; day <= sectionTo; day = day.addDays(1)) + { + retval += QString::fromLatin1("%1").arg(day.day(), timeWidth); + } + retval += cr; + retval += line; + } + + // the tasks + if (events.empty()) + { + retval += " "; + retval += i18n("No hours logged."); + } + else + { + if (justThisTask) + { + printTaskHistory(taskview->current_item(), taskdaytotals, daytotals, + sectionFrom, sectionTo, 0, retval, totalsOnly); + } + else + { + for (Task* task= taskview->current_item(); task; + task= task->nextSibling()) + { + printTaskHistory(task, taskdaytotals, daytotals, + sectionFrom, sectionTo, 0, retval, totalsOnly); + } + } + retval += line; + + // per-day totals at the bottom of the section + long sum = 0; + for (QDate day = sectionFrom; day <= sectionTo; day = day.addDays(1)) + { + QString daykey = day.toString(QString::fromLatin1("yyyyMMdd")); + + if (daytotals.contains(daykey)) + { + if ( !totalsOnly ) + { + retval += QString::fromLatin1("%1") + .arg(formatTime(daytotals[daykey]/60), timeWidth); + } + sum += daytotals[daykey]; // in seconds + } + else if ( !totalsOnly ) + { + buf.fill(' ', timeWidth); + retval += buf; + } + } + + retval += QString::fromLatin1("%1 %2") + .arg(formatTime(sum/60), totalTimeWidth) + .arg(i18n("Total")); + } + return retval; +} + +QString TimeKard::historyAsText(TaskView* taskview, const QDate& from, + const QDate& to, bool justThisTask, bool perWeek, bool totalsOnly) +{ + // header + QString retval; + retval += totalsOnly ? i18n("Task Totals") : i18n("Task History"); + retval += cr; + retval += i18n("From %1 to %2") + .arg(KGlobal::locale()->formatDate(from)) + .arg(KGlobal::locale()->formatDate(to)); + retval += cr; + retval += i18n("Printed on: %1") + .arg(KGlobal::locale()->formatDateTime(QDateTime::currentDateTime())); + + if ( perWeek ) + { + // output one time card table for each week in the date range + QValueList<Week> weeks = Week::weeksFromDateRange(from, to); + for (QValueList<Week>::iterator week = weeks.begin(); week != weeks.end(); ++week) + { + retval += sectionHistoryAsText( taskview, (*week).start(), (*week).end(), from, to, (*week).name(), justThisTask, totalsOnly ); + } + } else + { + retval += sectionHistoryAsText( taskview, from, to, from, to, "", justThisTask, totalsOnly ); + } + return retval; +} + +Week::Week() {} + +Week::Week(QDate from) +{ + _start = from; +} + +QDate Week::start() const +{ + return _start; +} + +QDate Week::end() const +{ + return _start.addDays(6); +} + +QString Week::name() const +{ + return i18n("Week of %1").arg(KGlobal::locale()->formatDate(start())); +} + +QValueList<Week> Week::weeksFromDateRange(const QDate& from, const QDate& to) +{ + QDate start; + QValueList<Week> weeks; + + // The QDate weekNumber() method always puts monday as the first day of the + // week. + // + // Not that it matters here, but week 1 always includes the first Thursday + // of the year. For example, January 1, 2000 was a Saturday, so + // QDate(2000,1,1).weekNumber() returns 52. + + // Since report always shows a full week, we generate a full week of dates, + // even if from and to are the same date. The week starts on the day + // that is set in the locale settings. + start = from.addDays( + -((7 - KGlobal::locale()->weekStartDay() + from.dayOfWeek()) % 7)); + + for (QDate d = start; d <= to; d = d.addDays(7)) + weeks.append(Week(d)); + + return weeks; +} + diff --git a/karm/timekard.h b/karm/timekard.h new file mode 100644 index 000000000..f47129fda --- /dev/null +++ b/karm/timekard.h @@ -0,0 +1,129 @@ +/* + * This file only: + * Copyright (C) 2003 Mark Bucciarelli <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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. + * + */ + +#ifndef KARM_TIMEKARD_H +#define KARM_TIMEKARD_H + +#undef Color // X11 headers +#undef GrayScale // X11 headers +#include <kprinter.h> +//#include <qdate.h> + +#include "karmstorage.h" + +class QString; +class QDate; + +class TaskView; + + +/** + * Seven consecutive days. + * + * The timecard report prints out one table for each week of data. The first + * day of the week should be read from the KControlPanel. Currently, it is + * hardcoded to Sunday. + */ +class Week +{ + public: + /** Need an empty constructor to use in a QValueList. */ + Week(); + Week(QDate from); + QDate start() const; + QDate end() const; + QValueList<QDate> days() const; + + /** + * Returns a list of weeks for the given date range. + * + * The first day of the week is picked up from the settings in the + * KontrolPanel. + * + * The list is inclusive; for example, if you pass in a date range of two + * days, one being a Sunday and the other being a Monday, you will get two + * weeks back in the list. + */ + static QValueList<Week> weeksFromDateRange(const QDate& from, + const QDate& to); + + /** + * Return the name of the week. + * + * Uses whatever the user has set up for the long date format in + * KControlPanel, prefixed by "Week of". + */ + QString name() const; + + + private: + QDate _start; +}; + +/** + * Routines to output timecard data. + */ +class TimeKard +{ + public: + TimeKard() {}; + + enum WhichTime { TotalTime, SessionTime }; + + /** + * Generates ascii text of task totals, for current task on down. + * + * Formatted for pasting into clipboard. + * + * @param taskview The view whose tasks need to be formatted. + * + * @param justThisTask Only useful when user has picked a root task. We + * use this parameter to distinguish between when a user just wants to + * print the task subtree for a root task and when they want to print + * all tasks. + */ + QString totalsAsText(TaskView* taskview, bool justThisTask, WhichTime which); + + /** + * Generates ascii text of weekly task history, for current task on down. + * + * Formatted for pasting into clipboard. + */ + QString historyAsText(TaskView* taskview, const QDate& from, + const QDate& to, bool justThisTask, bool perWeek, bool totalsOnly); + +private: + void printTask(Task *t, QString &s, int level, WhichTime which); + + void printTaskHistory(const Task *t, const QMap<QString, long>& datamap, + QMap<QString, long>& daytotals, + const QDate& from, const QDate& to, + const int level, QString& retval, bool totalsOnly); + + QString sectionHistoryAsText(TaskView* taskview, + const QDate& sectionFrom, const QDate& sectionTo, + const QDate& from, const QDate& to, + const QString& name, + bool justThisTask, bool totalsOnly); + + }; +#endif // KARM_TIMEKARD_H diff --git a/karm/toolicons.h b/karm/toolicons.h new file mode 100644 index 000000000..cf64a18dd --- /dev/null +++ b/karm/toolicons.h @@ -0,0 +1,164 @@ +/* Generated by qembed */ +static const unsigned int clock_xpm_len = 765; +static const unsigned char clock_xpm_data[] = { + 0x2f,0x2a,0x20,0x58,0x50,0x4d,0x20,0x2a,0x2f,0x0a,0x73,0x74,0x61,0x74, + 0x69,0x63,0x20,0x63,0x68,0x61,0x72,0x20,0x2a,0x6d,0x61,0x67,0x69,0x63, + 0x6b,0x5b,0x5d,0x20,0x3d,0x20,0x7b,0x0a,0x2f,0x2a,0x20,0x63,0x6f,0x6c, + 0x75,0x6d,0x6e,0x73,0x20,0x72,0x6f,0x77,0x73,0x20,0x63,0x6f,0x6c,0x6f, + 0x72,0x73,0x20,0x63,0x68,0x61,0x72,0x73,0x2d,0x70,0x65,0x72,0x2d,0x70, + 0x69,0x78,0x65,0x6c,0x20,0x2a,0x2f,0x0a,0x22,0x32,0x32,0x20,0x32,0x32, + 0x20,0x36,0x20,0x31,0x22,0x2c,0x0a,0x22,0x20,0x20,0x63,0x20,0x23,0x30, + 0x30,0x38,0x30,0x38,0x30,0x22,0x2c,0x0a,0x22,0x2e,0x20,0x63,0x20,0x23, + 0x38,0x30,0x38,0x30,0x30,0x30,0x22,0x2c,0x0a,0x22,0x58,0x20,0x63,0x20, + 0x23,0x38,0x30,0x38,0x30,0x38,0x30,0x22,0x2c,0x0a,0x22,0x6f,0x20,0x63, + 0x20,0x23,0x63,0x30,0x63,0x30,0x63,0x30,0x22,0x2c,0x0a,0x22,0x4f,0x20, + 0x63,0x20,0x47,0x72,0x61,0x79,0x31,0x30,0x30,0x22,0x2c,0x0a,0x22,0x2b, + 0x20,0x63,0x20,0x4e,0x6f,0x6e,0x65,0x22,0x2c,0x0a,0x2f,0x2a,0x20,0x70, + 0x69,0x78,0x65,0x6c,0x73,0x20,0x2a,0x2f,0x0a,0x22,0x2b,0x2b,0x2b,0x2b, + 0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b, + 0x2b,0x2b,0x2b,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b, + 0x2b,0x2b,0x2b,0x6f,0x6f,0x6f,0x6f,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b, + 0x2b,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x6f,0x6f, + 0x6f,0x58,0x20,0x20,0x58,0x6f,0x6f,0x6f,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b, + 0x22,0x2c,0x0a,0x22,0x2b,0x2b,0x2b,0x2b,0x2b,0x6f,0x58,0x58,0x58,0x4f, + 0x20,0x20,0x4f,0x58,0x58,0x58,0x6f,0x2b,0x2b,0x2b,0x2b,0x2b,0x22,0x2c, + 0x0a,0x22,0x2b,0x2b,0x2b,0x2b,0x6f,0x58,0x4f,0x4f,0x4f,0x4f,0x20,0x20, + 0x4f,0x4f,0x4f,0x4f,0x58,0x6f,0x2b,0x2b,0x2b,0x2b,0x22,0x2c,0x0a,0x22, + 0x2b,0x2b,0x2b,0x6f,0x58,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f, + 0x4f,0x4f,0x4f,0x58,0x6f,0x2b,0x2b,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x2b, + 0x6f,0x58,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f, + 0x4f,0x4f,0x58,0x6f,0x2b,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x2b,0x6f,0x58, + 0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f, + 0x58,0x6f,0x2b,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x2b,0x6f,0x58,0x4f,0x4f, + 0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x20,0x4f,0x4f,0x4f,0x4f,0x58,0x6f, + 0x2b,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x6f,0x58,0x4f,0x4f,0x4f,0x4f,0x4f, + 0x4f,0x4f,0x4f,0x4f,0x20,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x58,0x6f,0x2b, + 0x22,0x2c,0x0a,0x22,0x2b,0x6f,0x58,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f, + 0x20,0x20,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x58,0x6f,0x2b,0x22,0x2c, + 0x0a,0x22,0x2b,0x6f,0x58,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x20,0x20, + 0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x58,0x6f,0x2b,0x22,0x2c,0x0a,0x22, + 0x2b,0x6f,0x58,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x20,0x4f, + 0x4f,0x4f,0x4f,0x4f,0x4f,0x58,0x6f,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x2b, + 0x6f,0x58,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x20,0x4f,0x4f, + 0x4f,0x4f,0x58,0x6f,0x2b,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x2b,0x6f,0x58, + 0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x20,0x4f,0x4f,0x4f, + 0x58,0x6f,0x2b,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x2b,0x6f,0x58,0x4f,0x4f, + 0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x20,0x4f,0x4f,0x58,0x6f, + 0x2b,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x2b,0x2b,0x6f,0x58,0x4f,0x4f,0x4f, + 0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x58,0x6f,0x2b,0x2b,0x2b, + 0x22,0x2c,0x0a,0x22,0x2b,0x2b,0x2b,0x2b,0x6f,0x58,0x4f,0x4f,0x4f,0x4f, + 0x4f,0x4f,0x4f,0x4f,0x4f,0x4f,0x58,0x6f,0x2b,0x2b,0x2b,0x2b,0x22,0x2c, + 0x0a,0x22,0x2b,0x2b,0x2b,0x2b,0x2b,0x6f,0x58,0x58,0x58,0x4f,0x4f,0x4f, + 0x4f,0x58,0x58,0x58,0x6f,0x2b,0x2b,0x2b,0x2b,0x2b,0x22,0x2c,0x0a,0x22, + 0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x6f,0x6f,0x6f,0x58,0x58,0x58,0x58,0x6f, + 0x6f,0x6f,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x2b, + 0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x6f,0x6f,0x6f,0x6f,0x2b,0x2b,0x2b, + 0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x22,0x2c,0x0a,0x22,0x2b,0x2b,0x2b,0x2b, + 0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b,0x2b, + 0x2b,0x2b,0x2b,0x2b,0x22,0x0a,0x7d,0x3b,0x0a +}; + + +static const unsigned int clockedit_xpm_len = 819; +static const unsigned char clockedit_xpm_data[] = { + 0x2f,0x2a,0x20,0x58,0x50,0x4d,0x20,0x2a,0x2f,0x0a,0x73,0x74,0x61,0x74, + 0x69,0x63,0x20,0x63,0x68,0x61,0x72,0x20,0x2a,0x6d,0x61,0x67,0x69,0x63, + 0x6b,0x5b,0x5d,0x20,0x3d,0x20,0x7b,0x0a,0x2f,0x2a,0x20,0x63,0x6f,0x6c, + 0x75,0x6d,0x6e,0x73,0x20,0x72,0x6f,0x77,0x73,0x20,0x63,0x6f,0x6c,0x6f, + 0x72,0x73,0x20,0x63,0x68,0x61,0x72,0x73,0x2d,0x70,0x65,0x72,0x2d,0x70, + 0x69,0x78,0x65,0x6c,0x20,0x2a,0x2f,0x0a,0x22,0x32,0x32,0x20,0x32,0x32, + 0x20,0x31,0x30,0x20,0x31,0x22,0x2c,0x0a,0x22,0x20,0x20,0x63,0x20,0x42, + 0x6c,0x61,0x63,0x6b,0x22,0x2c,0x0a,0x22,0x2e,0x20,0x63,0x20,0x23,0x30, + 0x30,0x38,0x30,0x38,0x30,0x22,0x2c,0x0a,0x22,0x58,0x20,0x63,0x20,0x52, + 0x65,0x64,0x22,0x2c,0x0a,0x22,0x6f,0x20,0x63,0x20,0x4d,0x61,0x67,0x65, + 0x6e,0x74,0x61,0x22,0x2c,0x0a,0x22,0x4f,0x20,0x63,0x20,0x23,0x38,0x30, + 0x38,0x30,0x30,0x30,0x22,0x2c,0x0a,0x22,0x2b,0x20,0x63,0x20,0x59,0x65, + 0x6c,0x6c,0x6f,0x77,0x22,0x2c,0x0a,0x22,0x40,0x20,0x63,0x20,0x23,0x38, + 0x30,0x38,0x30,0x38,0x30,0x22,0x2c,0x0a,0x22,0x23,0x20,0x63,0x20,0x23, + 0x63,0x30,0x63,0x30,0x63,0x30,0x22,0x2c,0x0a,0x22,0x24,0x20,0x63,0x20, + 0x47,0x72,0x61,0x79,0x31,0x30,0x30,0x22,0x2c,0x0a,0x22,0x25,0x20,0x63, + 0x20,0x4e,0x6f,0x6e,0x65,0x22,0x2c,0x0a,0x2f,0x2a,0x20,0x70,0x69,0x78, + 0x65,0x6c,0x73,0x20,0x2a,0x2f,0x0a,0x22,0x25,0x25,0x25,0x25,0x25,0x25, + 0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25, + 0x25,0x25,0x22,0x2c,0x0a,0x22,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25, + 0x25,0x23,0x23,0x23,0x23,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25, + 0x22,0x2c,0x0a,0x22,0x25,0x25,0x25,0x25,0x25,0x25,0x23,0x23,0x23,0x40, + 0x2e,0x2e,0x40,0x23,0x23,0x23,0x23,0x23,0x23,0x25,0x25,0x25,0x22,0x2c, + 0x0a,0x22,0x25,0x25,0x25,0x25,0x25,0x23,0x40,0x40,0x23,0x23,0x23,0x23, + 0x23,0x23,0x40,0x40,0x23,0x6f,0x6f,0x6f,0x25,0x25,0x22,0x2c,0x0a,0x22, + 0x25,0x25,0x25,0x25,0x23,0x40,0x24,0x23,0x24,0x24,0x2e,0x2e,0x24,0x24, + 0x23,0x4f,0x4f,0x6f,0x6f,0x6f,0x23,0x25,0x22,0x2c,0x0a,0x22,0x25,0x25, + 0x25,0x23,0x40,0x23,0x23,0x24,0x24,0x24,0x24,0x24,0x24,0x23,0x4f,0x4f, + 0x4f,0x4f,0x6f,0x6f,0x23,0x25,0x22,0x2c,0x0a,0x22,0x25,0x25,0x23,0x40, + 0x24,0x23,0x24,0x24,0x24,0x24,0x24,0x24,0x23,0x4f,0x4f,0x2b,0x4f,0x4f, + 0x4f,0x23,0x25,0x25,0x22,0x2c,0x0a,0x22,0x25,0x25,0x23,0x40,0x23,0x24, + 0x24,0x24,0x24,0x24,0x24,0x23,0x4f,0x4f,0x4f,0x2b,0x2b,0x4f,0x4f,0x23, + 0x25,0x25,0x22,0x2c,0x0a,0x22,0x25,0x25,0x23,0x23,0x24,0x24,0x24,0x24, + 0x24,0x24,0x23,0x4f,0x4f,0x2b,0x2b,0x2b,0x4f,0x4f,0x23,0x23,0x25,0x25, + 0x22,0x2c,0x0a,0x22,0x25,0x23,0x40,0x23,0x24,0x24,0x24,0x24,0x24,0x23, + 0x4f,0x4f,0x4f,0x2b,0x2b,0x4f,0x4f,0x24,0x23,0x40,0x23,0x25,0x22,0x2c, + 0x0a,0x22,0x25,0x23,0x40,0x23,0x24,0x24,0x24,0x24,0x23,0x4f,0x4f,0x2b, + 0x2b,0x2b,0x4f,0x4f,0x24,0x24,0x23,0x40,0x23,0x25,0x22,0x2c,0x0a,0x22, + 0x25,0x23,0x40,0x23,0x24,0x24,0x24,0x23,0x4f,0x4f,0x4f,0x2b,0x2b,0x4f, + 0x4f,0x24,0x24,0x24,0x23,0x40,0x23,0x25,0x22,0x2c,0x0a,0x22,0x25,0x23, + 0x40,0x23,0x24,0x24,0x23,0x4f,0x4f,0x2b,0x2b,0x2b,0x4f,0x4f,0x24,0x24, + 0x24,0x24,0x23,0x40,0x23,0x25,0x22,0x2c,0x0a,0x22,0x25,0x25,0x23,0x23, + 0x24,0x23,0x4f,0x4f,0x4f,0x2b,0x2b,0x4f,0x4f,0x2e,0x24,0x24,0x24,0x24, + 0x23,0x23,0x25,0x25,0x22,0x2c,0x0a,0x22,0x25,0x25,0x23,0x40,0x23,0x4f, + 0x4f,0x2b,0x2b,0x2b,0x4f,0x4f,0x24,0x24,0x2e,0x24,0x24,0x23,0x40,0x23, + 0x25,0x25,0x22,0x2c,0x0a,0x22,0x25,0x25,0x23,0x40,0x4f,0x4f,0x4f,0x2b, + 0x2b,0x4f,0x4f,0x24,0x24,0x24,0x24,0x2e,0x23,0x24,0x40,0x23,0x25,0x25, + 0x22,0x2c,0x0a,0x22,0x25,0x25,0x25,0x4f,0x4f,0x2b,0x2b,0x2b,0x4f,0x4f, + 0x24,0x24,0x24,0x24,0x24,0x23,0x23,0x40,0x23,0x25,0x25,0x25,0x22,0x2c, + 0x0a,0x22,0x25,0x25,0x4f,0x4f,0x2b,0x2b,0x2b,0x4f,0x4f,0x24,0x24,0x24, + 0x24,0x24,0x23,0x24,0x40,0x23,0x25,0x25,0x25,0x25,0x22,0x2c,0x0a,0x22, + 0x25,0x25,0x20,0x4f,0x2b,0x2b,0x4f,0x4f,0x23,0x23,0x23,0x23,0x23,0x23, + 0x40,0x40,0x23,0x25,0x25,0x25,0x25,0x25,0x22,0x2c,0x0a,0x22,0x25,0x20, + 0x4f,0x4f,0x4f,0x4f,0x4f,0x23,0x23,0x40,0x40,0x40,0x40,0x23,0x23,0x23, + 0x25,0x25,0x25,0x25,0x25,0x25,0x22,0x2c,0x0a,0x22,0x25,0x20,0x4f,0x4f, + 0x20,0x4f,0x25,0x25,0x25,0x23,0x23,0x23,0x23,0x25,0x25,0x25,0x25,0x25, + 0x25,0x25,0x25,0x25,0x22,0x2c,0x0a,0x22,0x25,0x20,0x20,0x20,0x25,0x25, + 0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25,0x25, + 0x25,0x25,0x22,0x0a,0x7d,0x3b,0x0a +}; + +const char * delete_xpm[] = { +/* width height num_colors chars_per_pixel */ +"22 22 3 1", +/* colors */ +" c white", +". c none", +"X c black", +/* pixels */ +"......................", +"......................", +"......................", +"......................", +"......................", +"....XX ........XX ....", +"....XXXX .....XX .....", +".....XXXX ...XX ......", +".......XXX .X ........", +"........XXXXX ........", +".........XXX .........", +"........XXXXX ........", +".......XXX .XX .......", +"......XXX ...XX ......", +".....XXX .....X ......", +".....XXX ......X .....", +"......X ..............", +"................X ....", +"......................", +"......................", +"......................", +"......................"}; + +struct { + unsigned int size; + const unsigned char *data; + const char *name; +} embed_vec[] = { + { 765, clock_xpm_data, "clock.xpm" }, + { 819, clockedit_xpm_data, "clockedit.xpm" }, + { 0, 0, "dummy" } +}; diff --git a/karm/tray.cpp b/karm/tray.cpp new file mode 100644 index 000000000..228d807d8 --- /dev/null +++ b/karm/tray.cpp @@ -0,0 +1,168 @@ +/* +* KTray. +* +* This implements the functionality of the little icon in the kpanel +* tray. Among which are tool tips and the running clock animated icon +* +* Distributed under the GPL. +*/ + + +// #include <qkeycode.h> +// #include <qlayout.h> +#include <qpixmap.h> +#include <qptrlist.h> +#include <qstring.h> +#include <qtimer.h> +#include <qtooltip.h> + +#include <kaction.h> // actionPreferences() +#include <kglobal.h> +#include <kglobalsettings.h> +#include <kiconloader.h> // UserIcon +#include <klocale.h> // i18n +#include <kpopupmenu.h> // plug() +#include <ksystemtray.h> + +#include "mainwindow.h" +#include "task.h" +#include "tray.h" + +QPtrVector<QPixmap> *KarmTray::icons = 0; + +KarmTray::KarmTray(MainWindow* parent) + : KSystemTray(parent, "Karm Tray") +{ + // the timer that updates the "running" icon in the tray + _taskActiveTimer = new QTimer(this); + connect( _taskActiveTimer, SIGNAL( timeout() ), this, + SLOT( advanceClock()) ); + + if (icons == 0) { + icons = new QPtrVector<QPixmap>(8); + for (int i=0; i<8; i++) { + QPixmap *icon = new QPixmap(); + QString name; + name.sprintf("active-icon-%d.xpm",i); + *icon = UserIcon(name); + icons->insert(i,icon); + } + } + + parent->actionPreferences->plug( contextMenu() ); + parent->actionStopAll->plug( contextMenu() ); + + resetClock(); + initToolTip(); + + // start of a kind of menu for the tray + // this are experiments/tests + /* + for (int i=0; i<30; i++) + _tray->insertTitle(i 18n("bla ").arg(i)); + for (int i=0; i<30; i++) + _tray->insertTitle2(i 18n("bli ").arg(i)); + */ + // experimenting with menus for the tray + /* + trayPopupMenu = contextMenu(); + trayPopupMenu2 = new QPopupMenu(); + trayPopupMenu->insertItem(i18n("Submenu"), *trayPopupMenu2); + */ +} + +KarmTray::KarmTray(karmPart * parent) + : KSystemTray( 0 , "Karm Tray") +{ +// it is not convenient if every kpart gets an icon in the systray. + _taskActiveTimer = 0; +} + +KarmTray::~KarmTray() +{ +} + + +// experiment +/* +void KarmTray::insertTitle(QString title) +{ + trayPopupMenu->insertTitle(title); +} +*/ + +void KarmTray::startClock() +{ + if ( _taskActiveTimer ) + { + _taskActiveTimer->start(1000); + setPixmap( *(*icons)[_activeIcon] ); + show(); + } +} + +void KarmTray::stopClock() +{ + if ( _taskActiveTimer ) + { + _taskActiveTimer->stop(); + show(); + } +} + +void KarmTray::advanceClock() +{ + _activeIcon = (_activeIcon+1) % 8; + setPixmap( *(*icons)[_activeIcon]); +} + +void KarmTray::resetClock() +{ + _activeIcon = 0; + setPixmap( *(*icons)[_activeIcon]); + show(); +} + +void KarmTray::initToolTip() +{ + updateToolTip(QPtrList<Task> ()); +} + +void KarmTray::updateToolTip(QPtrList<Task> activeTasks) +{ + if ( activeTasks.isEmpty() ) { + QToolTip::add( this, i18n("No active tasks") ); + return; + } + + QFontMetrics fm( QToolTip::font() ); + const QString continued = i18n( ", ..." ); + const int buffer = fm.boundingRect( continued ).width(); + const int desktopWidth = KGlobalSettings::desktopGeometry(this).width(); + const int maxWidth = desktopWidth - buffer; + + QString qTip; + QString s; + + // Build the tool tip with all of the names of the active tasks. + // If at any time the width of the tool tip is larger than the desktop, + // stop building it. + QPtrListIterator<Task> item( activeTasks ); + for ( int i = 0; item.current(); ++item, ++i ) { + Task* task = item.current(); + if ( i > 0 ) + s += i18n( ", " ) + task->name(); + else + s += task->name(); + int width = fm.boundingRect( s ).width(); + if ( width > maxWidth ) { + qTip += continued; + break; + } + qTip = s; + } + + QToolTip::add( this, qTip ); +} + +#include "tray.moc" diff --git a/karm/tray.h b/karm/tray.h new file mode 100644 index 000000000..34701cff4 --- /dev/null +++ b/karm/tray.h @@ -0,0 +1,58 @@ +#ifndef KARM_TRAY_H +#define KARM_TRAY_H + +#include <qptrvector.h> +#include <qpixmap.h> +#include <qptrlist.h> +// experiement +// #include <kpopupmenu.h> +#include <ksystemtray.h> + +#include "task.h" +#include "karm_part.h" + +class KarmPart; + +class QPopupMenu; +class QTimer; + +class KSystemTray; +class MainWindow; +// experiment +// class KPopupMenu; + +class KarmTray : public KSystemTray +{ + Q_OBJECT + + public: + KarmTray(MainWindow * parent); + KarmTray(karmPart * parent); + ~KarmTray(); + + private: + int _activeIcon; + static QPtrVector<QPixmap> *icons; + QTimer *_taskActiveTimer; + + public slots: + void startClock(); + void stopClock(); + void resetClock(); + void updateToolTip( QPtrList<Task> activeTasks); + void initToolTip(); + + protected slots: + void advanceClock(); + + // experiment + /* + void insertTitle(QString title); + + private: + KPopupMenu *trayPopupMenu; + QPopupMenu *trayPopupMenu2; + */ +}; + +#endif // KARM_TRAY_H diff --git a/karm/uninstall.desktop b/karm/uninstall.desktop new file mode 100644 index 000000000..e1e3e1732 --- /dev/null +++ b/karm/uninstall.desktop @@ -0,0 +1,2 @@ +[Desktop Entry] +Hidden=true diff --git a/karm/version.h b/karm/version.h new file mode 100644 index 000000000..1a1820cf0 --- /dev/null +++ b/karm/version.h @@ -0,0 +1,3 @@ +#ifndef KARM_VERSION +#define KARM_VERSION "1.6.0" +#endif |