Search/Filter using Lucene

Searching and filtering the ride list using a search box.
This is implemented using a new optional dependency on
CLucene.

Fixes #627.
This commit is contained in:
Mark Liversedge
2012-10-21 15:28:26 +01:00
parent 914c07f576
commit 7a8d4377b9
21 changed files with 651 additions and 31 deletions

View File

@@ -7,7 +7,7 @@
John Ehrlinger
May 2011
Version 1.0
Version 1.1
A walkthrough of building GoldenCheetah from scratch on Ubuntu linux. This walkthrough
should be largely the same for any Linux distro.
@@ -31,7 +31,8 @@ CONTENTS
- flex
- bison
- libical - Diary window and CalDAV support (google/mobileme calendar integration)
- libvlc - Video playback in training mode
- libvlc - Video playback in training mode
- clucene - Indexing/Searching ride files
1. BASIC INSTALLATION WITH MANDATORY DEPENDENCIES
@@ -398,3 +399,29 @@ VLC_INSTALL = /usr/include/vlc/
$ make clean
$ qmake
$ make
CLUCENE - Indexing and Searching ride files (search box)
--------------------------------------------------------
You will need clucene runtime and core libraries, we developed against 0.9.21b-2 but
any 0.9 branch should work fine, let us know if you experience any issues. You may find
that the libclucene0ldbl runtime is already installed, this is fine and typical since
clucene is a very popular search library.
$ sudo apt-get install clucene-core
$ sudo apt-get install libclucene0ldbl
By default, and this is deliberate, the clucene install places the config headers into
a platform specific location. For my install I just copy the platform (linux) specific
header config into the normal /usr/include/CLucene directory with the following:
$ sudo cp /usr/lib/CLucene/clucene-config.h /usr/include/CLucene
Next we need to comment out the two CLUCENE lines in gcconfig.pri and they should read:
CLUCENE_INCLUDE = /usr/include/CLucene
CLUCENE_LIBS = -lclucene
$ make clean
$ qmake
$ make

View File

@@ -59,8 +59,9 @@
// 37 06 Apr 2012 Rainer Clasen Added non-zero average Power (watts)
// 38 8th Jul 2012 Mark Liversedge Computes metrics for manual files now
// 39 18 Aug 2012 Mark Liversedge New metric LRBalance
// 40 20 Oct 2012 Mark Liversedge Lucene search/filter and checkbox metadata field
static int DBSchemaVersion = 39;
static int DBSchemaVersion = 40;
DBAccess::DBAccess(MainWindow* main, QDir home) : main(main), home(home)
{
@@ -178,7 +179,7 @@ bool DBAccess::createMetricsTable()
// And all the metadata texts
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && (field.type < 3)) {
if (!main->specialFields.isMetric(field.name) && (field.type < 3 || field.type == 7)) {
createMetricTable += QString(", Z%1 varchar").arg(main->specialFields.makeTechName(field.name));
}
}
@@ -254,7 +255,7 @@ bool DBAccess::createMeasuresTable()
// And all the metadata texts
foreach(FieldDefinition field, fieldDefinitions)
if (field.type < 3) createMeasuresTable += QString(", Z%1 varchar").arg(main->specialFields.makeTechName(field.name));
if (field.type < 3 || field.type == 7) createMeasuresTable += QString(", Z%1 varchar").arg(main->specialFields.makeTechName(field.name));
// And all the metadata measures
foreach(FieldDefinition field, fieldDefinitions)
@@ -401,7 +402,7 @@ bool DBAccess::importRide(SummaryMetrics *summaryMetrics, RideFile *ride, QColor
// And all the metadata texts
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && (field.type < 3)) {
if (!main->specialFields.isMetric(field.name) && (field.type < 3 || field.type == 7)) {
insertStatement += QString(", Z%1 ").arg(main->specialFields.makeTechName(field.name));
}
}
@@ -416,7 +417,7 @@ bool DBAccess::importRide(SummaryMetrics *summaryMetrics, RideFile *ride, QColor
for (int i=0; i<factory.metricCount(); i++)
insertStatement += ",?";
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && field.type < 5) {
if (!main->specialFields.isMetric(field.name) && (field.type < 5 || field.type == 7)) {
insertStatement += ",?";
}
}
@@ -440,7 +441,7 @@ bool DBAccess::importRide(SummaryMetrics *summaryMetrics, RideFile *ride, QColor
// And all the metadata texts
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && (field.type < 3)) {
if (!main->specialFields.isMetric(field.name) && (field.type < 3 || field.type ==7)) {
query.addBindValue(ride->getTag(field.name, ""));
}
}
@@ -499,7 +500,7 @@ DBAccess::getRide(QString filename, SummaryMetrics &summaryMetrics, QColor&color
for (int i=0; i<factory.metricCount(); i++)
selectStatement += QString(", X%1 ").arg(factory.metricName(i));
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && field.type < 5) {
if (!main->specialFields.isMetric(field.name) && (field.type < 5 || field.type == 7)) {
selectStatement += QString(", Z%1 ").arg(main->specialFields.makeTechName(field.name));
}
}
@@ -556,7 +557,7 @@ QList<SummaryMetrics> DBAccess::getAllMetricsFor(QDateTime start, QDateTime end)
for (int i=0; i<factory.metricCount(); i++)
selectStatement += QString(", X%1 ").arg(factory.metricName(i));
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && field.type < 5) {
if (!main->specialFields.isMetric(field.name) && (field.type < 5 || field.type == 7)) {
selectStatement += QString(", Z%1 ").arg(main->specialFields.makeTechName(field.name));
}
}
@@ -585,7 +586,7 @@ QList<SummaryMetrics> DBAccess::getAllMetricsFor(QDateTime start, QDateTime end)
QString underscored = field.name;
summaryMetrics.setForSymbol(underscored.replace("_"," "), query.value(i+3).toDouble());
i++;
} else if (!main->specialFields.isMetric(field.name) && field.type < 3) {
} else if (!main->specialFields.isMetric(field.name) && (field.type < 3 || field.type == 7)) {
QString underscored = field.name;
// ignore texts for now XXX todo if want metadata from Summary Metrics
summaryMetrics.setText(underscored.replace("_"," "), query.value(i+3).toString());
@@ -607,7 +608,7 @@ SummaryMetrics DBAccess::getRideMetrics(QString filename)
for (int i=0; i<factory.metricCount(); i++)
selectStatement += QString(", X%1 ").arg(factory.metricName(i));
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && field.type < 5) {
if (!main->specialFields.isMetric(field.name) && (field.type < 5 || field.type == 7)) {
selectStatement += QString(", Z%1 ").arg(main->specialFields.makeTechName(field.name));
}
}
@@ -631,7 +632,7 @@ SummaryMetrics DBAccess::getRideMetrics(QString filename)
QString underscored = field.name;
summaryMetrics.setForSymbol(underscored.replace(" ","_"), query.value(i+2).toDouble());
i++;
} else if (!main->specialFields.isMetric(field.name) && field.type < 3) {
} else if (!main->specialFields.isMetric(field.name) && (field.type < 3 || field.type == 7)) {
// ignore texts for now XXX todo if want metadata from Summary Metrics
QString underscored = field.name;
summaryMetrics.setText(underscored.replace("_"," "), query.value(i+2).toString());
@@ -654,7 +655,7 @@ bool DBAccess::importMeasure(SummaryMetrics *summaryMetrics)
// And all the metadata texts
foreach(FieldDefinition field, mfieldDefinitions) {
if (field.type < 3) {
if (field.type < 3 || field.type == 7) {
insertStatement += QString(", Z%1 ").arg(msp.makeTechName(field.name));
}
}
@@ -668,7 +669,7 @@ bool DBAccess::importMeasure(SummaryMetrics *summaryMetrics)
insertStatement += " ) values (?,?"; // timestamp, measure_date
foreach(FieldDefinition field, mfieldDefinitions) {
if (field.type < 5) {
if (field.type < 5 || field.type == 7) {
insertStatement += ",?";
}
}
@@ -682,7 +683,7 @@ bool DBAccess::importMeasure(SummaryMetrics *summaryMetrics)
// And all the text measures
foreach(FieldDefinition field, mfieldDefinitions) {
if (field.type < 3) {
if (field.type < 3 || field.type == 7) {
query.addBindValue(summaryMetrics->getText(field.name, ""));
}
}
@@ -720,7 +721,7 @@ QList<SummaryMetrics> DBAccess::getAllMeasuresFor(QDateTime start, QDateTime end
// construct the select statement
QString selectStatement = "SELECT timestamp, measure_date";
foreach(FieldDefinition field, fieldDefinitions) {
if (!main->specialFields.isMetric(field.name) && field.type < 5) {
if (!main->specialFields.isMetric(field.name) && (field.type < 5 || field.type == 7)) {
selectStatement += QString(", Z%1 ").arg(main->specialFields.makeTechName(field.name));
}
}
@@ -744,7 +745,7 @@ QList<SummaryMetrics> DBAccess::getAllMeasuresFor(QDateTime start, QDateTime end
if (field.type == 3 || field.type == 4) {
add.setText(field.name, query.value(i).toString());
i++;
} else if (field.type < 3) {
} else if (field.type < 3 || field.type == 7) {
add.setText(field.name, query.value(i).toString());
i++;
}

156
src/Lucene.cpp Normal file
View File

@@ -0,0 +1,156 @@
/*
* Copyright (c) 2012 Mark Liversedge (liversedge@gmail.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "Lucene.h"
#include "MainWindow.h"
// stdc strings
using namespace std;
// here we go
using namespace lucene::analysis;
using namespace lucene::index;
using namespace lucene::document;
using namespace lucene::queryParser;
using namespace lucene::search;
using namespace lucene::store;
Lucene::Lucene(MainWindow *parent) : QObject(parent), main(parent)
{
// create the directory if needed
main->home.mkdir("index");
// make index directory if needed
QDir dir(main->home.canonicalPath() + "/index");
try {
bool indexExists = IndexReader::indexExists(dir.canonicalPath().toLocal8Bit().data());
// clear any locks
if (indexExists && IndexReader::isLocked(dir.canonicalPath().toLocal8Bit().data()))
IndexReader::unlock(dir.canonicalPath().toLocal8Bit().data());
if (!indexExists) {
IndexWriter *create = new IndexWriter(dir.canonicalPath().toLocal8Bit().data(), &analyzer, true);
// lets flush to disk and reopen
create->close();
delete create;
}
// now lets open using a mnodifier since the API is much simpler
writer = new IndexModifier(dir.canonicalPath().toLocal8Bit().data(), &analyzer, false); // for updates
} catch (CLuceneError &e) {
qDebug()<<"clucene error!"<<e.what();
}
}
Lucene::~Lucene()
{
writer->flush();
writer->close();
//XXXdelete writer; Causes a SEGV !?
}
bool Lucene::importRide(SummaryMetrics *, RideFile *ride, QColor , unsigned long, bool)
{
// create a document
Document doc;
// add Filename special field (unique)
std::wstring cname = ride->getTag("Filename","").toStdWString();
doc.add( *_CLNEW Field(_T("Filename"), cname.c_str(), Field::STORE_YES | Field::INDEX_UNTOKENIZED));
// And all the metadata texts
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && (field.type < 3 || field.type == 7)) {
std::wstring name = main->specialFields.makeTechName(field.name).toStdWString();
std::wstring value = ride->getTag(field.name,"").toStdWString();
doc.add( *_CLNEW Field(name.c_str(), value.c_str(), Field::STORE_YES | Field::INDEX_TOKENIZED));
}
}
// delete if already in the index
deleteRide(ride->getTag("Filename", ""));
// now add to index
writer->addDocument(&doc);
doc.clear();
return true;
}
bool Lucene::deleteRide(QString name)
{
std::wstring cname = name.toStdWString();
Term term(_T("Filename"), cname.c_str());
return (writer->deleteDocuments(&term)>0);
}
void Lucene::optimise()
{
writer->flush();
writer->optimize();
}
int Lucene::search(QString query)
{
try {
// parse query
QueryParser parser(_T("Notes"), &analyzer);
parser.setPhraseSlop(4);
std::wstring querystring = query.toStdWString();
Query* lquery = parser.parse(querystring.c_str());
if (lquery == NULL) return 0;
reader = IndexReader::open(writer->getDirectory()); // for querying against
searcher = new IndexSearcher(reader); // to perform searches
// go find hits
hits = searcher->search(lquery);
filenames.clear();
for (int i=0; i< hits->length(); i++) {
Document *d = &hits->doc(i);
filenames << QString::fromWCharArray(d->get(_T("Filename")));
}
delete hits;
delete lquery;
delete searcher;
delete reader;
} catch (CLuceneError &e) {
qDebug()<<"clucene error:"<<e.what();
return 0;
}
return filenames.count();
}

78
src/Lucene.h Normal file
View File

@@ -0,0 +1,78 @@
/*
* Copyright (c) 2012 Mark Liversedge (liversedge@gmail.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _GC_Lucene_h
#define _GC_Lucene_h
#include <QObject>
#include <QString>
#include <QDir>
#include "MainWindow.h"
#include "RideMetadata.h"
#include "SummaryMetrics.h"
#include "RideFile.h"
#include "CLucene.h"
#include "CLucene/index/IndexModifier.h"
using namespace lucene::analysis;
using namespace lucene::index;
using namespace lucene::document;
using namespace lucene::queryParser;
using namespace lucene::search;
using namespace lucene::store;
class Lucene : public QObject
{
Q_OBJECT
public:
Lucene(MainWindow *parent = 0);
~Lucene();
// Create/Delete Metrics
bool importRide(SummaryMetrics *summaryMetrics, RideFile *ride, QColor color, unsigned long, bool);
bool deleteRide(QString);
void optimise(); // for optimising the index once updated
// search
int search(QString query); // run query and return number of results found
QStringList &files() { return filenames; }
protected:
private slots:
signals:
private:
MainWindow *main;
// CLucene objects
SimpleAnalyzer analyzer;
IndexModifier* writer;
IndexReader* reader;
IndexSearcher* searcher;
// Query results
Hits *hits; // null when no results
QStringList filenames;
};
#endif

View File

@@ -89,6 +89,11 @@
#include "QTFullScreen.h"
#endif
#ifdef GC_HAVE_LUCENE
#include "SearchBox.h"
#include "Lucene.h"
#endif
#include <assert.h>
#include <QApplication>
#include <QtGui>
@@ -141,7 +146,7 @@ MainWindow::MainWindow(const QDir &home) :
GCColor *GCColorSet = new GCColor(this); // get/keep colorset
GCColorSet->colorSet(); // shut up the compiler
setStyleSheet("QFrame { FrameStyle = QFrame::NoFrame };"
"QWidget { background = Qt::white; border:0 px; margin: 2px; };"
"QWidget { background = Qt::white; border:0 px; margin: 0px; };"
"QTabWidget { background = Qt::white; };"
"::pane { FrameStyle = QFrame::NoFrame; border: 0px; };");
@@ -196,6 +201,9 @@ MainWindow::MainWindow(const QDir &home) :
// Metadata fields
_rideMetadata = new RideMetadata(this,true);
_rideMetadata->hide();
#ifdef GC_HAVE_LUCENE
lucene = new Lucene(this); // before metricDB attempts to refresh
#endif
metricDB = new MetricAggregator(this, home, zones(), hrZones()); // just to catch config updates!
metricDB->refreshMetrics();
@@ -321,7 +329,7 @@ MainWindow::MainWindow(const QDir &home) :
analButtons->setContentsMargins(0,0,0,0);
analButtons->setFocusPolicy(Qt::NoFocus);
analButtons->setAutoFillBackground(false);
analButtons->setStyleSheet("background-color: rgba( 255, 255, 255, 0% ); border: 0px;");
//XXX analButtons->setStyleSheet("background-color: rgba( 255, 255, 255, 0% ); border: 0px;");
analButtons->setLayout(toolbuttons);
analButtons->show();
@@ -449,6 +457,15 @@ MainWindow::MainWindow(const QDir &home) :
listView->setColumns(appsettings->cvalue(cyclist, GC_NAVHEADINGS).toString());
listView->setWidths(appsettings->cvalue(cyclist, GC_NAVHEADINGWIDTHS).toString());
}
#ifdef GC_HAVE_LUCENE
searchBox = new SearchBox(this);
toolbuttons->addWidget(searchBox);
//toolbuttons->addStretch();
connect(searchBox, SIGNAL(submitQuery(QString)), this, SLOT(searchSubmitted(QString)));
connect(searchBox, SIGNAL(clearQuery()), this, SLOT(searchCleared()));
#endif
// INTERVALS
intervalSummaryWindow = new IntervalSummaryWindow(this);
@@ -560,7 +577,16 @@ MainWindow::MainWindow(const QDir &home) :
currentWindow = analWindow;
// POPULATE TOOLBOX
toolBox->addItem(listView, QIcon(":images/activity.png"), "Activity History");
QWidget *activityHistory = new QWidget(this);
activityHistory->setContentsMargins(0,0,0,0);
activityHistory->setStyleSheet("padding: 0px; border: 0px; margin: 0px;");
QVBoxLayout *activityLayout = new QVBoxLayout(activityHistory);
activityLayout->setSpacing(0);
activityLayout->setContentsMargins(0,0,0,0);
activityLayout->addWidget(searchBox);
activityLayout->addWidget(listView);
toolBox->addItem(activityHistory, QIcon(":images/activity.png"), "Activity History");
toolBox->addItem(intervalSplitter, QIcon(":images/stopwatch.png"), "Activity Intervals");
toolBox->addItem(trainTool->controls(), QIcon(":images/library.png"), "Workout Library");
toolBox->addItem(masterControls, QIcon(":images/settings.png"), "Chart Settings");
@@ -2224,3 +2250,16 @@ MainWindow::setBubble(QString text, QPoint pos, Qt::Orientation orientation)
bubble->repaint();
}
}
#ifdef GC_HAVE_LUCENE
void MainWindow::searchSubmitted(QString query)
{
int count = lucene->search(query);
emit searchResults(lucene->files());
}
void MainWindow::searchCleared()
{
emit searchClear();
}
#endif

View File

@@ -62,6 +62,8 @@ class GcBubble;
class LionFullScreen;
class QTFullScreen;
class TrainTool;
class Lucene;
class SearchBox;
extern QList<MainWindow *> mainwindows; // keep track of all the MainWindows we have open
@@ -151,6 +153,10 @@ class MainWindow : public QMainWindow
QTFullScreen *fullScreen;
#endif
TrainTool *trainTool;
#ifdef GC_HAVE_LUCENE
SearchBox *searchBox;
Lucene *lucene;
#endif
// *********************************************
// APPLICATION EVENTS
@@ -207,6 +213,13 @@ class MainWindow : public QMainWindow
void rideDirty();
void rideClean();
// search
#ifdef GC_HAVE_LUCENE
void searchSubmit(QString);
void searchResults(QStringList);
void searchClear();
#endif
// realtime
void telemetryUpdate(RealtimeData rtData);
void ergFileSelected(ErgFile *);
@@ -315,6 +328,11 @@ class MainWindow : public QMainWindow
void toggleFullScreen();
#endif
#ifdef GC_HAVE_LUCENE
void searchSubmitted(QString);
void searchCleared();
#endif
protected:
static QString notesFileName(QString rideFileName);

View File

@@ -20,6 +20,7 @@
#include "DBAccess.h"
#include "RideFile.h"
#include "RideFileCache.h"
#include "Lucene.h"
#include "Zones.h"
#include "HrZones.h"
#include "Settings.h"
@@ -90,6 +91,7 @@ void MetricAggregator::refreshMetrics()
for (d = dbStatus.begin(); d != dbStatus.end(); ++d) {
if (QFile(home.absolutePath() + "/" + d.key()).exists() == false) {
dbaccess->deleteRide(d.key());
main->lucene->deleteRide(d.key());
}
}
@@ -177,6 +179,7 @@ void MetricAggregator::refreshMetrics()
// end LUW -- now syncs DB
dbaccess->connection().commit();
main->lucene->optimise();
main->isclean = true;
@@ -234,6 +237,7 @@ bool MetricAggregator::importRide(QDir path, RideFile *ride, QString fileName, u
QColor color = colorEngine->colorFor(ride->getTag("Calendar Text", ""));
dbaccess->importRide(summaryMetric, ride, color, fingerprint, modify);
main->lucene->importRide(summaryMetric, ride, color, fingerprint, modify);
delete summaryMetric;
return true;

View File

@@ -1701,6 +1701,7 @@ static void addFieldTypes(QComboBox *p)
p->addItem("Double");
p->addItem("Date");
p->addItem("Time");
p->addItem("Checkbox");
}
//

View File

@@ -316,7 +316,7 @@ RideFile *RideFileFactory::openRideFile(MainWindow *main, QFile &file,
result->setTag("Calendar Text", calendarText);
// set other "special" fields
result->setTag("Filename", file.fileName());
result->setTag("Filename", QFileInfo(file.fileName()).fileName());
result->setTag("Device", result->deviceType());
result->setTag("Athlete", QFileInfo(file).dir().dirName());
result->setTag("Year", result->startTime().toString("yyyy"));

View File

@@ -480,6 +480,12 @@ FormField::FormField(FieldDefinition field, RideMetadata *meta) : definition(fie
connect (widget, SIGNAL(timeChanged(const QTime)), this, SLOT(dataChanged()));
connect (widget, SIGNAL(editingFinished()), this, SLOT(editFinished()));
break;
case FIELD_CHECKBOX : // check
widget = new QCheckBox(this);
//widget->setFixedHeight(18);
connect(widget, SIGNAL(stateChanged(int)), this, SLOT(stateChanged(int)));
break;
}
//widget->setFont(font);
//connect(main, SIGNAL(rideSelected()), this, SLOT(rideSelected()));
@@ -504,6 +510,7 @@ FormField::~FormField()
case FIELD_DOUBLE : delete ((QDoubleSpinBox*)widget); break;
case FIELD_DATE : delete ((QDateEdit*)widget); break;
case FIELD_TIME : delete ((QTimeEdit*)widget); break;
case FIELD_CHECKBOX : delete ((QCheckBox*)widget); break;
}
if (enabled) delete enabled;
}
@@ -628,7 +635,14 @@ FormField::editFinished()
void
FormField::stateChanged(int state)
{
if (active) return; // being updated programmatically
if (active || ourRideItem == NULL) return; // being updated programmatically
// are we a checkbox -- do the simple stuff
if (definition.type == FIELD_CHECKBOX) {
ourRideItem->ride()->setTag(definition.name, ((QCheckBox *)widget)->isChecked() ? "1" : "0");
ourRideItem->setDirty(true);
return;
}
widget->setEnabled(state ? true : false);
widget->setHidden(state ? false : true);
@@ -752,6 +766,12 @@ FormField::metadataChanged()
((QTimeEdit*)widget)->setTime(time);
}
break;
case FIELD_CHECKBOX : // checkbox
{
((QCheckBox*)widget)->setChecked((value == "1") ? true : false);
}
break;
}
active = false;
}

View File

@@ -33,6 +33,7 @@
#define FIELD_DOUBLE 4
#define FIELD_DATE 5
#define FIELD_TIME 6
#define FIELD_CHECKBOX 7
class KeywordDefinition
{

View File

@@ -43,7 +43,7 @@ RideNavigator::RideNavigator(MainWindow *parent) : main(parent), active(false),
mainLayout = new QVBoxLayout(this);
mainLayout->setSpacing(0);
mainLayout->setContentsMargins(3,3,3,3);
mainLayout->setContentsMargins(0,0,0,0);
sqlModel = new QSqlTableModel(this, main->metricDB->db()->connection());
sqlModel->setTable("metrics");
@@ -52,8 +52,11 @@ RideNavigator::RideNavigator(MainWindow *parent) : main(parent), active(false),
sqlModel->select();
while (sqlModel->canFetchMore(QModelIndex())) sqlModel->fetchMore(QModelIndex());
searchFilter = new SearchFilter(this);
searchFilter->setSourceModel(sqlModel); // filter out/in search results
groupByModel = new GroupByModel(this);
groupByModel->setSourceModel(sqlModel);
groupByModel->setSourceModel(searchFilter);
sortModel = new BUGFIXQSortFilterProxyModel(this);
sortModel->setSourceModel(groupByModel);
@@ -118,6 +121,8 @@ RideNavigator::RideNavigator(MainWindow *parent) : main(parent), active(false),
connect(tableView,SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(showTreeContextMenuPopup(const QPoint &)));
connect(tableView->header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), this, SLOT(setSortBy(int,Qt::SortOrder)));
connect(main, SIGNAL(searchResults(QStringList)), this, SLOT(searchStrings(QStringList)));
connect(main, SIGNAL(searchClear()), this, SLOT(clearSearch()));
// we accept drag and drop operations
setAcceptDrops(true);
}
@@ -177,7 +182,7 @@ RideNavigator::resetView()
// add metadata fields...
SpecialFields sp; // all the special fields are in here...
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!sp.isMetric(field.name) && field.type < 5) {
if (!sp.isMetric(field.name) && (field.type < 5 || field.type == 7)) {
nameMap.insert(QString("Z%1").arg(sp.makeTechName(field.name)), field.name);
}
}
@@ -239,8 +244,17 @@ RideNavigator::resetView()
rideTreeSelectionChanged();
}
void
RideNavigator::setWidth(int x)
void RideNavigator::searchStrings(QStringList list)
{
searchFilter->setStrings(list);
}
void RideNavigator::clearSearch()
{
searchFilter->clearStrings();
}
void RideNavigator::setWidth(int x)
{
if (init == false) return;
@@ -248,7 +262,7 @@ RideNavigator::setWidth(int x)
if (tableView->verticalScrollBar()->isVisible())
x -= tableView->verticalScrollBar()->width()
+ 6 ; // !! account for content margins of 3,3,3,3
+ 0 ; // !! no longer account for content margins of 3,3,3,3 was + 6
// ** NOTE **
// When iterating over the section headings we

View File

@@ -34,6 +34,7 @@
class NavigatorCellDelegate;
class GroupByModel;
class SearchFilter;
class DiaryWindow;
class BUGFIXQSortFilterProxyModel;
@@ -119,6 +120,9 @@ class RideNavigator : public GcWindow
void resetView(); // when columns/width changes
void searchStrings(QStringList);
void clearSearch();
protected:
QSqlTableModel *sqlModel; // the sql table
GroupByModel *groupByModel; // for group by
@@ -147,6 +151,9 @@ class RideNavigator : public GcWindow
int _groupBy;
QString _columns;
QString _widths;
// search filter
SearchFilter *searchFilter;
};
//

View File

@@ -285,7 +285,7 @@ public:
.arg(groupToSourceRow.value(groups[proxyIndex.row()])->count());
returning = QVariant(returnString);
} else {
QString returnString = QString("All %1 activities")
QString returnString = QString("%1 activities")
.arg(groupToSourceRow.value(groups[proxyIndex.row()])->count());
returning = QVariant(returnString);
}
@@ -494,4 +494,61 @@ class BUGFIXQSortFilterProxyModel : public QSortFilterProxyModel
return false;
}
};
class SearchFilter : public QSortFilterProxyModel
{
Q_OBJECT
public:
SearchFilter(QWidget *p) : QSortFilterProxyModel(p), searchActive(false) {}
void setSourceModel(QAbstractItemModel *model) {
QAbstractProxyModel::setSourceModel(model);
this->model = model;
// find the filename column
fileIndex = -1;
for(int i=0; i<model->columnCount(); i++) {
if (model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString() == "filename") {
fileIndex = i;
}
}
}
bool filterAcceptsRow (int source_row, const QModelIndex &source_parent) const {
if (fileIndex == -1 || searchActive == false) return true; // nothing to do
// lets get the filename
QModelIndex source_index = model->index(source_row, fileIndex, source_parent);
if (!source_index.isValid()) return true;
QString key = model->data(source_index, Qt::DisplayRole).toString();
return strings.contains(key);
}
public slots:
void setStrings(QStringList list) {
beginResetModel();
strings = list;
searchActive = true;
endResetModel();
}
void clearStrings() {
beginResetModel();
strings.clear();
searchActive = false;
endResetModel();
}
private:
QAbstractItemModel *model;
QStringList strings;
int fileIndex;
bool searchActive;
};
#endif

126
src/SearchBox.cpp Normal file
View File

@@ -0,0 +1,126 @@
/*
* Copyright (c) 2012 Mark Liversedge (liversedge@gmail.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "SearchBox.h"
#include <QToolButton>
#include <QStyle>
#include <QDebug>
SearchBox::SearchBox(QWidget *parent)
: QLineEdit(parent)
{
//clear button
clearButton = new QToolButton(this);
QPixmap pixmap(":images/toolbar/clear.png");
clearButton->setIcon(QIcon(pixmap));
clearButton->setIconSize(pixmap.size());
clearButton->setCursor(Qt::ArrowCursor);
clearButton->setStyleSheet("QToolButton { border: none; padding: 0px; }");
clearButton->hide();
connect(clearButton, SIGNAL(clicked()), this, SLOT(clear()));
connect(clearButton, SIGNAL(clicked()), this, SIGNAL(clearQuery()));
connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(updateCloseButton(const QString&)));
// search button
searchButton = new QToolButton(this);
QPixmap search(":images/toolbar/search.png");
searchButton->setIcon(QIcon(search));
searchButton->setIconSize(search.size());
searchButton->setCursor(Qt::ArrowCursor);
searchButton->setStyleSheet("QToolButton { border: none; padding: 0px; }");
connect(searchButton, SIGNAL(clicked()), this, SLOT(searchSubmit()));
int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
setStyleSheet(QString( //"QLineEdit { padding-right: %1px; } "
"QLineEdit {"
" selection-color: white; "
//" border: 0px groove gray;"
" border-radius: 5px;"
" padding: 0px %1px;"
"}"
"QLineEdit:focus {"
" selection-color: white; "
//" border: 0px groove gray;"
" border-radius: 5px;"
" padding: 0px %1px;"
"}"
""
"QLineEdit:edit-focus {"
" selection-color: white; "
//" border: 0px groove gray;"
" border-radius: 5px;"
" padding: 0px %1px;"
"}"
).arg(clearButton->sizeHint().width() + frameWidth + 1));
QSize msz = minimumSizeHint();
setMinimumSize(qMax(msz.width(), clearButton->sizeHint().height() + frameWidth * 2 + 2),
qMax(msz.height(), clearButton->sizeHint().height() /* + frameWidth * 2 + -2*/));
setPlaceholderText("Search...");
setDragEnabled(true);
connect(this, SIGNAL(returnPressed()), this, SLOT(searchSubmit()));
}
void SearchBox::resizeEvent(QResizeEvent *)
{
QSize sz = clearButton->sizeHint();
int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
clearButton->move(rect().right() - frameWidth - sz.width(),
(rect().bottom() + 1 - sz.height())/2);
searchButton->move(rect().left() + frameWidth,
(rect().bottom() + 1 - sz.height())/2);
}
void SearchBox::updateCloseButton(const QString& text)
{
if (clearButton->isVisible() && text.isEmpty()) clearQuery();
clearButton->setVisible(!text.isEmpty());
}
void SearchBox::searchSubmit()
{
// return hit / key pressed
if (text() != "") {
submitQuery(text());
}
}
// Drag and drop columns from the chooser...
void
SearchBox::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->data("application/x-columnchooser") != "")
event->acceptProposedAction(); // whatever you wanna drop we will try and process!
else
event->ignore();
}
void
SearchBox::dropEvent(QDropEvent *event)
{
QString name = event->mimeData()->data("application/x-columnchooser");
// fugly, but it works for BikeScore with the (TM) in it...
if (name == "BikeScore?") name = QString("BikeScore&#8482;").replace("&#8482;", QChar(0x2122));
// we do very little to the name, just space to _ and lower case it for now...
name.replace(' ', '_');
insert(name + ":\"\"");
}

54
src/SearchBox.h Normal file
View File

@@ -0,0 +1,54 @@
/*
* Copyright (c) 2012 Mark Liversedge (liversedge@gmail.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _GC_SearchBox_h
#define _GC_SearchBox_h
#include <QLineEdit>
#include <QDragEnterEvent>
#include <QDropEvent>
class QToolButton;
class SearchBox : public QLineEdit
{
Q_OBJECT
public:
SearchBox(QWidget *parent = 0);
protected:
void resizeEvent(QResizeEvent *);
private slots:
void updateCloseButton(const QString &text);
void searchSubmit();
// drop column headings from column chooser
void dragEnterEvent(QDragEnterEvent *event);
void dropEvent(QDropEvent *event);
signals:
void submitQuery(QString);
void clearQuery();
private:
QToolButton *clearButton, *searchButton;
};
#endif

View File

@@ -21,6 +21,8 @@
<file>images/toolbar/main/home.png</file>
<file>images/toolbar/main/measures.png</file>
<file>images/toolbar/main/train.png</file>
<file>images/toolbar/clear.png</file>
<file>images/toolbar/search.png</file>
<file>images/maps/cycling_feed.png</file>
<file>images/maps/loop.png</file>
<file>images/maps/cycling.png</file>
@@ -85,7 +87,6 @@
<file>images/oxygen/open.png</file>
<file>images/toolbar/close-icon.png</file>
<file>images/toolbar/save.png</file>
<file>images/toolbar/search.png</file>
<file>images/toolbar/splash green.png</file>
<file>images/toolbar/cut.png</file>
<file>images/toolbar/copy.png</file>

View File

@@ -168,6 +168,13 @@ BOOST_INCLUDE =
#VLC_INCLUDE =
#VLC_LIBS =
#If you want search functionality then uncomment the following
#two lines once you habve installed clucene developer libraries
#and runtimes. See the INSTALL guide for your platform.
#CLUCENE_INCLUDE = /usr/include/CLucene
#CLUCENE_LIBS = -lclucene
# *** Mac users NOTE ***
# On MAC you don't need libvlc since we use the
# native QTKit (OSX framework) for video playback

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -4,6 +4,7 @@ include( gcconfig.pri )
TEMPLATE = app
TARGET = GoldenCheetah
!isEmpty( APP_NAME ) { TARGET = $${APP_NAME} }
DEPENDPATH += .
@@ -117,6 +118,14 @@ LIBS += -lm $${LIBZ_LIBS}
}
}
!isEmpty( CLUCENE_LIBS ) {
INCLUDEPATH += $${CLUCENE_INCLUDE}
LIBS += $${CLUCENE_LIBS}
DEFINES += GC_HAVE_LUCENE
HEADERS += Lucene.h SearchBox.h
SOURCES += Lucene.cpp SearchBox.cpp
}
# Mac specific build for
# Segmented mac style button (but not used at present)
# Video playback using Quicktime Framework