File indexing completed on 2024-11-24 04:48:17

0001 /*
0002   This file is part of KOrganizer.
0003 
0004   SPDX-FileCopyrightText: 1998 Preston Brown <pbrown@kde.org>
0005   SPDX-FileCopyrightText: 2000, 2001 Cornelius Schumacher <schumacher@kde.org>
0006   SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
0007 
0008   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0009 */
0010 
0011 #include "searchdialog.h"
0012 
0013 #include "calendarview.h"
0014 #include "koeventpopupmenu.h"
0015 #include "korganizer_debug.h"
0016 #include "ui_searchdialog_base.h"
0017 
0018 #include <EventViews/ListView>
0019 #include <PimCommon/PimUtil>
0020 
0021 #include <KConfigGroup>
0022 #include <KMessageBox>
0023 #include <KSharedConfig>
0024 #include <KWindowConfig>
0025 #include <QWindow>
0026 
0027 #include <QDialogButtonBox>
0028 #include <QPushButton>
0029 
0030 namespace
0031 {
0032 static const char mySearchDialogConfigGroupName[] = "SearchDialog";
0033 }
0034 
0035 SearchDialog::SearchDialog(CalendarView *calendarview)
0036     : QDialog(calendarview)
0037     , m_ui(new Ui::SearchDialog)
0038     , m_calendarview(calendarview)
0039 {
0040     setWindowTitle(i18nc("@title:window", "Find in Calendars"));
0041     setModal(false);
0042 
0043     auto mainLayout = new QVBoxLayout(this);
0044 
0045     auto mainWidget = new QWidget(this);
0046     m_ui->setupUi(mainWidget);
0047 
0048     // Set nice initial start and end dates for the search
0049     const QDate currDate = QDate::currentDate();
0050     m_ui->startDate->setDate(currDate);
0051     m_ui->endDate->setDate(currDate.addYears(1));
0052 
0053     connect(m_ui->searchEdit, &QLineEdit::textChanged, this, &SearchDialog::searchPatternChanged);
0054 
0055     connect(m_ui->dateRangeCheckbox, &QCheckBox::toggled, this, &SearchDialog::dateRangeCheckboxToggled);
0056     dateRangeCheckboxToggled();
0057 
0058     // Results list view
0059     auto layout = new QVBoxLayout;
0060     layout->setContentsMargins({});
0061     m_listView = new EventViews::ListView(this);
0062     layout->addWidget(m_listView);
0063     m_ui->listViewFrame->setLayout(layout);
0064 
0065     auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::Help, this);
0066     mainLayout->addWidget(mainWidget);
0067     m_user1Button = new QPushButton;
0068     buttonBox->addButton(m_user1Button, QDialogButtonBox::ActionRole);
0069     connect(buttonBox, &QDialogButtonBox::rejected, this, &SearchDialog::reject);
0070     connect(buttonBox, &QDialogButtonBox::helpRequested, this, &SearchDialog::slotHelpRequested);
0071     mainLayout->addWidget(buttonBox);
0072     m_user1Button->setDefault(true);
0073     KGuiItem::assign(m_user1Button, KGuiItem(i18nc("@action:button search in calendar", "&Search")));
0074     m_user1Button->setIcon(QIcon::fromTheme(QStringLiteral("search")));
0075     m_user1Button->setToolTip(i18nc("@info:tooltip", "Start the search"));
0076     m_user1Button->setWhatsThis(i18nc("@info:whatsthis", "Press this button to start the search."));
0077 
0078     connect(m_user1Button, &QPushButton::clicked, this, &SearchDialog::doSearch);
0079 
0080     // Propagate edit and delete event signals from event list view
0081     connect(m_listView, &EventViews::ListView::showIncidenceSignal, this, &SearchDialog::showIncidenceSignal);
0082     connect(m_listView, &EventViews::ListView::editIncidenceSignal, this, &SearchDialog::editIncidenceSignal);
0083     connect(m_listView, &EventViews::ListView::deleteIncidenceSignal, this, &SearchDialog::deleteIncidenceSignal);
0084 
0085     m_popupMenu = new KOEventPopupMenu(KOEventPopupMenu::MiniList, this);
0086     connect(m_listView, &EventViews::ListView::showIncidencePopupSignal, m_popupMenu, &KOEventPopupMenu::showIncidencePopup);
0087 
0088     connect(m_popupMenu, &KOEventPopupMenu::showIncidenceSignal, this, &SearchDialog::showIncidenceSignal);
0089     connect(m_popupMenu, &KOEventPopupMenu::editIncidenceSignal, this, &SearchDialog::editIncidenceSignal);
0090     connect(m_popupMenu, &KOEventPopupMenu::deleteIncidenceSignal, this, &SearchDialog::deleteIncidenceSignal);
0091     // TODO: add these
0092     //   connect(m_popupMenu, &KOEventPopupMenu::toggleAlarmSignal, this,
0093     //           &SearchDialog::toggleAlarmSignal);
0094     //   connect(m_popupMenu, &KOEventPopupMenu::toggleTodoCompletedSignal, this,
0095     //           &SearchDialog::toggleTodoCompletedSignal);
0096 
0097     for (const auto &calendar : m_calendarview->enabledCalendars()) {
0098         m_listView->addCalendar(calendar);
0099     }
0100 
0101     readConfig();
0102 }
0103 
0104 SearchDialog::~SearchDialog()
0105 {
0106     writeConfig();
0107     delete m_popupMenu;
0108     delete m_ui;
0109 }
0110 
0111 void SearchDialog::showEvent(QShowEvent *event)
0112 {
0113     Q_UNUSED(event)
0114     activateWindow();
0115     m_ui->searchEdit->setFocus();
0116 }
0117 
0118 void SearchDialog::searchPatternChanged(const QString &pattern)
0119 {
0120     m_user1Button->setEnabled(!pattern.isEmpty());
0121 }
0122 
0123 void SearchDialog::dateRangeCheckboxToggled()
0124 {
0125     m_ui->startDate->setEnabled(m_ui->dateRangeCheckbox->isChecked());
0126     m_ui->startDateLabel->setEnabled(m_ui->dateRangeCheckbox->isChecked());
0127     m_ui->endDate->setEnabled(m_ui->dateRangeCheckbox->isChecked());
0128     m_ui->endDateLabel->setEnabled(m_ui->dateRangeCheckbox->isChecked());
0129     m_ui->inclusiveCheck->setEnabled(m_ui->dateRangeCheckbox->isChecked());
0130     m_ui->includeUndatedTodos->setEnabled(m_ui->dateRangeCheckbox->isChecked());
0131 }
0132 
0133 void SearchDialog::doSearch()
0134 {
0135     QRegularExpression re;
0136     re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
0137     QRegularExpression::WildcardConversionOptions options;
0138     options |= QRegularExpression::UnanchoredWildcardConversion;
0139     options |= QRegularExpression::NonPathWildcardConversion;
0140     const QString pattern = QRegularExpression::wildcardToRegularExpression(m_ui->searchEdit->text(), options);
0141     re.setPattern(pattern);
0142     if (!re.isValid()) {
0143         KMessageBox::error(this,
0144                            i18nc("@info",
0145                                  "Invalid search expression, cannot perform the search. "
0146                                  "Please enter a search expression using the wildcard characters "
0147                                  "'*' and '?' where needed."));
0148         return;
0149     }
0150 
0151     search(re);
0152     m_listView->showIncidences(m_matchedEvents, QDate());
0153     updateMatchesText();
0154     if (m_matchedEvents.isEmpty()) {
0155         m_ui->numItems->setText(QString());
0156         KMessageBox::information(this,
0157                                  i18nc("@info", "No items were found that match your search pattern."),
0158                                  i18nc("@title:window", "Search Results"),
0159                                  QStringLiteral("NoSearchResults"));
0160     }
0161 }
0162 
0163 void SearchDialog::popupMenu(const QPoint &point)
0164 {
0165     m_listView->popupMenu(point);
0166 }
0167 
0168 void SearchDialog::updateMatchesText()
0169 {
0170     if (m_matchedEvents.isEmpty()) {
0171         m_ui->numItems->setText(QString());
0172     } else {
0173         m_ui->numItems->setText(i18ncp("@label", "%1 match", "%1 matches", m_matchedEvents.count()));
0174     }
0175 }
0176 
0177 void SearchDialog::updateView()
0178 {
0179     QRegularExpression re;
0180     re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
0181     re.setPattern(m_ui->searchEdit->text());
0182     m_listView->clear();
0183     if (re.isValid()) {
0184         search(re);
0185     } else {
0186         m_matchedEvents.clear();
0187     }
0188     m_listView->showIncidences(m_matchedEvents, QDate());
0189     updateMatchesText();
0190 }
0191 
0192 void SearchDialog::search(const QRegularExpression &regularExpression)
0193 {
0194     const QDate startDt = m_ui->startDate->date();
0195     const QDate endDt = m_ui->endDate->date();
0196 
0197     KCalendarCore::Event::List events;
0198     KCalendarCore::Todo::List todos;
0199     KCalendarCore::Journal::List journals;
0200     for (const auto &calendar : m_calendarview->enabledCalendars()) {
0201         if (m_ui->dateRangeCheckbox->isChecked()) {
0202             if (m_ui->eventsCheck->isChecked()) {
0203                 events += calendar->events(startDt, endDt, QTimeZone::systemTimeZone(), m_ui->inclusiveCheck->isChecked());
0204             }
0205             if (m_ui->includeUndatedTodos->isChecked()) {
0206                 for (const KCalendarCore::Todo::Ptr &todo : calendar->todos()) {
0207                     Q_ASSERT(todo);
0208                     if ((!todo->hasStartDate() && !todo->hasDueDate()) // undated
0209                         || (todo->hasStartDate() && (todo->dtStart().toLocalTime().date() >= startDt)
0210                             && (todo->dtStart().toLocalTime().date() <= endDt)) // start dt in range
0211                         || (todo->hasDueDate() && (todo->dtDue().toLocalTime().date() >= startDt)
0212                             && (todo->dtDue().toLocalTime().date() <= endDt)) // due dt in range
0213                         || (todo->hasCompletedDate() && (todo->completed().toLocalTime().date() >= startDt)
0214                             && (todo->completed().toLocalTime().date() <= endDt))) { // completed dt in range
0215                         todos.append(todo);
0216                     }
0217                 }
0218             } else {
0219                 todos += calendar->todos(startDt, endDt, QTimeZone::systemTimeZone(), m_ui->inclusiveCheck->isChecked());
0220             }
0221 
0222             if (m_ui->journalsCheck->isChecked()) {
0223                 for (auto dt = startDt; dt <= endDt; dt = dt.addDays(1)) {
0224                     journals += calendar->journals(dt);
0225                 }
0226             }
0227 
0228         } else {
0229             if (m_ui->eventsCheck->isChecked()) {
0230                 events += calendar->events();
0231             }
0232             if (m_ui->todosCheck->isChecked()) {
0233                 todos += calendar->todos();
0234             }
0235             if (m_ui->journalsCheck->isChecked()) {
0236                 journals += calendar->journals();
0237             }
0238         }
0239     }
0240 
0241     m_matchedEvents.clear();
0242     const KCalendarCore::Incidence::List incidences = Akonadi::ETMCalendar::mergeIncidenceList(events, todos, journals);
0243     for (const KCalendarCore::Incidence::Ptr &ev : incidences) {
0244         Q_ASSERT(ev);
0245         auto collectionId = ev->customProperty("VOLATILE", "COLLECTION-ID").toLongLong();
0246         Akonadi::Item item;
0247         for (const auto &calendar : m_calendarview->enabledCalendars()) {
0248             if (calendar->collection().id() == collectionId) {
0249                 item = calendar->item(ev);
0250             }
0251         }
0252         if (!item.isValid()) {
0253             qCWarning(KORGANIZER_LOG) << "Failed to translate incidence " << ev->uid() << " to Akonadi Item";
0254             continue;
0255         }
0256 
0257         if (m_ui->summaryCheck->isChecked()) {
0258             if (regularExpression.match(ev->summary()).hasMatch()) {
0259                 m_matchedEvents.append(item);
0260                 continue;
0261             }
0262         }
0263         if (m_ui->descriptionCheck->isChecked()) {
0264             if (regularExpression.match(ev->description()).hasMatch()) {
0265                 m_matchedEvents.append(item);
0266                 continue;
0267             }
0268         }
0269         if (m_ui->categoryCheck->isChecked()) {
0270             if (regularExpression.match(ev->categoriesStr()).hasMatch()) {
0271                 m_matchedEvents.append(item);
0272                 continue;
0273             }
0274         }
0275         if (m_ui->locationCheck->isChecked()) {
0276             if (regularExpression.match(ev->location()).hasMatch()) {
0277                 m_matchedEvents.append(item);
0278                 continue;
0279             }
0280         }
0281         if (m_ui->attendeeCheck->isChecked()) {
0282             const KCalendarCore::Attendee::List lstAttendees = ev->attendees();
0283             for (const KCalendarCore::Attendee &attendee : lstAttendees) {
0284                 if (regularExpression.match(attendee.fullName()).hasMatch()) {
0285                     m_matchedEvents.append(item);
0286                     break;
0287                 }
0288             }
0289         }
0290     }
0291 }
0292 
0293 void SearchDialog::readConfig()
0294 {
0295     create(); // ensure a window is created
0296     windowHandle()->resize(QSize(775, 600));
0297     KConfigGroup group(KSharedConfig::openStateConfig(), QLatin1StringView(mySearchDialogConfigGroupName));
0298     KWindowConfig::restoreWindowSize(windowHandle(), group);
0299     resize(windowHandle()->size()); // workaround for QTBUG-40584
0300 
0301     m_listView->readSettings(group);
0302 }
0303 
0304 void SearchDialog::writeConfig()
0305 {
0306     KConfigGroup group(KSharedConfig::openStateConfig(), QLatin1StringView(mySearchDialogConfigGroupName));
0307     KWindowConfig::saveWindowSize(windowHandle(), group);
0308     m_listView->writeSettings(group);
0309     group.sync();
0310 }
0311 
0312 void SearchDialog::slotHelpRequested()
0313 {
0314     PimCommon::Util::invokeHelp(QStringLiteral("korganizer/search-view.html"));
0315 }
0316 
0317 #include "moc_searchdialog.cpp"