/* * alarmlistview.cpp - widget showing list of outstanding alarms * Program: kalarm * Copyright © 2001-2007 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU 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 "kalarm.h" #include #include #include #include #include #include #include #include #include #include #include "alarmcalendar.h" #include "alarmtext.h" #include "functions.h" #include "kalarmapp.h" #include "preferences.h" #include "alarmlistview.moc" class AlarmListTooltip : public TQToolTip { public: AlarmListTooltip(TQWidget* parent) : TQToolTip(parent) { } virtual ~AlarmListTooltip() {} protected: virtual void maybeTip(const TQPoint&); }; /*============================================================================= = Class: AlarmListView = Displays the list of outstanding alarms. =============================================================================*/ TQValueList AlarmListView::mInstanceList; bool AlarmListView::mDragging = false; AlarmListView::AlarmListView(const TQValueList& order, TQWidget* parent, const char* name) : EventListViewBase(parent, name), mMousePressed(false), mDrawMessageInColour(false), mShowExpired(false) { static TQString titles[COLUMN_COUNT] = { i18n("Time"), i18n("Time To"), i18n("Repeat"), TQString(), TQString(), i18n("Message, File or Command") }; setSelectionMode(TQListView::Extended); // Set the column order int i; bool ok = false; if (order.count() >= COLUMN_COUNT) { // The column order is specified bool posns[COLUMN_COUNT]; for (i = 0; i < COLUMN_COUNT; ++i) posns[i] = false; for (i = 0; i < COLUMN_COUNT; ++i) { int ord = order[i]; if (ord < COLUMN_COUNT && ord >= 0) { mColumn[i] = ord; posns[ord] = true; } } ok = true; for (i = 0; i < COLUMN_COUNT; ++i) if (!posns[i]) ok = false; if (ok && mColumn[MESSAGE_COLUMN] != MESSAGE_COLUMN) { // Shift the message column to be last, since otherwise // column widths get screwed up. int messageCol = mColumn[MESSAGE_COLUMN]; for (i = 0; i < COLUMN_COUNT; ++i) if (mColumn[i] > messageCol) --mColumn[i]; mColumn[MESSAGE_COLUMN] = MESSAGE_COLUMN; } } if (!ok) { // Either no column order was specified, or it was invalid, // so use the default order for (i = 0; i < COLUMN_COUNT; ++i) mColumn[i] = i; } // Initialise the columns for (i = 0; i < COLUMN_COUNT; ++i) { for (int j = 0; j < COLUMN_COUNT; ++j) if (mColumn[j] == i) { if (j != MESSAGE_COLUMN) addColumn(titles[j]); break; } } addLastColumn(titles[MESSAGE_COLUMN]); setSorting(mColumn[TIME_COLUMN]); // sort initially by date/time mTimeColumnHeaderWidth = columnWidth(mColumn[TIME_COLUMN]); mTimeToColumnHeaderWidth = columnWidth(mColumn[TIME_TO_COLUMN]); setColumnAlignment(mColumn[REPEAT_COLUMN], TQt::AlignHCenter); setColumnWidthMode(mColumn[REPEAT_COLUMN], TQListView::Maximum); // Set the width of the colour column in proportion to height setColumnWidth(mColumn[COLOUR_COLUMN], itemHeight() * 3/4); setColumnWidthMode(mColumn[COLOUR_COLUMN], TQListView::Manual); // Set the width of the alarm type column to exactly accommodate the icons. // Don't allow the user to resize it (to avoid refresh problems, and bearing // in mind that resizing doesn't seem very useful anyway). setColumnWidth(mColumn[TYPE_COLUMN], AlarmListViewItem::typeIconWidth(this)); setColumnWidthMode(mColumn[TYPE_COLUMN], TQListView::Manual); header()->setResizeEnabled(false, mColumn[TYPE_COLUMN]); mInstanceList.append(this); mTooltip = new AlarmListTooltip(viewport()); } AlarmListView::~AlarmListView() { delete mTooltip; mTooltip = 0; mInstanceList.remove(this); } /****************************************************************************** * Add all the current alarms to the list. */ void AlarmListView::populate() { KAEvent event; KCal::Event::List events; KCal::Event::List::ConstIterator it; TQDateTime now = TQDateTime::currentDateTime(); if (mShowExpired) { AlarmCalendar* cal = AlarmCalendar::expiredCalendarOpen(); if (cal) { events = cal->events(); for (it = events.begin(); it != events.end(); ++it) { KCal::Event* kcalEvent = *it; if (kcalEvent->alarms().count() > 0) { event.set(*kcalEvent); addEntry(event, now); } } } } events = AlarmCalendar::activeCalendar()->events(); for (it = events.begin(); it != events.end(); ++it) { KCal::Event* kcalEvent = *it; event.set(*kcalEvent); if (mShowExpired || !event.expired()) addEntry(event, now); } } /****************************************************************************** * Set which time columns are to be displayed. */ void AlarmListView::selectTimeColumns(bool time, bool timeTo) { if (!time && !timeTo) return; // always show at least one time column bool changed = false; int w = columnWidth(mColumn[TIME_COLUMN]); if (time && !w) { // Unhide the time column int colWidth = mTimeColumnHeaderWidth; TQFontMetrics fm = fontMetrics(); for (AlarmListViewItem* item = firstChild(); item; item = item->nextSibling()) { int w = item->width(fm, this, mColumn[TIME_COLUMN]); if (w > colWidth) colWidth = w; } setColumnWidth(mColumn[TIME_COLUMN], colWidth); setColumnWidthMode(mColumn[TIME_COLUMN], TQListView::Maximum); changed = true; } else if (!time && w) { // Hide the time column setColumnWidthMode(mColumn[TIME_COLUMN], TQListView::Manual); setColumnWidth(mColumn[TIME_COLUMN], 0); changed = true; } w = columnWidth(mColumn[TIME_TO_COLUMN]); if (timeTo && !w) { // Unhide the time-to-alarm column setColumnWidthMode(mColumn[TIME_TO_COLUMN], TQListView::Maximum); updateTimeToAlarms(true); if (columnWidth(mColumn[TIME_TO_COLUMN]) < mTimeToColumnHeaderWidth) setColumnWidth(mColumn[TIME_TO_COLUMN], mTimeToColumnHeaderWidth); changed = true; } else if (!timeTo && w) { // Hide the time-to-alarm column setColumnWidthMode(mColumn[TIME_TO_COLUMN], TQListView::Manual); setColumnWidth(mColumn[TIME_TO_COLUMN], 0); changed = true; } if (changed) { resizeLastColumn(); triggerUpdate(); // ensure scroll bar appears if needed } } /****************************************************************************** * Update all the values in the time-to-alarm column. */ void AlarmListView::updateTimeToAlarms(bool forceDisplay) { if (forceDisplay || columnWidth(mColumn[TIME_TO_COLUMN])) { TQDateTime now = TQDateTime::currentDateTime(); for (AlarmListViewItem* item = firstChild(); item; item = item->nextSibling()) item->updateTimeToAlarm(now, forceDisplay); } } /****************************************************************************** * Add an event to every list instance. * The selection highlight is moved to the new event in the specified instance only. */ void AlarmListView::addEvent(const KAEvent& event, EventListViewBase* view) { TQDateTime now = TQDateTime::currentDateTime(); for (InstanceListConstIterator it = mInstanceList.begin(); it != mInstanceList.end(); ++it) static_cast(*it)->addEntry(event, now, true, (*it == view)); } /****************************************************************************** * Add a new item to the list. */ AlarmListViewItem* AlarmListView::addEntry(const KAEvent& event, const TQDateTime& now, bool setSize, bool reselect) { if (!mShowExpired && event.expired()) return 0; AlarmListViewItem* item = new AlarmListViewItem(this, event, now); return static_cast(EventListViewBase::addEntry(item, setSize, reselect)); } /****************************************************************************** * Create a new list item for addEntry(). */ EventListViewItemBase* AlarmListView::createItem(const KAEvent& event) { return new AlarmListViewItem(this, event, TQDateTime::currentDateTime()); } /****************************************************************************** * Check whether an item's alarm has expired. */ bool AlarmListView::expired(AlarmListViewItem* item) const { return item->event().expired(); } /****************************************************************************** * Return the column order. */ TQValueList AlarmListView::columnOrder() const { TQHeader* hdr = header(); int order[COLUMN_COUNT]; order[TIME_COLUMN] = hdr->mapToIndex(mColumn[TIME_COLUMN]); order[TIME_TO_COLUMN] = hdr->mapToIndex(mColumn[TIME_TO_COLUMN]); order[REPEAT_COLUMN] = hdr->mapToIndex(mColumn[REPEAT_COLUMN]); order[COLOUR_COLUMN] = hdr->mapToIndex(mColumn[COLOUR_COLUMN]); order[TYPE_COLUMN] = hdr->mapToIndex(mColumn[TYPE_COLUMN]); order[MESSAGE_COLUMN] = hdr->mapToIndex(mColumn[MESSAGE_COLUMN]); TQValueList orderList; for (int i = 0; i < COLUMN_COUNT; ++i) orderList += order[i]; return orderList; } /****************************************************************************** * Returns the TQWhatsThis text for a specified column. */ TQString AlarmListView::whatsThisText(int column) const { if (column == mColumn[TIME_COLUMN]) return i18n("Next scheduled date and time of the alarm"); if (column == mColumn[TIME_TO_COLUMN]) return i18n("How long until the next scheduled trigger of the alarm"); if (column == mColumn[REPEAT_COLUMN]) return i18n("How often the alarm recurs"); if (column == mColumn[COLOUR_COLUMN]) return i18n("Background color of alarm message"); if (column == mColumn[TYPE_COLUMN]) return i18n("Alarm type (message, file, command or email)"); if (column == mColumn[MESSAGE_COLUMN]) return i18n("Alarm message text, URL of text file to display, command to execute, or email subject line"); return i18n("List of scheduled alarms"); } /****************************************************************************** * Called when the mouse button is pressed. * Records the position of the mouse when the left button is pressed, for use * in drag operations. */ void AlarmListView::contentsMousePressEvent(TQMouseEvent* e) { TQListView::contentsMousePressEvent(e); if (e->button() == Qt::LeftButton) { TQPoint p(contentsToViewport(e->pos())); if (itemAt(p)) { mMousePressPos = e->pos(); mMousePressed = true; } mDragging = false; } } /****************************************************************************** * Called when the mouse is moved. * Creates a drag object when the mouse drags one or more selected items. */ void AlarmListView::contentsMouseMoveEvent(TQMouseEvent* e) { TQListView::contentsMouseMoveEvent(e); if (mMousePressed && (mMousePressPos - e->pos()).manhattanLength() > TQApplication::startDragDistance()) { // Create a calendar object containing all the currently selected alarms kdDebug(5950) << "AlarmListView::contentsMouseMoveEvent(): drag started" << endl; mMousePressed = false; KCal::CalendarLocal cal(TQString::fromLatin1("UTC")); cal.setLocalTime(); // write out using local time (i.e. no time zone) TQValueList items = selectedItems(); if (!items.count()) return; for (TQValueList::Iterator it = items.begin(); it != items.end(); ++it) { const KAEvent& event = (*it)->event(); KCal::Event* kcalEvent = new KCal::Event; event.updateKCalEvent(*kcalEvent, false, true); kcalEvent->setUid(event.id()); cal.addEvent(kcalEvent); } // Create the drag object for the destination program to receive mDragging = true; KCal::ICalDrag* dobj = new KCal::ICalDrag(&cal, this); dobj->dragCopy(); // the drag operation will copy the alarms } } /****************************************************************************** * Called when the mouse button is released. * Notes that the mouse left button is no longer pressed, for use in drag * operations. */ void AlarmListView::contentsMouseReleaseEvent(TQMouseEvent *e) { TQListView::contentsMouseReleaseEvent(e); mMousePressed = false; mDragging = false; } /*============================================================================= = Class: AlarmListViewItem = Contains the details of one alarm for display in the AlarmListView. =============================================================================*/ int AlarmListViewItem::mTimeHourPos = -2; int AlarmListViewItem::mDigitWidth = -1; AlarmListViewItem::AlarmListViewItem(AlarmListView* parent, const KAEvent& event, const TQDateTime& now) : EventListViewItemBase(parent, event), mMessageTruncated(false), mTimeToAlarmShown(false) { setLastColumnText(); // set the message column text DateTime dateTime = event.expired() ? event.startDateTime() : event.displayDateTime(); if (parent->column(AlarmListView::TIME_COLUMN) >= 0) setText(parent->column(AlarmListView::TIME_COLUMN), alarmTimeText(dateTime)); if (parent->column(AlarmListView::TIME_TO_COLUMN) >= 0) { TQString tta = timeToAlarmText(now); setText(parent->column(AlarmListView::TIME_TO_COLUMN), tta); mTimeToAlarmShown = !tta.isNull(); } TQTime t = dateTime.time(); mDateTimeOrder.sprintf("%04d%03d%02d%02d", dateTime.date().year(), dateTime.date().dayOfYear(), t.hour(), t.minute()); int repeatOrder = 0; int repeatInterval = 0; TQString repeatText = event.recurrenceText(true); // text displayed in Repeat column if (repeatText.isEmpty()) repeatText = event.repetitionText(true); if (event.repeatAtLogin()) repeatOrder = 1; else { repeatInterval = event.recurInterval(); switch (event.recurType()) { case KARecurrence::MINUTELY: repeatOrder = 2; break; case KARecurrence::DAILY: repeatOrder = 3; break; case KARecurrence::WEEKLY: repeatOrder = 4; break; case KARecurrence::MONTHLY_DAY: case KARecurrence::MONTHLY_POS: repeatOrder = 5; break; case KARecurrence::ANNUAL_DATE: case KARecurrence::ANNUAL_POS: repeatOrder = 6; break; case KARecurrence::NO_RECUR: default: break; } } setText(parent->column(AlarmListView::REPEAT_COLUMN), repeatText); mRepeatOrder.sprintf("%c%08d", '0' + repeatOrder, repeatInterval); bool showColour = (event.action() == KAEvent::MESSAGE || event.action() == KAEvent::FILE); mColourOrder.sprintf("%06u", (showColour ? event.bgColour().rgb() : 0)); mTypeOrder.sprintf("%02d", event.action()); } /****************************************************************************** * Return the single line alarm summary text. */ TQString AlarmListViewItem::alarmText(const KAEvent& event) const { bool truncated; TQString text = AlarmText::summary(event, 1, &truncated); mMessageTruncated = truncated; return text; } /****************************************************************************** * Return the alarm time text in the form "date time". */ TQString AlarmListViewItem::alarmTimeText(const DateTime& dateTime) const { TDELocale* locale = TDEGlobal::locale(); TQString dateTimeText = locale->formatDate(dateTime.date(), true); if (!dateTime.isDateOnly()) { dateTimeText += ' '; TQString time = locale->formatTime(dateTime.time()); if (mTimeHourPos == -2) { // Initialise the position of the hour within the time string, if leading // zeroes are omitted, so that displayed times can be aligned with each other. mTimeHourPos = -1; // default = alignment isn't possible/sensible if (!TQApplication::reverseLayout()) // don't try to align right-to-left languages { TQString fmt = locale->timeFormat(); int i = fmt.find(TQRegExp("%[kl]")); // check if leading zeroes are omitted if (i >= 0 && i == fmt.find('%')) // and whether the hour is first mTimeHourPos = i; // yes, so need to align } } if (mTimeHourPos >= 0 && (int)time.length() > mTimeHourPos + 1 && time[mTimeHourPos].isDigit() && !time[mTimeHourPos + 1].isDigit()) dateTimeText += '~'; // improve alignment of times with no leading zeroes dateTimeText += time; } return dateTimeText + ' '; } /****************************************************************************** * Return the time-to-alarm text. */ TQString AlarmListViewItem::timeToAlarmText(const TQDateTime& now) const { if (event().expired()) return TQString(); DateTime dateTime = event().displayDateTime(); if (dateTime.isDateOnly()) { int days = now.date().daysTo(dateTime.date()); return i18n("n days", " %1d ").arg(days); } int mins = (now.secsTo(dateTime.dateTime()) + 59) / 60; if (mins < 0) return TQString(); char minutes[3] = "00"; minutes[0] = (mins%60) / 10 + '0'; minutes[1] = (mins%60) % 10 + '0'; if (mins < 24*60) return i18n("hours:minutes", " %1:%2 ").arg(mins/60).arg(minutes); int days = mins / (24*60); mins = mins % (24*60); return i18n("days hours:minutes", " %1d %2:%3 ").arg(days).arg(mins/60).arg(minutes); } /****************************************************************************** * Update the displayed time-to-alarm value. * The updated value is only displayed if it is different from the existing value, * or if 'forceDisplay' is true. */ void AlarmListViewItem::updateTimeToAlarm(const TQDateTime& now, bool forceDisplay) { if (event().expired()) { if (forceDisplay || mTimeToAlarmShown) { setText(alarmListView()->column(AlarmListView::TIME_TO_COLUMN), TQString()); mTimeToAlarmShown = false; } } else { TQString tta = timeToAlarmText(now); if (forceDisplay || tta != text(alarmListView()->column(AlarmListView::TIME_TO_COLUMN))) setText(alarmListView()->column(AlarmListView::TIME_TO_COLUMN), tta); mTimeToAlarmShown = !tta.isNull(); } } /****************************************************************************** * Paint one value in one of the columns in the list view. */ void AlarmListViewItem::paintCell(TQPainter* painter, const TQColorGroup& cg, int column, int width, int /*align*/) { const AlarmListView* listView = alarmListView(); int margin = listView->itemMargin(); TQRect box(margin, margin, width - margin*2, height() - margin*2); // area within which to draw bool selected = isSelected(); TQColor bgColour = selected ? cg.highlight() : cg.base(); TQColor fgColour = selected ? cg.highlightedText() : !event().enabled() ? Preferences::disabledColour() : event().expired() ? Preferences::expiredColour() : cg.text(); painter->setPen(fgColour); painter->fillRect(0, 0, width, height(), bgColour); if (column == listView->column(AlarmListView::TIME_COLUMN)) { int i = -1; TQString str = text(column); if (mTimeHourPos >= 0) { // Need to pad out spacing to align times without leading zeroes i = str.find(" ~"); if (i >= 0) { if (mDigitWidth < 0) mDigitWidth = painter->fontMetrics().width("0"); TQString date = str.left(i + 1); int w = painter->fontMetrics().width(date) + mDigitWidth; painter->drawText(box, AlignVCenter, date); box.setLeft(box.left() + w); painter->drawText(box, AlignVCenter, str.mid(i + 2)); } } if (i < 0) painter->drawText(box, AlignVCenter, str); } else if (column == listView->column(AlarmListView::TIME_TO_COLUMN)) painter->drawText(box, AlignVCenter | AlignRight, text(column)); else if (column == listView->column(AlarmListView::REPEAT_COLUMN)) painter->drawText(box, AlignVCenter | AlignHCenter, text(column)); else if (column == listView->column(AlarmListView::COLOUR_COLUMN)) { // Paint the cell the colour of the alarm message if (event().action() == KAEvent::MESSAGE || event().action() == KAEvent::FILE) painter->fillRect(box, event().bgColour()); } else if (column == listView->column(AlarmListView::TYPE_COLUMN)) { // Display the alarm type icon, horizontally and vertically centred in the cell TQPixmap* pixmap = eventIcon(); TQRect pixmapRect = pixmap->rect(); int diff = box.height() - pixmap->height(); if (diff < 0) { pixmapRect.setTop(-diff / 2); pixmapRect.setHeight(box.height()); } TQPoint iconTopLeft(box.left() + (box.width() - pixmapRect.width()) / 2, box.top() + (diff > 0 ? diff / 2 : 0)); painter->drawPixmap(iconTopLeft, *pixmap, pixmapRect); } else if (column == listView->column(AlarmListView::MESSAGE_COLUMN)) { if (!selected && listView->drawMessageInColour()) { painter->fillRect(box, event().bgColour()); painter->setBackgroundColor(event().bgColour()); // painter->setPen(event().fgColour()); } TQString txt = text(column); painter->drawText(box, AlignVCenter, txt); mMessageColWidth = listView->fontMetrics().boundingRect(txt).width(); } } /****************************************************************************** * Return the width needed for the icons in the alarm type column. */ int AlarmListViewItem::typeIconWidth(AlarmListView* v) { return iconWidth() + 2 * v->style().pixelMetric(TQStyle::PM_DefaultFrameWidth); } /****************************************************************************** * Return the column sort order for one item in the list. */ TQString AlarmListViewItem::key(int column, bool) const { AlarmListView* listView = alarmListView(); if (column == listView->column(AlarmListView::TIME_COLUMN) || column == listView->column(AlarmListView::TIME_TO_COLUMN)) return mDateTimeOrder; if (column == listView->column(AlarmListView::REPEAT_COLUMN)) return mRepeatOrder; if (column == listView->column(AlarmListView::COLOUR_COLUMN)) return mColourOrder; if (column == listView->column(AlarmListView::TYPE_COLUMN)) return mTypeOrder; return text(column).lower(); } /*============================================================================= = Class: AlarmListTooltip = Displays the full alarm text in a tooltip when necessary. =============================================================================*/ /****************************************************************************** * Displays the full alarm text in a tooltip, if not all the text is displayed. */ void AlarmListTooltip::maybeTip(const TQPoint& pt) { AlarmListView* listView = (AlarmListView*)parentWidget()->parentWidget(); int column = listView->column(AlarmListView::MESSAGE_COLUMN); int xOffset = listView->contentsX(); if (listView->header()->sectionAt(pt.x() + xOffset) == column) { AlarmListViewItem* item = (AlarmListViewItem*)listView->itemAt(pt); if (item) { int columnX = listView->header()->sectionPos(column) - xOffset; int columnWidth = listView->columnWidth(column); int widthNeeded = item->messageColWidthNeeded(); if (!item->messageTruncated() && columnWidth >= widthNeeded) { if (columnX + widthNeeded <= listView->viewport()->width()) return; } TQRect rect = listView->itemRect(item); rect.setLeft(columnX); rect.setWidth(columnWidth); kdDebug(5950) << "AlarmListTooltip::maybeTip(): display\n"; tip(rect, AlarmText::summary(item->event(), 10)); // display up to 10 lines of text } } }