From 04cdfce782f2a104ab5d0ee92de67c7b6271835b Mon Sep 17 00:00:00 2001 From: Maximilian Güntner Date: Sat, 2 Nov 2013 02:20:02 +0100 Subject: Added a custom widget for tagging dives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A custom tag widget has been added to MainTab. Tags are seperated by a comma ",". The implementation supports escaping a comma by using "\,". While typing, the widget supports the user by suggesting tags using a QCompleter. Signed-off-by: Maximilian Güntner --- qt-ui/completionmodels.cpp | 13 +++ qt-ui/completionmodels.h | 7 ++ qt-ui/groupedlineedit.cpp | 204 +++++++++++++++++++++++++++++++++++++++++++++ qt-ui/groupedlineedit.h | 66 +++++++++++++++ qt-ui/maintab.cpp | 40 ++++++++- qt-ui/maintab.h | 4 + qt-ui/maintab.ui | 121 +++++++++++++++++---------- qt-ui/tagwidget.cpp | 135 ++++++++++++++++++++++++++++++ qt-ui/tagwidget.h | 26 ++++++ subsurface.pro | 10 ++- 10 files changed, 579 insertions(+), 47 deletions(-) create mode 100644 qt-ui/groupedlineedit.cpp create mode 100644 qt-ui/groupedlineedit.h create mode 100644 qt-ui/tagwidget.cpp create mode 100644 qt-ui/tagwidget.h diff --git a/qt-ui/completionmodels.cpp b/qt-ui/completionmodels.cpp index 8bd4f5441..31733addb 100644 --- a/qt-ui/completionmodels.cpp +++ b/qt-ui/completionmodels.cpp @@ -12,6 +12,7 @@ CREATE_SINGLETON(BuddyCompletionModel); CREATE_SINGLETON(DiveMasterCompletionModel); CREATE_SINGLETON(LocationCompletionModel); CREATE_SINGLETON(SuitCompletionModel); +CREATE_SINGLETON(TagCompletionModel); #undef CREATE_SINGLETON @@ -35,3 +36,15 @@ CREATE_UPDATE_METHOD(DiveMasterCompletionModel, divemaster); CREATE_UPDATE_METHOD(LocationCompletionModel, location); CREATE_UPDATE_METHOD(SuitCompletionModel, suit); +void TagCompletionModel::updateModel() +{ + if(g_tag_list == NULL) + return; + QStringList list; + struct tag_entry *current_tag_entry = g_tag_list->next; + while (current_tag_entry != NULL) { + list.append(QString(current_tag_entry->tag->name)); + current_tag_entry = current_tag_entry->next; + } + setStringList(list); +} diff --git a/qt-ui/completionmodels.h b/qt-ui/completionmodels.h index e4f1770e2..146186531 100644 --- a/qt-ui/completionmodels.h +++ b/qt-ui/completionmodels.h @@ -31,4 +31,11 @@ public: void updateModel(); }; +class TagCompletionModel : public QStringListModel { + Q_OBJECT +public: + static TagCompletionModel* instance(); + void updateModel(); +}; + #endif diff --git a/qt-ui/groupedlineedit.cpp b/qt-ui/groupedlineedit.cpp new file mode 100644 index 000000000..7502db5ce --- /dev/null +++ b/qt-ui/groupedlineedit.cpp @@ -0,0 +1,204 @@ +/* + * This file is part of the Nepomuk widgets collection + * Copyright (c) 2013 Denis Steckelmacher + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License version 2.1 as published by the Free Software Foundation, + * or 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 "groupedlineedit.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GroupedLineEdit::Private +{ + struct Block { + int start; + int end; + QString text; + }; + QVector blocks; + QVector colors; +}; + +GroupedLineEdit::GroupedLineEdit(QWidget* parent) +: QPlainTextEdit(parent), + d(new Private) +{ + setWordWrapMode(QTextOption::NoWrap); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + document()->setMaximumBlockCount(1); +} + + +GroupedLineEdit::~GroupedLineEdit() +{ + delete d; +} + +QString GroupedLineEdit::text() const +{ + // Remove the block crosses from the text + return toPlainText(); +} + +int GroupedLineEdit::cursorPosition() const +{ + return textCursor().positionInBlock(); +} + +void GroupedLineEdit::addBlock(int start, int end) +{ + Private::Block block; + + block.start = start; + block.end = end; + block.text = text().mid(start, end-start+1).trimmed(); + d->blocks.append(block); + viewport()->update(); +} + +void GroupedLineEdit::addColor(QColor color) +{ + d->colors.append(color); +} + +void GroupedLineEdit::removeAllColors() +{ + d->colors.clear(); +} + +QStringList GroupedLineEdit::getBlockStringList() +{ + QStringList retList; + Private::Block block; + foreach(block, d->blocks) + retList.append(block.text); + return retList; +} + +void GroupedLineEdit::setCursorPosition(int position) +{ + QTextCursor c = textCursor(); + + c.setPosition(position, QTextCursor::MoveAnchor); + + setTextCursor(c); +} + +void GroupedLineEdit::setText(const QString &text) +{ + setPlainText(text); +} + +void GroupedLineEdit::clear() +{ + QPlainTextEdit::clear(); + removeAllBlocks(); +} + +void GroupedLineEdit::selectAll() +{ + QTextCursor c = textCursor(); + + c.select(QTextCursor::LineUnderCursor); + + setTextCursor(c); +} + +void GroupedLineEdit::removeAllBlocks() +{ + d->blocks.clear(); + viewport()->update(); +} + +QSize GroupedLineEdit::sizeHint() const +{ + QSize rs( + 40, + document()->findBlock(0).layout()->lineAt(0).height() + + document()->documentMargin() * 2 + + frameWidth() * 2 + ); + + return rs; +} + +QSize GroupedLineEdit::minimumSizeHint() const +{ + return sizeHint(); +} + +void GroupedLineEdit::keyPressEvent(QKeyEvent *e) +{ + switch (e->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + emit editingFinished(); + return; + } + + QPlainTextEdit::keyPressEvent(e); +} + +void GroupedLineEdit::paintEvent(QPaintEvent *e) +{ + + QTextLine line = document()->findBlock(0).layout()->lineAt(0); + QPainter painter(viewport()); + + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::HighQualityAntialiasing, true); + + painter.fillRect(0, 0, viewport()->width(), viewport()->height(), palette().base()); + + QVectorIterator i(d->colors); + i.toFront(); + foreach (const Private::Block &block, d->blocks) { + qreal start_x = line.cursorToX(block.start, QTextLine::Trailing); + qreal end_x = line.cursorToX(block.end + 1, QTextLine::Leading); + QPainterPath path; + QRectF rectangle( + start_x - 1.0 - double(horizontalScrollBar()->value()), + 1.0, + end_x - start_x + 2.0, + double(viewport()->height() - 2) + ); + if (! i.hasNext()) + i.toFront(); + path.addRoundedRect(rectangle, 5.0, 5.0); + painter.setPen(i.peekNext()); + painter.setBrush(i.next().lighter(180)); + painter.drawPath(path); + } + + QPlainTextEdit::paintEvent(e); +} diff --git a/qt-ui/groupedlineedit.h b/qt-ui/groupedlineedit.h new file mode 100644 index 000000000..327932c9d --- /dev/null +++ b/qt-ui/groupedlineedit.h @@ -0,0 +1,66 @@ +/* Original License: + * + * This file is part of the Nepomuk widgets collection + * Copyright (c) 2013 Denis Steckelmacher + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License version 2.1 as published by the Free Software Foundation, + * or 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 __GROUPEDLINEEDIT_H__ +#define __GROUPEDLINEEDIT_H__ + +#include +#include + +class GroupedLineEdit : public QPlainTextEdit +{ + Q_OBJECT + +public: + explicit GroupedLineEdit(QWidget *parent = 0); + virtual ~GroupedLineEdit(); + + QString text() const; + + int cursorPosition() const; + void setCursorPosition(int position); + void setText(const QString &text); + void clear(); + void selectAll(); + + void removeAllBlocks(); + void addBlock(int start, int end); + QStringList getBlockStringList(); + + void addColor(QColor color); + void removeAllColors(); + + virtual QSize sizeHint() const; + virtual QSize minimumSizeHint() const; + +signals: + void editingFinished(); + +protected: + virtual void paintEvent(QPaintEvent *e); + virtual void keyPressEvent(QKeyEvent *e); + +private: + struct Private; + Private *d; +}; +#endif diff --git a/qt-ui/maintab.cpp b/qt-ui/maintab.cpp index ddcdc6c63..b7c0f15c7 100644 --- a/qt-ui/maintab.cpp +++ b/qt-ui/maintab.cpp @@ -58,6 +58,7 @@ MainTab::MainTab(QWidget *parent) : QTabWidget(parent), ui.airtemp->installEventFilter(this); ui.watertemp->installEventFilter(this); ui.dateTimeEdit->installEventFilter(this); + ui.tagWidget->installEventFilter(this); QList statisticsTabWidgets = ui.statisticsTab->children(); Q_FOREACH(QObject* obj, statisticsTabWidgets) { @@ -87,10 +88,12 @@ MainTab::MainTab(QWidget *parent) : QTabWidget(parent), completers.divemaster = new QCompleter(DiveMasterCompletionModel::instance(), ui.divemaster); completers.location = new QCompleter(LocationCompletionModel::instance(), ui.location); completers.suit = new QCompleter(SuitCompletionModel::instance(), ui.suit); + completers.tags = new QCompleter(TagCompletionModel::instance(), ui.tagWidget); ui.buddy->setCompleter(completers.buddy); ui.divemaster->setCompleter(completers.divemaster); ui.location->setCompleter(completers.location); ui.suit->setCompleter(completers.suit); + ui.tagWidget->setCompleter(completers.tags); setMinimumHeight(0); setMinimumWidth(0); @@ -161,6 +164,9 @@ void MainTab::enableEdition(EditMode newEditMode) notesBackup[mydive].airtemp = get_temperature_string(mydive->airtemp, true); notesBackup[mydive].watertemp = get_temperature_string(mydive->watertemp, true); notesBackup[mydive].datetime = QDateTime::fromTime_t(mydive->when - gettimezoneoffset()).toString(QString("M/d/yy h:mm")); + char buf[1024]; + taglist_get_tagstring(mydive->tag_list, buf, 1024); + notesBackup[mydive].tags = QString(buf); // maybe this is a place for memset? for (int i = 0; i < MAX_CYLINDERS; i++) { @@ -182,7 +188,9 @@ bool MainTab::eventFilter(QObject* object, QEvent* event) enableEdition(); } - if (isEnabled() && event->type() == QEvent::FocusIn && (object == ui.rating || object == ui.visibility)) { + if (isEnabled() && event->type() == QEvent::FocusIn && (object == ui.rating || + object == ui.visibility || + object == ui.tagWidget)) { tabBar()->setTabIcon(currentIndex(), QIcon(":warning")); enableEdition(); } @@ -215,6 +223,7 @@ void MainTab::clearInfo() ui.airTemperatureText->clear(); ui.airPressureText->clear(); ui.salinityText->clear(); + ui.tagWidget->clear(); } void MainTab::clearStats() @@ -356,6 +365,11 @@ void MainTab::updateDiveInfo(int dive) ui.timeLimits->setMaximum(get_time_string(stats_selection.longest_time.seconds, 0)); ui.timeLimits->setMinimum(get_time_string(stats_selection.shortest_time.seconds, 0)); + + char buf[1024]; + taglist_get_tagstring(d->tag_list, buf, 1024); + ui.tagWidget->setText(QString(buf)); + multiEditEquipmentPlaceholder = *d; cylindersModel->setDive(&multiEditEquipmentPlaceholder); weightModel->setDive(&multiEditEquipmentPlaceholder); @@ -393,6 +407,7 @@ void MainTab::reload() BuddyCompletionModel::instance()->updateModel(); LocationCompletionModel::instance()->updateModel(); DiveMasterCompletionModel::instance()->updateModel(); + TagCompletionModel::instance()->updateModel(); } void MainTab::acceptChanges() @@ -423,7 +438,8 @@ void MainTab::acceptChanges() notesBackup[curr].airtemp != ui.airtemp->text() || notesBackup[curr].watertemp != ui.watertemp->text() || notesBackup[curr].datetime != ui.dateTimeEdit->dateTime().toString(QString("M/d/yy h:mm")) || - notesBackup[curr].visibility != ui.rating->currentStars()) { + notesBackup[curr].visibility != ui.rating->currentStars() || + notesBackup[curr].tags != ui.tagWidget->text()) { mark_divelist_changed(TRUE); } if (notesBackup[curr].location != ui.location->text() || @@ -431,6 +447,9 @@ void MainTab::acceptChanges() mainWindow()->globe()->reload(); } + if (notesBackup[curr].tags != ui.tagWidget->text()) + saveTags(); + if (cylindersModel->changed) { mark_divelist_changed(TRUE); Q_FOREACH (dive *d, notesBackup.keys()) { @@ -480,6 +499,7 @@ void MainTab::resetPallete() ui.airtemp->setPalette(p); ui.watertemp->setPalette(p); ui.dateTimeEdit->setPalette(p); + ui.tagWidget->setPalette(p); } #define EDIT_TEXT2(what, text) \ @@ -522,6 +542,7 @@ void MainTab::rejectChanges() ui.airtemp->setText(notesBackup[curr].airtemp); ui.watertemp->setText(notesBackup[curr].watertemp); ui.dateTimeEdit->setDateTime(QDateTime::fromString(notesBackup[curr].datetime, QString("M/d/y h:mm"))); + ui.tagWidget->setText(notesBackup[curr].tags); struct dive *mydive; for (int i = 0; i < dive_table.nr; i++) { @@ -628,6 +649,21 @@ void MainTab::on_dateTimeEdit_dateTimeChanged(const QDateTime& datetime) markChangedWidget(ui.dateTimeEdit); } +void MainTab::saveTags() +{ + EDIT_SELECTED_DIVES( + QString tag; + taglist_clear(mydive->tag_list); + foreach (tag, ui.tagWidget->getBlockStringList()) + taglist_add_tag(mydive->tag_list, tag.toAscii().data()); + ); +} + +void MainTab::on_tagWidget_textChanged() +{ + markChangedWidget(ui.tagWidget); +} + void MainTab::on_location_textChanged(const QString& text) { if (editMode == NONE) diff --git a/qt-ui/maintab.h b/qt-ui/maintab.h index afc30a079..22a705530 100644 --- a/qt-ui/maintab.h +++ b/qt-ui/maintab.h @@ -31,6 +31,7 @@ struct NotesBackup{ int rating; int visibility; QString divemaster; + QString tags; cylinder_t cylinders[MAX_CYLINDERS]; weightsystem_t weightsystem[MAX_WEIGHTSYSTEMS ]; }; @@ -40,6 +41,7 @@ struct Completers{ QCompleter *divemaster; QCompleter *buddy; QCompleter *suit; + QCompleter *tags; }; class MainTab : public QTabWidget @@ -73,6 +75,7 @@ public slots: void on_dateTimeEdit_dateTimeChanged(const QDateTime& datetime); void on_rating_valueChanged(int value); void on_visibility_valueChanged(int value); + void on_tagWidget_textChanged(); void editCylinderWidget(const QModelIndex& index); void editWeightWidget(const QModelIndex& index); void addDiveStarted(); @@ -94,6 +97,7 @@ private: Completers completers; void resetPallete(); + void saveTags(); QString printGPSCoords(int lat, int lon); }; diff --git a/qt-ui/maintab.ui b/qt-ui/maintab.ui index 3685f19ac..3e12d4c47 100644 --- a/qt-ui/maintab.ui +++ b/qt-ui/maintab.ui @@ -37,8 +37,8 @@ 0 0 - 513 - 468 + 501 + 535 @@ -102,13 +102,6 @@ - - - - Location - - - @@ -116,10 +109,17 @@ - - + + - Coordinates + Location + + + + + + + Tags @@ -130,10 +130,17 @@ - - + + + + false + + + + + - Divemaster + Coordinates @@ -144,15 +151,22 @@ - - + + false - - + + + + Divemaster + + + + + false @@ -176,6 +190,13 @@ + + + + false + + + @@ -193,34 +214,45 @@ - - - - false - - - - + Notes - - - - false - - - - + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + 0 + 0 + + + + + 0 + 0 + + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOff + + + QPlainTextEdit::NoWrap + + + @@ -248,8 +280,8 @@ 0 0 - 513 - 468 + 515 + 473 @@ -335,8 +367,8 @@ 0 0 - 513 - 468 + 515 + 473 @@ -584,7 +616,7 @@ - + Salinity @@ -642,8 +674,8 @@ 0 0 - 513 - 468 + 515 + 473 @@ -802,6 +834,11 @@
tableview.h
1 + + TagWidget + QPlainTextEdit +
qt-ui/tagwidget.h
+
diff --git a/qt-ui/tagwidget.cpp b/qt-ui/tagwidget.cpp new file mode 100644 index 000000000..bcefe5704 --- /dev/null +++ b/qt-ui/tagwidget.cpp @@ -0,0 +1,135 @@ +#include "tagwidget.h" +#include +#include + +TagWidget::TagWidget(QWidget *parent) : GroupedLineEdit(parent), m_completer(NULL) +{ + connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(reparse())); + + addColor(QColor(0x00, 0xAE, 0xFF)); + addColor(QColor(0x00, 0x78, 0xB0)); +} + +void TagWidget::setCompleter(QCompleter *completer) +{ + m_completer = completer; + m_completer->setWidget(this); + connect(m_completer, SIGNAL(activated(QString)), this, SLOT(completionSelected(QString))); +} + +QPair TagWidget::getCursorTagPosition() { + int i = 0, start = 0, end = 0; + /* Parse string near cursor */ + i = cursorPosition(); + while (--i > 0) { + if (text().at(i) == ',') { + if (i > 0 && text().at(i-1) != '\\') { + i++; + break; + } + } + } + start = i; + while (++i < text().length()) { + if (text().at(i) == ',') { + if (i > 0 && text().at(i-1) != '\\') + break; + } + } + end = i; + if (start < 0 || end < 0) { + start = 0; + end = 0; + } + return QPair(start,end); +} + +enum ParseState {FINDSTART, FINDEND}; + +void TagWidget::highlight() { + int i = 0, start = 0, end = 0; + ParseState state = FINDEND; + removeAllBlocks(); + + while(i < text().length()) { + if (text().at(i) == ',') { + if (state == FINDSTART) { + /* Detect empty tags */ + } else if (state == FINDEND) { + /* Found end of tag */ + if (i > 1) { + if(text().at(i-1) != '\\') { + addBlock(start, end); + state = FINDSTART; + } + } else { + state = FINDSTART; + } + } + } else if (text().at(i) == ' ') { + /* Handled */ + } else { + /* Found start of tag */ + if (state == FINDSTART) { + state = FINDEND; + start = i; + } else if (state == FINDEND) { + end = i; + } + } + i++; + } + if (state == FINDEND) { + if (end < start) + end = text().length()-1; + if (text().length() > 0) + addBlock(start, end); + } +} + +void TagWidget::reparse() +{ + highlight(); + QPair pos = getCursorTagPosition(); + QString currentText; + if (pos.first >= 0 && pos.second > 0) + currentText = text().mid(pos.first, pos.second-pos.first).trimmed(); + else + currentText = ""; + if (m_completer) { + m_completer->setCompletionPrefix(currentText); + m_completer->complete(); + } +} + +void TagWidget::completionSelected(QString completion) { + QPair pos; + pos = getCursorTagPosition(); + if (pos.first >= 0 && pos.second > 0) { + setText(text().remove(pos.first, pos.second-pos.first).insert(pos.first, completion)); + setCursorPosition(pos.first+completion.length()); + } + else { + setText(completion.append(", ")); + setCursorPosition(text().length()); + } +} + +void TagWidget::setCursorPosition(int position) { + blockSignals(true); + GroupedLineEdit::setCursorPosition(position); + blockSignals(false); +} + +void TagWidget::setText(QString text) { + blockSignals(true); + GroupedLineEdit::setText(text); + blockSignals(false); + highlight(); +} + +void TagWidget::clear() { + blockSignals(true); + GroupedLineEdit::clear(); + blockSignals(false); +} diff --git a/qt-ui/tagwidget.h b/qt-ui/tagwidget.h new file mode 100644 index 000000000..ff19d7283 --- /dev/null +++ b/qt-ui/tagwidget.h @@ -0,0 +1,26 @@ +#ifndef __TAGWIDGET_H +#define __TAGWIDGET_H + +#include "groupedlineedit.h" +#include +#include + +class TagWidget : public GroupedLineEdit +{ + Q_OBJECT +public: + explicit TagWidget(QWidget *parent = 0); + void setCompleter(QCompleter *completer); + QPair getCursorTagPosition(); + void highlight(); + void setText(QString text); + void clear(); + void setCursorPosition(int position); +public slots: + void reparse(); + void completionSelected(QString); +private: + QCompleter *m_completer; +}; + +#endif /* __TAGWIDGET_H */ diff --git a/subsurface.pro b/subsurface.pro index 15caa8f99..fb861d52c 100644 --- a/subsurface.pro +++ b/subsurface.pro @@ -56,7 +56,9 @@ HEADERS = \ subsurfacestartup.h \ uemis.h \ webservice.h \ - qt-ui/csvimportdialog.h + qt-ui/csvimportdialog.h \ + qt-ui/tagwidget.h \ + qt-ui/groupedlineedit.h SOURCES = \ deco.c \ @@ -103,7 +105,9 @@ SOURCES = \ time.c \ uemis.c \ uemis-downloader.c \ - qt-ui/csvimportdialog.cpp + qt-ui/csvimportdialog.cpp \ + qt-ui/tagwidget.cpp \ + qt-ui/groupedlineedit.cpp linux*: SOURCES += linux.c mac: SOURCES += macos.c @@ -121,7 +125,7 @@ FORMS = \ qt-ui/renumber.ui \ qt-ui/webservices.ui \ qt-ui/tableview.ui \ - qt-ui/csvimportdialog.ui + qt-ui/csvimportdialog.ui RESOURCES = subsurface.qrc -- cgit v1.2.3-70-g09d2