From: Sebastian Trueg Date: Fri, 21 Mar 2008 21:05:05 +0000 (+0000) Subject: This is the first step towards a better looking and more usable metadata GUI. X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/commitdiff_plain/d3a04321886e8ca39ab91a647a9547ebe4d52154 This is the first step towards a better looking and more usable metadata GUI. - A nicer comment widget shows a popup to edit the comment. - A tag cloud replaces the ugly tagwidget from libnepomuk. The plan is to use Dolphin as a testbed to optimize the look and then move at least the tagcloud to libnepomuk to make it available for all apps since this is a common feature. So please test it and provide feedback. The layout is still cluttered. So we also need feedback on that. And of course on the usability. Apart from the GUI Dolphin now uses the mass metadata update job to perform metadata updates on many files in an async KJob without blocking the GUI. This is another candidate for public API at some point. svn path=/trunk/KDE/kdebase/apps/; revision=788565 --- diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6af8fc9f3..0354f925d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,7 @@ add_subdirectory( tests ) macro_optional_find_package(Soprano) include_directories( ${KDE4_INCLUDE_DIR} ${QT_INCLUDES} ${BLITZ_INCLUDES} ) + if (Soprano_FOUND) include_directories( ${SOPRANO_INCLUDE_DIR} ) endif (Soprano_FOUND) @@ -27,7 +28,6 @@ set(dolphinprivate_LIB_SRCS dolphinview.cpp dolphinviewactionhandler.cpp iconmanager.cpp - ratingpainter.cpp renamedialog.cpp selectiontoggle.cpp selectionmanager.cpp @@ -103,6 +103,8 @@ set(dolphin_SRCS infosidebarpage.cpp main.cpp metadatawidget.cpp + commentwidget.cpp + commenteditwidget.cpp metatextlabel.cpp pixmapviewer.cpp settingspagebase.cpp @@ -117,6 +119,19 @@ set(dolphin_SRCS viewsettingspage.cpp viewpropsprogressinfo.cpp ) +if(Nepomuk_FOUND) +set(dolphin_SRCS + ${dolphin_SRCS} + nepomukmassupdatejob.cpp + tagcloud/tagcloud.cpp + tagcloud/resourcetaggingwidget.cpp + tagcloud/taggingpopup.cpp + tagcloud/newtagdialog.cpp +) +kde4_add_ui_files(dolphin_SRCS tagcloud/newtagdialog.ui) + +endif(Nepomuk_FOUND) + if(NOT WIN32) set(dolphin_SRCS ${dolphin_SRCS} terminalsidebarpage.cpp) endif(NOT WIN32) diff --git a/src/commenteditwidget.cpp b/src/commenteditwidget.cpp new file mode 100644 index 000000000..3b98d44d7 --- /dev/null +++ b/src/commenteditwidget.cpp @@ -0,0 +1,241 @@ +/*************************************************************************** + * Copyright (C) 2008 by Sebastian Trueg * + * * + * 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 "commenteditwidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + + +class CommentEditWidget::Private +{ +public: + Private( CommentEditWidget* parent ) + : eventLoop( 0 ), + q( parent ) { + } + + QEventLoop* eventLoop; + bool success; + QTextEdit* textEdit; + QToolButton* buttonSave; + QToolButton* buttonCancel; + + QString comment; + + QRect geometryForPopupPos( const QPoint& p ) { + QSize size = q->sizeHint(); + + // we want a little margin + const int margin = KDialog::marginHint(); + size.setHeight( size.height() + margin*2 ); + size.setWidth( size.width() + margin*2 ); + + QRect screen = QApplication::desktop()->screenGeometry( QApplication::desktop()->screenNumber( p ) ); + + // calculate popup position + QPoint pos( p.x() - size.width()/2, p.y() - size.height()/2 ); + + // ensure we do not leave the desktop + if ( pos.x() + size.width() > screen.right() ) { + pos.setX( screen.right() - size.width() ); + } + else if ( pos.x() < screen.left() ) { + pos.setX( screen.left() ); + } + + if ( pos.y() + size.height() > screen.bottom() ) { + pos.setY( screen.bottom() - size.height() ); + } + else if ( pos.y() < screen.top() ) { + pos.setY( screen.top() ); + } + + return QRect( pos, size ); + } + + void _k_saveClicked(); + void _k_cancelClicked(); + +private: + CommentEditWidget* q; +}; + + +void CommentEditWidget::Private::_k_saveClicked() +{ + comment = textEdit->toPlainText(); + success = true; + q->hide(); +} + + +void CommentEditWidget::Private::_k_cancelClicked() +{ + success = false; + q->hide(); +} + + +CommentEditWidget::CommentEditWidget( QWidget* parent ) + : QFrame( parent ), + d( new Private( this ) ) +{ + setFrameStyle( QFrame::Box|QFrame::Plain ); + setWindowFlags( Qt::Popup ); + + d->textEdit = new QTextEdit( this ); + d->textEdit->installEventFilter( this ); + QVBoxLayout* layout = new QVBoxLayout( this ); + layout->setMargin( 0 ); + layout->addWidget( d->textEdit ); + + d->buttonSave = new QToolButton( d->textEdit ); + d->buttonCancel = new QToolButton( d->textEdit ); + d->buttonSave->setToolButtonStyle( Qt::ToolButtonTextBesideIcon ); + d->buttonCancel->setToolButtonStyle( Qt::ToolButtonTextBesideIcon ); + d->buttonSave->setAutoRaise( true ); + d->buttonCancel->setAutoRaise( true ); + d->buttonSave->setIcon( KIcon( "document-save" ) ); + d->buttonCancel->setIcon( KIcon( "edit-delete" ) ); + d->buttonSave->setText( i18n( "Save" ) ); + d->buttonCancel->setText( i18n( "Cancel" ) ); + + QFont fnt( font() ); + fnt.setPointSize( fnt.pointSize()-2 ); + d->buttonSave->setFont( fnt ); + d->buttonCancel->setFont( fnt ); + + connect( d->buttonSave, SIGNAL(clicked()), + this, SLOT( _k_saveClicked() ) ); + connect( d->buttonCancel, SIGNAL(clicked()), + this, SLOT( _k_cancelClicked() ) ); +} + + +CommentEditWidget::~CommentEditWidget() +{ + delete d; +} + + +void CommentEditWidget::setComment( const QString& s ) +{ + d->comment = s; +} + + +QString CommentEditWidget::comment() +{ + return d->comment; +} + + +bool CommentEditWidget::exec( const QPoint& pos ) +{ + d->success = false; + d->textEdit->setText( d->comment ); + d->textEdit->setFocus(); + d->textEdit->moveCursor( QTextCursor::End ); + QEventLoop eventLoop; + d->eventLoop = &eventLoop; + setGeometry( d->geometryForPopupPos( pos ) ); + show(); + + QPointer guard = this; + (void) eventLoop.exec(); + if ( !guard.isNull() ) + d->eventLoop = 0; + return d->success; +} + + +void CommentEditWidget::mousePressEvent( QMouseEvent* e ) +{ + // clicking outside of the widget means cancel + if ( !rect().contains( e->pos() ) ) { + d->success = false; + hide(); + } + else { + QWidget::mousePressEvent( e ); + } +} + + +void CommentEditWidget::hideEvent( QHideEvent* e ) +{ + Q_UNUSED( e ); + if ( d->eventLoop ) { + d->eventLoop->exit(); + } +} + + +void CommentEditWidget::updateButtons() +{ + QSize sbs = d->buttonSave->sizeHint(); + QSize cbs = d->buttonCancel->sizeHint(); + + // FIXME: button order + d->buttonCancel->setGeometry( QRect( QPoint( d->textEdit->width() - cbs.width() - frameWidth(), + d->textEdit->height() - cbs.height() - frameWidth() ), + cbs ) ); + d->buttonSave->setGeometry( QRect( QPoint( d->textEdit->width() - cbs.width() - sbs.width() - frameWidth(), + d->textEdit->height() - sbs.height() - frameWidth() ), + sbs ) ); +} + + +void CommentEditWidget::resizeEvent( QResizeEvent* e ) +{ + QWidget::resizeEvent( e ); + updateButtons(); +} + + +bool CommentEditWidget::eventFilter( QObject* watched, QEvent* event ) +{ + if ( watched == d->textEdit && event->type() == QEvent::KeyPress ) { + QKeyEvent* ke = static_cast( event ); + kDebug() << "keypress:" << ke->key() << ke->modifiers(); + if ( ( ke->key() == Qt::Key_Enter || + ke->key() == Qt::Key_Return ) && + ke->modifiers() & Qt::ControlModifier ) { + d->_k_saveClicked(); + return true; + } + } + + return QFrame::eventFilter( watched, event ); +} + +#include "commenteditwidget.moc" diff --git a/src/commenteditwidget.h b/src/commenteditwidget.h new file mode 100644 index 000000000..18ab8d7b2 --- /dev/null +++ b/src/commenteditwidget.h @@ -0,0 +1,62 @@ +/*************************************************************************** + * Copyright (C) 2008 by Sebastian Trueg * + * * + * 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 _COMMENT_EDIT_WIDGET_H_ +#define _COMMENT_EDIT_WIDGET_H_ + +#include + +class QResizeEvent; +class QMouseEvent; +class QHideEvent; + +class CommentEditWidget : public QFrame +{ + Q_OBJECT + +public: + CommentEditWidget( QWidget* parent = 0 ); + ~CommentEditWidget(); + + void setComment( const QString& s ); + QString comment(); + + /** + * Show the comment widget at position pos. + * \return true if the user chose to save the comment, + * false otherwise. + */ + bool exec( const QPoint& pos ); + + bool eventFilter( QObject* watched, QEvent* event ); + +private: + void updateButtons(); + void resizeEvent( QResizeEvent* ); + void mousePressEvent( QMouseEvent* e ); + void hideEvent( QHideEvent* e ); + + class Private; + Private* const d; + + Q_PRIVATE_SLOT( d, void _k_saveClicked() ) + Q_PRIVATE_SLOT( d, void _k_cancelClicked() ) +}; + +#endif diff --git a/src/commentwidget.cpp b/src/commentwidget.cpp new file mode 100644 index 000000000..eebf12ad6 --- /dev/null +++ b/src/commentwidget.cpp @@ -0,0 +1,114 @@ +/*************************************************************************** + * Copyright (C) 2008 by Sebastian Trueg * + * * + * 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 "commentwidget.h" +#include "commenteditwidget.h" + +#include +#include +#include +#include +#include + +#include + + +class CommentWidget::Private +{ +public: + Private( CommentWidget* parent ) + : q( parent ) { + } + + void update(); + void _k_slotEnableEditing(); + + QLabel* label; + CommentEditWidget* edit; + + QString comment; + +private: + CommentWidget* q; +}; + + +void CommentWidget::Private::update() +{ + if ( comment.isEmpty() ) { + label->setText( "

" + i18n( "Click to add comment..." ) + "" ); + } + else { + label->setText( "

" + comment + "

" + i18n( "Change comment..." ) + "" ); + } +} + + +void CommentWidget::Private::_k_slotEnableEditing() +{ + CommentEditWidget w; + w.setComment( comment ); + if ( w.exec( QCursor::pos() ) ) { + comment = w.comment(); + update(); + emit q->commentChanged( comment ); + } +} + + + +CommentWidget::CommentWidget( QWidget* parent ) + : QWidget( parent ), + d( new Private( this ) ) +{ + d->label = new QLabel( this ); + d->label->setWordWrap( true ); + QVBoxLayout* layout = new QVBoxLayout( this ); + layout->setMargin( 0 ); + layout->addWidget( d->label ); + d->update(); + connect( d->label, SIGNAL( linkActivated( const QString& ) ), this, SLOT( _k_slotEnableEditing() ) ); +} + + +CommentWidget::~CommentWidget() +{ + delete d; +} + + +void CommentWidget::setComment( const QString& comment ) +{ + d->comment = comment; + d->update(); +} + + +QString CommentWidget::comment() const +{ + return d->comment; +} + + +bool CommentWidget::eventFilter( QObject* watched, QEvent* event ) +{ + return QWidget::eventFilter( watched, event ); +} + +#include "commentwidget.moc" diff --git a/src/commentwidget.h b/src/commentwidget.h new file mode 100644 index 000000000..8c588518c --- /dev/null +++ b/src/commentwidget.h @@ -0,0 +1,48 @@ +/*************************************************************************** + * Copyright (C) 2008 by Sebastian Trueg * + * * + * 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 _NEPOMUK_COMMENT_WIDGET_H_ +#define _NEPOMUK_COMMENT_WIDGET_H_ + +#include + +class CommentWidget : public QWidget +{ + Q_OBJECT + +public: + CommentWidget( QWidget* parent = 0 ); + ~CommentWidget(); + + void setComment( const QString& comment ); + QString comment() const; + +Q_SIGNALS: + void commentChanged( const QString& ); + +private: + bool eventFilter( QObject* watched, QEvent* event ); + + class Private; + Private* const d; + + Q_PRIVATE_SLOT( d, void _k_slotEnableEditing() ) +}; + +#endif diff --git a/src/dolphincategorydrawer.cpp b/src/dolphincategorydrawer.cpp index 6cf84560f..b96387646 100644 --- a/src/dolphincategorydrawer.cpp +++ b/src/dolphincategorydrawer.cpp @@ -18,24 +18,22 @@ * Boston, MA 02110-1301, USA. */ +#include "config-nepomuk.h" + #include "dolphincategorydrawer.h" -#include "ratingpainter.h" #include #include #include +#ifdef HAVE_NEPOMUK +#include +#endif + #include #include #include #include -#include -#ifdef HAVE_NEPOMUK -#include -#include -#include -#endif - #include "dolphinview.h" #include "dolphinmodel.h" @@ -217,7 +215,7 @@ void DolphinCategoryDrawer::drawCategory(const QModelIndex &index, int sortRole, QRect ratingRect( option.rect ); ratingRect.setTop(option.rect.top() + (option.rect.height() / 2) - (iconSize / 2)); ratingRect.setHeight( iconSize ); - Nepomuk::RatingPainter::drawRating( painter, ratingRect, Qt::AlignLeft, category.toInt() ); + KRatingPainter::paintRating( painter, ratingRect, Qt::AlignLeft, category.toInt() ); break; } diff --git a/src/metadatawidget.cpp b/src/metadatawidget.cpp index a8086d8a5..922b1b495 100644 --- a/src/metadatawidget.cpp +++ b/src/metadatawidget.cpp @@ -19,9 +19,13 @@ #include "metadatawidget.h" +#include "commentwidget.h" + #include #include +#include +#include #include #include @@ -29,13 +33,14 @@ #include #ifdef HAVE_NEPOMUK +#include "nepomukmassupdatejob.h" #include #include #include #include #include -#include #include +#include "tagcloud/resourcetaggingwidget.h" #endif @@ -57,24 +62,16 @@ public: QMap files; - QTextEdit* editComment; + CommentWidget* editComment; KRatingWidget* ratingWidget; - Nepomuk::TagWidget* tagWidget; + Nepomuk::ResourceTaggingWidget* tagWidget; #endif }; #ifdef HAVE_NEPOMUK void MetaDataWidget::Private::loadComment(const QString& comment) { - editComment->blockSignals(true); - if (comment.isEmpty()) { - editComment->setFontItalic(true); - editComment->setText(i18nc("@info:tooltip", "Click to add comment...")); - } else { - editComment->setFontItalic(false); - editComment->setText(comment); - } - editComment->blockSignals(false); + editComment->setComment( comment ); } #endif @@ -84,28 +81,21 @@ MetaDataWidget::MetaDataWidget(QWidget* parent) : { #ifdef HAVE_NEPOMUK d = new Private; - d->editComment = new QTextEdit(this); + d->editComment = new CommentWidget(this); d->editComment->setFocusPolicy(Qt::ClickFocus); d->ratingWidget = new KRatingWidget(this); - d->tagWidget = new Nepomuk::TagWidget(this); + d->ratingWidget->setAlignment( Qt::AlignCenter ); + d->tagWidget = new Nepomuk::ResourceTaggingWidget(this); connect(d->ratingWidget, SIGNAL(ratingChanged(unsigned int)), this, SLOT(slotRatingChanged(unsigned int))); - connect(d->editComment, SIGNAL(textChanged()), this, SLOT(slotCommentChanged())); + connect(d->editComment, SIGNAL(commentChanged(const QString&)), this, SLOT(slotCommentChanged(const QString&))); + connect( d->tagWidget, SIGNAL( tagClicked( const Nepomuk::Tag& ) ), this, SLOT( slotTagClicked( const Nepomuk::Tag& ) ) ); QVBoxLayout* lay = new QVBoxLayout(this); lay->setMargin(0); - QHBoxLayout* hbox = new QHBoxLayout; - hbox->addWidget(new QLabel(i18nc("@label:slider", "Rating:"), this)); - hbox->addStretch(1); - hbox->addWidget(d->ratingWidget); - lay->addLayout(hbox); + lay->addWidget(d->ratingWidget); lay->addWidget(d->editComment); - hbox = new QHBoxLayout; - hbox->addWidget(new QLabel(i18nc("@label:textbox", "Tags:"), this)); - hbox->addWidget(d->tagWidget, 1); - lay->addLayout(hbox); - - d->editComment->installEventFilter(this); - d->editComment->viewport()->installEventFilter(this); + QHBoxLayout* hbox = new QHBoxLayout; + lay->addWidget( d->tagWidget ); #else d = 0; #endif @@ -120,6 +110,7 @@ MetaDataWidget::~MetaDataWidget() void MetaDataWidget::setFile(const KUrl& url) { + kDebug() << url; KUrl::List urls; urls.append( url ); setFiles( urls ); @@ -129,52 +120,44 @@ void MetaDataWidget::setFile(const KUrl& url) void MetaDataWidget::setFiles(const KUrl::List& urls) { #ifdef HAVE_NEPOMUK - // FIXME #1: For 100 files MetaDataWidget::setFiles() blocks - // for around 15 seconds (maybe we should delegate this to a KJob). - // In the meantime we temporary just reset the widgets: - d->ratingWidget->setRating( 0 ); - d->loadComment( QString() ); - return; - - // FIXME #2: replace with KMetaData::File once we have it again d->files.clear(); bool first = true; QList fileRes; Q_FOREACH( KUrl url, urls ) { - Nepomuk::Resource file( url.url(), Soprano::Vocabulary::Xesam::File() ); -// file.setLocation(url.url()); + Nepomuk::Resource file( url, Soprano::Vocabulary::Xesam::File() ); d->files.insert( url, file ); fileRes.append( file ); - if ( !first && - d->ratingWidget->rating() != file.rating() ) { - d->ratingWidget->setRating( 0 ); // reset rating - } - else if ( first ) { - d->ratingWidget->setRating( (qint32)(file.rating()) ); - } - - if ( !first && - d->editComment->toPlainText() != file.description() ) { - d->loadComment( QString() ); - } - else if ( first ) { - d->loadComment( file.description() ); - } - first = false; + if ( !first && + d->ratingWidget->rating() != file.rating() ) { + d->ratingWidget->setRating( 0 ); // reset rating + } + else if ( first ) { + d->ratingWidget->setRating( (qint32)(file.rating()) ); + } + + if ( !first && + d->editComment->comment() != file.description() ) { + d->loadComment( QString() ); + } + else if ( first ) { + d->loadComment( file.description() ); + } + first = false; } - d->tagWidget->setTaggedResources(fileRes); + d->tagWidget->setResource( fileRes.first() ); #endif } -void MetaDataWidget::slotCommentChanged() +void MetaDataWidget::slotCommentChanged( const QString& s ) { #ifdef HAVE_NEPOMUK - for ( QMap::iterator it = d->files.begin(); - it != d->files.end(); ++it ) { - it.value().setDescription(d->editComment->toPlainText()); - } + Nepomuk::MassUpdateJob* job = Nepomuk::MassUpdateJob::commentResources( d->files.values(), s ); + connect( job, SIGNAL( result( KJob* ) ), + this, SLOT( metadataUpdateDone() ) ); + setEnabled( false ); // no updates during execution + job->start(); #endif } @@ -182,31 +165,31 @@ void MetaDataWidget::slotCommentChanged() void MetaDataWidget::slotRatingChanged(unsigned int rating) { #ifdef HAVE_NEPOMUK - for ( QMap::iterator it = d->files.begin(); - it != d->files.end(); ++it ) { - it.value().setRating(rating); - } + Nepomuk::MassUpdateJob* job = Nepomuk::MassUpdateJob::rateResources( d->files.values(), rating ); + connect( job, SIGNAL( result( KJob* ) ), + this, SLOT( metadataUpdateDone() ) ); + setEnabled( false ); // no updates during execution + job->start(); #endif } -bool MetaDataWidget::eventFilter(QObject* obj, QEvent* event) +void MetaDataWidget::metadataUpdateDone() { -#ifdef HAVE_NEPOMUK - if (obj == d->editComment->viewport() || obj == d->editComment) { - if (event->type() == QEvent::FocusOut) { - // make sure the info text is displayed again - d->loadComment(d->editComment->toPlainText()); - } else if (event->type() == QEvent::FocusIn) { - d->editComment->setFontItalic(false); - if (!d->files.isEmpty() && d->files.begin().value().description().isEmpty()) { - d->editComment->setText(QString()); - } - } - } -#endif + setEnabled( true ); +} + +bool MetaDataWidget::eventFilter(QObject* obj, QEvent* event) +{ return QWidget::eventFilter(obj, event); } + +void MetaDataWidget::slotTagClicked( const Nepomuk::Tag& tag ) +{ + // FIXME + KMessageBox::information( this, "FIXME: connect me to the dolphinmodel: tags:/" + tag.genericLabel() ); +} + #include "metadatawidget.moc" diff --git a/src/metadatawidget.h b/src/metadatawidget.h index b9b8787d0..06411cd7a 100644 --- a/src/metadatawidget.h +++ b/src/metadatawidget.h @@ -24,6 +24,9 @@ #include +namespace Nepomuk { + class Tag; +} class MetaDataWidget : public QWidget { @@ -52,8 +55,10 @@ signals: void metaDataChanged(); private Q_SLOTS: - void slotCommentChanged(); + void slotCommentChanged(const QString&); void slotRatingChanged(unsigned int rating); + void metadataUpdateDone(); + void slotTagClicked( const Nepomuk::Tag& ); protected: bool eventFilter(QObject* obj, QEvent* event); diff --git a/src/nepomukmassupdatejob.cpp b/src/nepomukmassupdatejob.cpp new file mode 100644 index 000000000..484846916 --- /dev/null +++ b/src/nepomukmassupdatejob.cpp @@ -0,0 +1,163 @@ +/*************************************************************************** + * Copyright (C) 2008 by Sebastian Trueg * + * * + * 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 "nepomukmassupdatejob.h" + +#include +#include + +#include +#include + + +Nepomuk::MassUpdateJob::MassUpdateJob( QObject* parent ) + : KJob( parent ), + m_index( -1 ) +{ + kDebug(); + setCapabilities( Killable|Suspendable ); + connect( &m_processTimer, SIGNAL( timeout() ), + this, SLOT( slotNext() ) ); +} + + +Nepomuk::MassUpdateJob::~MassUpdateJob() +{ + kDebug(); +} + + +void Nepomuk::MassUpdateJob::setFiles( const KUrl::List& urls ) +{ + m_resources.clear(); + foreach( KUrl url, urls ) { + m_resources.append( Resource( url ) ); + } + setTotalAmount( KJob::Files, m_resources.count() ); +} + + +void Nepomuk::MassUpdateJob::setResources( const QList& rl ) +{ + m_resources = rl; + setTotalAmount( KJob::Files, m_resources.count() ); +} + + +void Nepomuk::MassUpdateJob::setProperties( const QList >& props ) +{ + m_properties = props; +} + + +void Nepomuk::MassUpdateJob::start() +{ + if ( m_index < 0 ) { + kDebug(); + emit description( this, + i18n("Changing annotations") ); + m_index = 0; + m_processTimer.start(); + } + else { + kDebug() << "Job has already been started"; + } +} + + +bool Nepomuk::MassUpdateJob::doKill() +{ + if ( m_index > 0 ) { + m_processTimer.stop(); + m_index = -1; + return true; + } + else { + return false; + } +} + + +bool Nepomuk::MassUpdateJob::doSuspend() +{ + m_processTimer.stop(); + return true; +} + + +bool Nepomuk::MassUpdateJob::doResume() +{ + if ( m_index > 0 ) { + m_processTimer.start(); + return true; + } + else { + return false; + } +} + + +void Nepomuk::MassUpdateJob::slotNext() +{ + if ( !isSuspended() ) { + if ( m_index < m_resources.count() ) { + Nepomuk::Resource& res = m_resources[m_index]; + for ( int i = 0; i < m_properties.count(); ++i ) { + res.setProperty( m_properties[i].first, m_properties[i].second ); + } + ++m_index; + setProcessedAmount( KJob::Files, m_index ); + } + else if ( m_index >= m_resources.count() ) { + kDebug() << "done"; + m_index = -1; + m_processTimer.stop(); + emitResult(); + } + } +} + + +Nepomuk::MassUpdateJob* Nepomuk::MassUpdateJob::tagResources( const QList& rl, const QList& tags ) +{ + Nepomuk::MassUpdateJob* job = new Nepomuk::MassUpdateJob(); + job->setResources( rl ); + job->setProperties( QList >() << qMakePair( QUrl( Nepomuk::Resource::tagUri() ), Nepomuk::Variant( convertResourceList( tags ) ) ) ); + return job; +} + + +Nepomuk::MassUpdateJob* Nepomuk::MassUpdateJob::rateResources( const QList& rl, int rating ) +{ + Nepomuk::MassUpdateJob* job = new Nepomuk::MassUpdateJob(); + job->setResources( rl ); + job->setProperties( QList >() << qMakePair( QUrl( Nepomuk::Resource::ratingUri() ), Nepomuk::Variant( rating ) ) ); + return job; +} + + +Nepomuk::MassUpdateJob* Nepomuk::MassUpdateJob::commentResources( const QList& rl, const QString& comment ) +{ + Nepomuk::MassUpdateJob* job = new Nepomuk::MassUpdateJob(); + job->setResources( rl ); + job->setProperties( QList >() << qMakePair( QUrl( Nepomuk::Resource::descriptionUri() ), Nepomuk::Variant( comment ) ) ); + return job; +} + +#include "nepomukmassupdatejob.moc" diff --git a/src/nepomukmassupdatejob.h b/src/nepomukmassupdatejob.h new file mode 100644 index 000000000..a19fa5ff9 --- /dev/null +++ b/src/nepomukmassupdatejob.h @@ -0,0 +1,85 @@ +/*************************************************************************** + * Copyright (C) 2008 by Sebastian Trueg * + * * + * 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 _NEPOMUK_MASS_UPDATE_JOB_H_ +#define _NEPOMUK_MASS_UPDATE_JOB_H_ + +#include +#include + +#include +#include +#include + +#include +#include + + +namespace Nepomuk { + class MassUpdateJob : public KJob + { + Q_OBJECT + + public: + MassUpdateJob( QObject* parent = 0 ); + ~MassUpdateJob(); + + /** + * Set a list of files to change + * This has the same effect as using setResources + * with a list of manually created resources. + */ + void setFiles( const KUrl::List& urls ); + + /** + * Set a list of resources to change. + */ + void setResources( const QList& ); + + /** + * Set the properties to change in the mass update. + */ + void setProperties( const QList >& props ); + + /** + * Actually start the job. + */ + void start(); + + static MassUpdateJob* tagResources( const QList&, const QList& tags ); + static MassUpdateJob* commentResources( const QList&, const QString& comment ); + static MassUpdateJob* rateResources( const QList&, int rating ); + + protected: + bool doKill(); + bool doSuspend(); + bool doResume(); + + private Q_SLOTS: + void slotNext(); + + private: + QList m_resources; + QList > m_properties; + int m_index; + QTimer m_processTimer; + }; +} + +#endif diff --git a/src/ratingpainter.cpp b/src/ratingpainter.cpp deleted file mode 100644 index 5dacf1670..000000000 --- a/src/ratingpainter.cpp +++ /dev/null @@ -1,310 +0,0 @@ -/* - This file is part of the Nepomuk KDE project. - Copyright (C) 2007 Sebastian Trueg - - 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 as published by the Free Software Foundation. - - 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 "ratingpainter.h" - -#include -#include - -#include -#include -#include - - -class Nepomuk::RatingPainter::Private -{ -public: - Private() - : maxRating(10), - icon( "rating" ), - bHalfSteps(true), - alignment(Qt::AlignCenter), - direction(Qt::LeftToRight) { - } - - int maxRating; - KIcon icon; - bool bHalfSteps; - Qt::Alignment alignment; - Qt::LayoutDirection direction; - QPixmap customPixmap; -}; - - -Nepomuk::RatingPainter::RatingPainter() - : d(new Private()) -{ -} - - -Nepomuk::RatingPainter::~RatingPainter() -{ - delete d; -} - - -int Nepomuk::RatingPainter::maxRating() const -{ - return d->maxRating; -} - - -bool Nepomuk::RatingPainter::halfStepsEnabled() const -{ - return d->bHalfSteps; -} - - -Qt::Alignment Nepomuk::RatingPainter::alignment() const -{ - return d->alignment; -} - - -Qt::LayoutDirection Nepomuk::RatingPainter::direction() const -{ - return d->direction; -} - - -KIcon Nepomuk::RatingPainter::icon() const -{ - return d->icon; -} - - -QPixmap Nepomuk::RatingPainter::customPixmap() const -{ - return d->customPixmap; -} - - -void Nepomuk::RatingPainter::setMaxRating( int max ) -{ - d->maxRating = max; -} - - -void Nepomuk::RatingPainter::setHalfStepsEnabled( bool enabled ) -{ - d->bHalfSteps = enabled; -} - - -void Nepomuk::RatingPainter::setAlignment( Qt::Alignment align ) -{ - d->alignment = align; -} - - -void Nepomuk::RatingPainter::setLayoutDirection( Qt::LayoutDirection direction ) -{ - d->direction = direction; -} - - -void Nepomuk::RatingPainter::setIcon( const KIcon& icon ) -{ - d->icon = icon; -} - - -void Nepomuk::RatingPainter::setCustomPixmap( const QPixmap& pixmap ) -{ - d->customPixmap = pixmap; -} - - -void Nepomuk::RatingPainter::draw( QPainter* painter, const QRect& rect, int rating, int hoverRating ) -{ - rating = qMin( rating, d->maxRating ); - hoverRating = qMin( hoverRating, d->maxRating ); - - int numUsedStars = d->bHalfSteps ? d->maxRating/2 : d->maxRating; - - if ( hoverRating > 0 && hoverRating < rating ) { - int tmp = hoverRating; - hoverRating = rating; - rating = tmp; - } - - // get the rating pixmaps - QPixmap ratingPix; - if ( !d->customPixmap.isNull() ) { - ratingPix = d->customPixmap; - } - else { - KIcon ratingIcon( "rating" ); - int iconSize = qMin( rect.height(), rect.width() / numUsedStars ); - ratingPix = ratingIcon.pixmap( iconSize ); - } - - QPixmap disabledRatingPix = KIconEffect().apply( ratingPix, KIconEffect::ToGray, 1.0, QColor(), false ); - QPixmap hoverPix; - - bool half = d->bHalfSteps && rating%2; - int numRatingStars = d->bHalfSteps ? rating/2 : rating; - - int numHoverStars = 0; - bool halfHover = false; - if ( hoverRating > 0 && rating != hoverRating ) { - numHoverStars = d->bHalfSteps ? hoverRating/2 : hoverRating; - halfHover = d->bHalfSteps && hoverRating%2; - hoverPix = KIconEffect().apply( ratingPix, KIconEffect::ToGray, 0.5, QColor(), false ); - } - - int usedSpacing = 0; - if ( d->alignment & Qt::AlignJustify ) { - int w = rect.width(); - w -= numUsedStars * ratingPix.width(); - usedSpacing = w / ( numUsedStars-1 ); - } - - int i = 0; - int x = rect.x(); - if ( d->alignment & Qt::AlignRight ) { - x += ( rect.width() - ratingPix.width()*numUsedStars ); - } - else if ( d->alignment & Qt::AlignCenter ) { - x += ( rect.width() - ratingPix.width()*numUsedStars )/2; - } - - int xInc = ratingPix.width() + usedSpacing; - if ( d->direction == Qt::RightToLeft ) { - x = rect.width() - ratingPix.width() - x; - xInc = -xInc; - } - - int y = rect.y(); - if( d->alignment & Qt::AlignVCenter ) { - y += ( rect.height() / 2 - ratingPix.height() / 2 ); - } - else if ( d->alignment & Qt::AlignBottom ) { - y += ( rect.height() - ratingPix.height() ); - } - for(; i < numRatingStars; ++i ) { - painter->drawPixmap( x, y, ratingPix ); - x += xInc; - } - if( half ) { - painter->drawPixmap( x, y, ratingPix.width()/2, ratingPix.height(), - d->direction == Qt::LeftToRight ? ratingPix : ( numHoverStars > 0 ? hoverPix : disabledRatingPix ), - 0, 0, ratingPix.width()/2, ratingPix.height() ); - painter->drawPixmap( x + ratingPix.width()/2, y, ratingPix.width()/2, ratingPix.height(), - d->direction == Qt::LeftToRight ? ( numHoverStars > 0 ? hoverPix : disabledRatingPix ) : ratingPix, - ratingPix.width()/2, 0, ratingPix.width()/2, ratingPix.height() ); - x += xInc; - ++i; - } - for(; i < numHoverStars; ++i ) { - painter->drawPixmap( x, y, hoverPix ); - x += xInc; - } - if( halfHover ) { - painter->drawPixmap( x, y, ratingPix.width()/2, ratingPix.height(), - d->direction == Qt::LeftToRight ? hoverPix : disabledRatingPix, - 0, 0, ratingPix.width()/2, ratingPix.height() ); - painter->drawPixmap( x + ratingPix.width()/2, y, ratingPix.width()/2, ratingPix.height(), - d->direction == Qt::LeftToRight ? disabledRatingPix : hoverPix, - ratingPix.width()/2, 0, ratingPix.width()/2, ratingPix.height() ); - x += xInc; - ++i; - } - for(; i < numUsedStars; ++i ) { - painter->drawPixmap( x, y, disabledRatingPix ); - x += xInc; - } -} - - -int Nepomuk::RatingPainter::fromPosition( const QRect& rect, const QPoint& pos ) -{ - int numUsedStars = d->bHalfSteps ? d->maxRating/2 : d->maxRating; - QPixmap ratingPix; - if ( !d->customPixmap.isNull() ) { - ratingPix = d->customPixmap; - } - else { - KIcon ratingIcon( "rating" ); - int iconSize = qMin( rect.height(), rect.width() / numUsedStars ); - ratingPix = ratingIcon.pixmap( iconSize ); - } - - QRect usedRect( rect ); - if ( d->alignment & Qt::AlignRight ) { - usedRect.setLeft( rect.right() - numUsedStars * ratingPix.width() ); - } - else if ( d->alignment & Qt::AlignHCenter ) { - int x = ( rect.width() - numUsedStars * ratingPix.width() )/2; - usedRect.setLeft( rect.left() + x ); - usedRect.setRight( rect.right() - x ); - } - else { // d->alignment & Qt::AlignLeft - usedRect.setRight( rect.left() + numUsedStars * ratingPix.width() - 1 ); - } - - if ( d->alignment & Qt::AlignBottom ) { - usedRect.setTop( rect.bottom() - ratingPix.height() + 1 ); - } - else if ( d->alignment & Qt::AlignVCenter ) { - int x = ( rect.height() - ratingPix.height() )/2; - usedRect.setTop( rect.top() + x ); - usedRect.setBottom( rect.bottom() - x ); - } - else { // d->alignment & Qt::AlignTop - usedRect.setBottom( rect.top() + ratingPix.height() - 1 ); - } - - if ( usedRect.contains( pos ) ) { - int x = 0; - if ( d->direction == Qt::RightToLeft ) { - x = usedRect.right() - pos.x(); - } - else { - x = pos.x() - usedRect.left(); - } - - double one = ( double )usedRect.width() / ( double )d->maxRating; - - kDebug() << "rating:" << ( int )( ( double )x/one + 0.5 ); - - return ( int )( ( double )x/one + 0.5 ); - } - else { - return -1; - } -} - - -void Nepomuk::RatingPainter::drawRating( QPainter* painter, const QRect& rect, Qt::Alignment align, int rating, int hoverRating ) -{ - RatingPainter rp; - rp.setAlignment( align ); - rp.setLayoutDirection( painter->layoutDirection() ); - rp.draw( painter, rect, rating, hoverRating ); -} - - -int Nepomuk::RatingPainter::getRatingFromPosition( const QRect& rect, Qt::Alignment align, Qt::LayoutDirection direction, const QPoint& pos ) -{ - RatingPainter rp; - rp.setAlignment( align ); - rp.setLayoutDirection( direction ); - return rp.fromPosition( rect, pos ); -} diff --git a/src/ratingpainter.h b/src/ratingpainter.h deleted file mode 100644 index 31c406de2..000000000 --- a/src/ratingpainter.h +++ /dev/null @@ -1,122 +0,0 @@ -/* - This file is part of the Nepomuk KDE project. - Copyright (C) 2007 Sebastian Trueg - - 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 as published by the Free Software Foundation. - - 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 _NEPOMUK_RATING_PAINTER_H_ -#define _NEPOMUK_RATING_PAINTER_H_ - -class QPainter; - -#include -#include - -#include - - -namespace Nepomuk { - /** - * Utility class that draws a row of stars for a rating value. - */ - class RatingPainter - { - public: - /** - * Create a new RatingPainter. - * Normally the static methods should be sufficient. - */ - RatingPainter(); - ~RatingPainter(); - - int maxRating() const; - bool halfStepsEnabled() const; - Qt::Alignment alignment() const; - Qt::LayoutDirection direction() const; - KIcon icon() const; - QPixmap customPixmap() const; - - /** - * The maximum rating. Defaults to 10. - */ - void setMaxRating( int max ); - - /** - * If half steps are enabled (the default) then - * one rating step corresponds to half a star. - */ - void setHalfStepsEnabled( bool enabled ); - - /** - * The alignment of the stars in the drawing rect. - * All alignment flags are supported. - */ - void setAlignment( Qt::Alignment align ); - - /** - * LTR or RTL - */ - void setLayoutDirection( Qt::LayoutDirection direction ); - - /** - * Set a custom icon. Defaults to "rating". - */ - void setIcon( const KIcon& icon ); - - /** - * Set a custom pixmap. - */ - void setCustomPixmap( const QPixmap& pixmap ); - - /** - * Draw the rating into the configured rect. - */ - void draw( QPainter* painter, const QRect& rect, int rating, int hoverRating = -1 ); - - /** - * Calculate the rating value from mouse position pos. - * - * \return The rating corresponding to pos or -1 if pos is - * outside of the configured rect. - */ - int fromPosition( const QRect& rect, const QPoint& pos ); - - /** - * Convenience method that draws a rating into the given rect. - * - * LayoutDirection is read from QPainter. - * - * \param align can be aligned vertically and horizontally. Using Qt::AlignJustify will insert spacing - * between the stars. - */ - static void drawRating( QPainter* p, const QRect& rect, Qt::Alignment align, int rating, int hoverRating = -1 ); - - /** - * Get the rating that would be selected if the user clicked position pos - * within rect if the rating has been drawn with drawRating() using the same - * rect and align values. - * - * \return The new rating or -1 if pos is outside of the rating area. - */ - static int getRatingFromPosition( const QRect& rect, Qt::Alignment align, Qt::LayoutDirection direction, const QPoint& pos ); - - private: - class Private; - Private* const d; - }; -} - -#endif diff --git a/src/tagcloud/newtagdialog.cpp b/src/tagcloud/newtagdialog.cpp new file mode 100644 index 000000000..0fe574fdc --- /dev/null +++ b/src/tagcloud/newtagdialog.cpp @@ -0,0 +1,83 @@ +/* + Copyright (C) 2008 by Sebastian Trueg + + 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, 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 "newtagdialog.h" + +#include + +#include +#include +#include + + +NewTagDialog::NewTagDialog( QWidget* parent ) + : KDialog( parent ) +{ + setCaption( i18n( "Create new Tag" ) ); + setButtons( Ok|Cancel ); + enableButtonOk( false ); + + setupUi( mainWidget() ); + + connect( m_editTagLabel, SIGNAL( textChanged(const QString&) ), + this, SLOT( slotLabelChanged(const QString&) ) ); +} + + +NewTagDialog::~NewTagDialog() +{ +} + + +void NewTagDialog::slotLabelChanged( const QString& text ) +{ + enableButtonOk( !text.isEmpty() ); +} + + +Nepomuk::Tag NewTagDialog::createTag( QWidget* parent ) +{ + NewTagDialog dlg( parent ); + dlg.m_labelTitle->setText( i18n( "Create New Tag" ) ); + dlg.m_labelTitle->setComment( i18n( "with optional icon and description" ) ); + dlg.m_labelTitle->setPixmap( KIcon( "nepomuk" ).pixmap( 32, 32 ) ); + + dlg.m_editTagLabel->setFocus(); + + if ( dlg.exec() ) { + QString name = dlg.m_editTagLabel->text(); + QString comment = dlg.m_editTagComment->text(); + QString icon = dlg.m_buttonTagIcon->icon(); + + Nepomuk::Tag newTag( name ); + newTag.setLabel( name ); + newTag.addIdentifier( name ); + if ( !comment.isEmpty() ) { + newTag.setDescription( comment ); + } + if ( !icon.isEmpty() ) { + newTag.addSymbol( icon ); + } + return newTag; + } + else { + return Nepomuk::Tag(); + } +} + +#include "newtagdialog.moc" diff --git a/src/tagcloud/newtagdialog.h b/src/tagcloud/newtagdialog.h new file mode 100644 index 000000000..c3f6ff017 --- /dev/null +++ b/src/tagcloud/newtagdialog.h @@ -0,0 +1,45 @@ +/* + Copyright (C) 2008 by Sebastian Trueg + + 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, 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 _NEW_TAG_DIALOG_H_ +#define _NEW_TAG_DIALOG_H_ + +#include +#include "ui_newtagdialog.h" + +namespace Nepomuk { + class Tag; +} + +class NewTagDialog : public KDialog, public Ui_NewTagDialog +{ + Q_OBJECT + +public: + ~NewTagDialog(); + + static Nepomuk::Tag createTag( QWidget* parent = 0 ); + +private Q_SLOTS: + void slotLabelChanged( const QString& text ); + +private: + NewTagDialog( QWidget* parent = 0 ); +}; + +#endif diff --git a/src/tagcloud/newtagdialog.ui b/src/tagcloud/newtagdialog.ui new file mode 100644 index 000000000..ab2ea97a6 --- /dev/null +++ b/src/tagcloud/newtagdialog.ui @@ -0,0 +1,113 @@ + + NewTagDialog + + + + 0 + 0 + 390 + 149 + + + + Form + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + Name: + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Detailed Desctiption (optional): + + + + + + + + + + + KIconButton + QPushButton +

kicondialog.h
+ + + KLineEdit + QLineEdit +
klineedit.h
+
+ + KTitleWidget + QWidget +
ktitlewidget.h
+
+ + + + diff --git a/src/tagcloud/resourcetaggingwidget.cpp b/src/tagcloud/resourcetaggingwidget.cpp new file mode 100644 index 000000000..e7d6dfa78 --- /dev/null +++ b/src/tagcloud/resourcetaggingwidget.cpp @@ -0,0 +1,138 @@ +/* + This file is part of the Nepomuk KDE project. + Copyright (C) 2007 Sebastian Trueg + + 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 as published by the Free Software Foundation. + + 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 "resourcetaggingwidget.h" +#include "tagcloud.h" +#include "taggingpopup.h" + +#include +#include +#include +#include + +#include + + +class Nepomuk::ResourceTaggingWidget::Private +{ +public: + Nepomuk::Resource resource; + + TagCloud* resourceTagCloud; + TaggingPopup* popup; + + QList resourceTags; + + QAction* changeTagsAction; + + void showTaggingPopup( const QPoint& ); + void _k_slotShowTaggingPopup(); +}; + + +void Nepomuk::ResourceTaggingWidget::Private::showTaggingPopup( const QPoint& pos ) +{ + popup->showAllTags(); + resourceTags = resource.tags(); + Q_FOREACH( Tag tag, resourceTags ) { + popup->setTagSelected( tag, true ); + } + + popup->exec( pos ); + + resource.setTags( resourceTags ); +} + + +void Nepomuk::ResourceTaggingWidget::Private::_k_slotShowTaggingPopup() +{ + showTaggingPopup( QCursor::pos() ); +} + + +Nepomuk::ResourceTaggingWidget::ResourceTaggingWidget( QWidget* parent ) + : QWidget( parent ), + d( new Private() ) +{ + QVBoxLayout* layout = new QVBoxLayout( this ); + layout->setMargin( 0 ); + d->resourceTagCloud = new TagCloud( this ); + layout->addWidget( d->resourceTagCloud ); + + d->changeTagsAction = new QAction( i18n( "Change tags..." ), this ); + d->resourceTagCloud->setCustomNewTagAction( d->changeTagsAction ); + + // the popup tag cloud + d->popup = new TaggingPopup; + d->popup->setSelectionEnabled( true ); + d->popup->setNewTagButtonEnabled( true ); + + connect( d->popup, SIGNAL( tagToggled( const Nepomuk::Tag&, bool ) ), + this, SLOT( slotTagToggled( const Nepomuk::Tag&, bool ) ) ); + connect( d->popup, SIGNAL( tagAdded( const Nepomuk::Tag& ) ), + this, SLOT( slotTagAdded( const Nepomuk::Tag& ) ) ); + + connect( d->changeTagsAction, SIGNAL( activated() ), + this, SLOT( _k_slotShowTaggingPopup() ) ); + + connect( d->resourceTagCloud, SIGNAL( tagClicked( const Nepomuk::Tag& ) ), + this, SIGNAL( tagClicked( const Nepomuk::Tag& ) ) ); +} + + +Nepomuk::ResourceTaggingWidget::~ResourceTaggingWidget() +{ + delete d->popup; + delete d; +} + + +void Nepomuk::ResourceTaggingWidget::setResource( const Nepomuk::Resource& res ) +{ + d->resource = res; + d->resourceTagCloud->showResourceTags( res ); +} + + +void Nepomuk::ResourceTaggingWidget::slotTagToggled( const Nepomuk::Tag& tag, bool enabled ) +{ + if ( enabled ) { + d->resourceTags.append( tag ); + } + else { + d->resourceTags.removeAll( tag ); + } + d->popup->hide(); +} + + +void Nepomuk::ResourceTaggingWidget::slotTagAdded( const Nepomuk::Tag& tag ) +{ + // assign it right away + d->resourceTags.append( tag ); + d->resource.addTag( tag ); +} + + +void Nepomuk::ResourceTaggingWidget::contextMenuEvent( QContextMenuEvent* e ) +{ + d->showTaggingPopup( e->globalPos() ); +} + +#include "resourcetaggingwidget.moc" diff --git a/src/tagcloud/resourcetaggingwidget.h b/src/tagcloud/resourcetaggingwidget.h new file mode 100644 index 000000000..a75746348 --- /dev/null +++ b/src/tagcloud/resourcetaggingwidget.h @@ -0,0 +1,60 @@ +/* + This file is part of the Nepomuk KDE project. + Copyright (C) 2007 Sebastian Trueg + + 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 as published by the Free Software Foundation. + + 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 _NEPOMUK_RESOURCE_TAGGING_WIDGET_H_ +#define _NEPOMUK_RESOURCE_TAGGING_WIDGET_H_ + +#include + +#include + +class QEvent; +class QContextMenuEvent; + +namespace Nepomuk { + class ResourceTaggingWidget : public QWidget + { + Q_OBJECT + + public: + ResourceTaggingWidget( QWidget* parent = 0 ); + ~ResourceTaggingWidget(); + + Q_SIGNALS: + void tagClicked( const Nepomuk::Tag& tag ); + + public Q_SLOTS: + void setResource( const Nepomuk::Resource& ); + + private Q_SLOTS: + void slotTagToggled( const Nepomuk::Tag& tag, bool enabled ); + void slotTagAdded( const Nepomuk::Tag& tag ); + + protected: + void contextMenuEvent( QContextMenuEvent* e ); + + private: + class Private; + Private* const d; + + Q_PRIVATE_SLOT( d, void _k_slotShowTaggingPopup() ) + }; +} + +#endif diff --git a/src/tagcloud/tagcloud.cpp b/src/tagcloud/tagcloud.cpp new file mode 100644 index 000000000..8fe5cba89 --- /dev/null +++ b/src/tagcloud/tagcloud.cpp @@ -0,0 +1,1002 @@ +/* + This file is part of the Nepomuk KDE project. + Copyright (C) 2007 Sebastian Trueg + + 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 as published by the Free Software Foundation. + + 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 "tagcloud.h" +#include "newtagdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + + +namespace { + const int s_hSpacing = 10; + const int s_vSpacing = 5; + + class TagNode { + public: + TagNode() + : weight( 0 ), + selected( false ) { + } + + // fixed info + Nepomuk::Tag tag; + int weight; + + // misc + bool selected; + + // info generated by rebuildCloud + QFont font; + QRect rect; + QRect zoomedRect; + QString text; + }; + + bool tagNodeNameLessThan( const TagNode& n1, const TagNode& n2 ) { + return n1.text < n2.text; + } + + bool tagNodeWeightLessThan( const TagNode& n1, const TagNode& n2 ) { + return n1.weight < n2.weight; + } + + int rowLength( const QList& row ) { + int rowLen = 0; + for ( int j = 0; j < row.count(); ++j ) { + rowLen += row[j]->rect.width(); + if ( j < row.count()-1 ) { + rowLen += s_hSpacing; + } + } + return rowLen; + } + + int rowHeight( const QList& row ) { + int h = 0; + for ( int j = 0; j < row.count(); ++j ) { + h = qMax( row[j]->rect.height(), h ); + } + return h; + } + + QSize cloudSize( const QList >& rows ) { + int w = 0; + int h = 0; + for ( int i = 0; i < rows.count(); ++i ) { + w = qMax( w, rowLength( rows[i] ) ); + h += rowHeight( rows[i] ); + if ( i < rows.count()-1 ) { + h += s_vSpacing; + } + } + return QSize( w, h ); + } +} + + +class Nepomuk::TagCloud::Private +{ +public: + Private( TagCloud* parent ) + : maxFontSize( 0 ), + minFontSize( 0 ), + maxNumberDisplayedTags( 0 ), + selectionEnabled( false ), + newTagButtonEnabled( false ), + alignment( Qt::AlignCenter ), + sorting( SortAlpabetically ), + zoomEnabled( true ), + showAllTags( false ), + customNewTagAction( 0 ), + hoverTag( 0 ), + cachedHfwWidth( -1 ), + m_parent( parent ) { + newTagNode.text = i18n( "New Tag..." ); + } + + int maxFontSize; + int minFontSize; + int maxNumberDisplayedTags; + bool selectionEnabled; + bool newTagButtonEnabled; + Qt::Alignment alignment; + Sorting sorting; + bool zoomEnabled; + + // The resource whose tags we are showing + // invalid if we show all tags or a selection + QUrl resource; + bool showAllTags; + + // the actual nodes + QList nodes; + + // just a helper structure for speeding up things + QList > rows; + + TagNode newTagNode; + QAction* customNewTagAction; + + TagNode* hoverTag; + + QMatrix zoomMatrix; + + QSize cachedSizeHint; + int cachedHfwWidth; + int cachedHfwHeight; + + void invalidateCachedValues() { + cachedSizeHint = QSize(); + cachedHfwWidth = -1; + } + + int getMinFontSize() const; + int getMaxFontSize() const; + void updateNodeWeights(); + void updateNodeFonts(); + void sortNodes(); + void rebuildCloud(); + TagNode* tagAt( const QPoint& pos ); + TagNode* findTagInRow( const QList& row, const QPoint& pos ); + TagNode* nodeForTag( const Tag& tag ); + int calculateWeight( const Nepomuk::Tag& tag ); + +private: + TagCloud* m_parent; +}; + + +int Nepomuk::TagCloud::Private::getMinFontSize() const +{ + return minFontSize > 0 ? minFontSize : ( 8 * m_parent->font().pointSize() / 10 ); +} + + +int Nepomuk::TagCloud::Private::getMaxFontSize() const +{ + return maxFontSize > 0 ? maxFontSize : ( 22 * m_parent->font().pointSize() / 10 ); +} + + +int Nepomuk::TagCloud::Private::calculateWeight( const Nepomuk::Tag& tag ) +{ + // stupid SPARQL has no functions such as count! + Soprano::QueryResultIterator it + = ResourceManager::instance()->mainModel()->executeQuery( QString( "select ?r where { ?r <%1> <%2> . }" ) + .arg( Soprano::Vocabulary::NAO::hasTag().toString() ) + .arg( tag.resourceUri().toString() ), + Soprano::Query::QueryLanguageSparql ); + int w = 0; + while ( it.next() ) { + ++w; + } + return w; +} + + +void Nepomuk::TagCloud::Private::updateNodeWeights() +{ + bool changedWeights = false; + for ( QList::iterator it = nodes.begin(); + it != nodes.end(); ++it ) { + TagNode& node = *it; + int w = calculateWeight( node.tag ); + if ( w != node.weight ) { + node.weight = w; + changedWeights = true; + } + } + if ( changedWeights ) { + updateNodeFonts(); + } +} + + +void Nepomuk::TagCloud::Private::updateNodeFonts() +{ + int maxWeight = 0; + int minWeight = 0; + for ( QList::iterator it = nodes.begin(); + it != nodes.end(); ++it ) { + TagNode& node = *it; + minWeight = qMin( minWeight, node.weight ); + maxWeight = qMax( maxWeight, node.weight ); + } + + // calculate font sizes + // ---------------------------------------------- + int usedMinFontSize = getMinFontSize(); + int usedMaxFontSize = getMaxFontSize(); + for ( QList::iterator it = nodes.begin(); + it != nodes.end(); ++it ) { + TagNode& node = *it; + double normalizedWeight = (double)(node.weight - minWeight) / (double)qMax(maxWeight - minWeight, 1); + node.font = m_parent->font(); + node.font.setPointSize( usedMinFontSize + (int)((double)(usedMaxFontSize-usedMinFontSize) * normalizedWeight) ); + if( normalizedWeight > 0.8 ) + node.font.setBold( true ); + } + + if ( newTagButtonEnabled ) { + newTagNode.font = m_parent->font(); + newTagNode.font.setPointSize( usedMinFontSize ); + newTagNode.font.setUnderline( true ); + } +} + + +void Nepomuk::TagCloud::Private::sortNodes() +{ + if ( sorting == SortAlpabetically ) { + qSort( nodes.begin(), nodes.end(), tagNodeNameLessThan ); + } + else if ( sorting == SortByWeight ) { + qSort( nodes.begin(), nodes.end(), tagNodeWeightLessThan ); + } + else if ( sorting == SortRandom ) { + KRandomSequence().randomize( nodes ); + } +} + + +void Nepomuk::TagCloud::Private::rebuildCloud() +{ + if ( nodes.isEmpty() && !newTagButtonEnabled ) { + return; + } + + // - Always try to be quadratic + // - Always prefer to expand horizontally + // - If we cannot fit everything into m_parent->contentsRect(), zoom + // - If alignment & Qt::AlignJustify insert spaces between tags + + sortNodes(); + + QRect contentsRect = m_parent->contentsRect(); + + // initialize the nodes' sizes + // ---------------------------------------------- + for ( QList::iterator it = nodes.begin(); + it != nodes.end(); ++it ) { + TagNode& node = *it; + node.rect = QFontMetrics( node.font ).boundingRect( node.text ); + } + if ( newTagButtonEnabled ) { + newTagNode.rect = QFontMetrics( newTagNode.font ).boundingRect( customNewTagAction ? customNewTagAction->text() : newTagNode.text ); + } + + + // and position the nodes + // ---------------------------------------------- + rows.clear(); + if ( 0 ) { // FIXME: make it configurable + QRect lineRect; + QRect totalRect; + QList row; + for ( QList::iterator it = nodes.begin(); + it != nodes.end(); /* We do increment it below */ ) { + TagNode& node = *it; + + int usedSpacing = row.isEmpty() ? 0 : s_hSpacing; + if ( lineRect.width() + usedSpacing + node.rect.width() <= contentsRect.width() ) { + node.rect.moveBottomLeft( QPoint( lineRect.right() + usedSpacing, lineRect.bottom() ) ); + QRect newLineRect = lineRect.united( node.rect ); + newLineRect.moveTopLeft( lineRect.topLeft() ); + lineRect = newLineRect; + row.append( &node ); + + // update all other nodes in this line + Q_FOREACH( TagNode* n, row ) { + n->rect.moveBottom( lineRect.bottom() - ( lineRect.height() - n->rect.height() )/2 ); + } + + ++it; + } + else { + rows.append( row ); + row.clear(); + int newLineTop = lineRect.bottom() + s_vSpacing; + lineRect = QRect(); + lineRect.moveTop( newLineTop ); + } + } + rows.append( row ); + } + else { + // initialize first row + rows.append( QList() ); + for ( QList::iterator it = nodes.begin(); + it != nodes.end(); ++it ) { + TagNode& node = *it; + rows.first().append( &node ); + } + if ( newTagButtonEnabled ) { + rows.first().append( &newTagNode ); + } + + // calculate the rows + QList > bestRows( rows ); + QSize size( rowLength( rows.first() ), rowHeight( rows.first() ) ); + QSize bestSize( size ); + while ( ( size.height() < size.width() || + size.width() > contentsRect.width() ) && + size.height() <= contentsRect.height() ) { + // find the longest row + int maxRow = 0; + int maxLen = 0; + for ( int i = 0; i < rows.count(); ++i ) { + int rowLen = rowLength( rows[i] ); + if ( rowLen > maxLen ) { + maxLen = rowLen; + maxRow = i; + } + } + + // move the last item from the longest row to the next row + TagNode* node = rows[maxRow].takeLast(); + if ( rows.count() <= maxRow+1 ) { + rows.append( QList() ); + } + rows[maxRow+1].prepend( node ); + + // update the size + size = cloudSize( rows ); + + if ( size.width() < bestSize.width() && + ( size.width() > size.height() || + bestSize.width() > contentsRect.width() ) && + size.height() <= contentsRect.height() ) { + bestSize = size; + bestRows = rows; + } + } + rows = bestRows; + + // position the tags + int y = 0; + for ( QList >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) { + QList& row = *rowIt; + int h = rowHeight( row ); + int x = 0; + Q_FOREACH( TagNode* node, row ) { + node->rect.moveTop( y + ( h - node->rect.height() )/2 ); + node->rect.moveLeft( x ); + x += s_hSpacing + node->rect.width(); + } + y += h + s_vSpacing; + } + } + + + // let's see if we have to zoom + // ---------------------------------------------- + zoomMatrix = QMatrix(); + int w = contentsRect.width(); + if ( zoomEnabled ) { + for ( QList >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) { + QList& row = *rowIt; + w = qMax( w, row.last()->rect.right() ); + } + if ( w > contentsRect.width() ) { + double zoomFactor = ( double )contentsRect.width() / ( double )w; + zoomMatrix.scale( zoomFactor, zoomFactor ); + } + } + + // force horizontal alignment + // ---------------------------------------------- + for ( QList >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) { + QList& row = *rowIt; + int space = /*contentsRect.right()*/w - row.last()->rect.right(); + if ( alignment & ( Qt::AlignRight|Qt::AlignHCenter ) ) { + Q_FOREACH( TagNode* node, row ) { + node->rect.moveLeft( node->rect.left() + ( alignment & Qt::AlignRight ? space : space/2 ) ); + } + } + else if ( alignment & Qt::AlignJustify && row.count() > 1 ) { + space /= ( row.count()-1 ); + int i = 0; + Q_FOREACH( TagNode* node, row ) { + node->rect.moveLeft( node->rect.left() + ( space * i++ ) ); + } + } + } + + // force vertical alignment + // ---------------------------------------------- + int verticalSpace = contentsRect.bottom() - rows.last().first()->rect.bottom(); + if ( alignment & ( Qt::AlignBottom|Qt::AlignVCenter ) ) { + for ( QList >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) { + Q_FOREACH( TagNode* node, *rowIt ) { + node->rect.moveTop( node->rect.top() + ( alignment & Qt::AlignBottom ? verticalSpace : verticalSpace/2 ) ); + } + } + } + + for( QList::iterator it = nodes.begin(); it != nodes.end(); ++it ) { + it->zoomedRect = zoomMatrix.mapRect( it->rect ); + } + newTagNode.zoomedRect = zoomMatrix.mapRect( newTagNode.rect ); + + m_parent->updateGeometry(); + m_parent->update(); +} + + +// binary search in row +TagNode* Nepomuk::TagCloud::Private::findTagInRow( const QList& row, const QPoint& pos ) +{ + int x = row.count() * pos.x() / m_parent->width(); + int i = 0; + while ( 1 ) { + if ( x-i >= 0 && x-i < row.count() && row[x-i]->zoomedRect.contains( pos ) ) { + return row[x-i]; + } + else if ( x+i >= 0 && x+i < row.count() && row[x+i]->zoomedRect.contains( pos ) ) { + return row[x+i]; + } + if ( x-i < 0 && x+i >= row.count() ) { + return 0; + } + ++i; + } + return 0; +} + + +// binary search in cloud +TagNode* Nepomuk::TagCloud::Private::tagAt( const QPoint& pos ) +{ + int y = rows.count() * pos.y() / m_parent->height(); + + int i = 0; + while ( 1 ) { + if ( y-i >= 0 && y-i < rows.count() ) { + if ( TagNode* node = findTagInRow( rows[y-i], pos ) ) { + return node; + } + } + if ( y+i >= 0 && y+i < rows.count() ) { + if ( TagNode* node = findTagInRow( rows[y+i], pos ) ) { + return node; + } + } + if ( y-i < 0 && y+i >= rows.count() ) { + return 0; + } + ++i; + } + return 0; +} + + +TagNode* Nepomuk::TagCloud::Private::nodeForTag( const Tag& tag ) +{ + for ( QList::iterator it = nodes.begin(); + it != nodes.end(); ++it ) { + TagNode& node = *it; + if ( tag == node.tag ) { + return &node; + } + } + return 0; +} + + + +Nepomuk::TagCloud::TagCloud( QWidget* parent ) + : QFrame( parent ), + d( new Private(this) ) +{ + QSizePolicy policy( QSizePolicy::Preferred, + QSizePolicy::Preferred ); + policy.setHeightForWidth( true ); + setSizePolicy( policy ); + setMouseTracking( true ); + + // Since signals are delivered in no particular order + // our slot might be called before the resources are updated + // Then, we would use invalid cached data. + // By using queued connections this problem should be solved. + connect( ResourceManager::instance()->mainModel(), + SIGNAL( statementAdded( const Soprano::Statement& ) ), + this, + SLOT( slotStatementAdded( const Soprano::Statement& ) ), + Qt::QueuedConnection ); + connect( ResourceManager::instance()->mainModel(), + SIGNAL( statementRemoved( const Soprano::Statement& ) ), + this, + SLOT( slotStatementRemoved( const Soprano::Statement& ) ), + Qt::QueuedConnection ); +} + + +Nepomuk::TagCloud::~TagCloud() +{ + delete d; +} + + +void Nepomuk::TagCloud::setMaxFontSize( int size ) +{ + d->invalidateCachedValues(); + d->maxFontSize = size; + d->updateNodeFonts(); + d->rebuildCloud(); +} + + +void Nepomuk::TagCloud::setMinFontSize( int size ) +{ + d->invalidateCachedValues(); + d->minFontSize = size; + d->updateNodeFonts(); + d->rebuildCloud(); +} + + +void Nepomuk::TagCloud::setMaxNumberDisplayedTags( int n ) +{ + d->maxNumberDisplayedTags = n; + d->rebuildCloud(); +} + + +void Nepomuk::TagCloud::setSelectionEnabled( bool enabled ) +{ + d->selectionEnabled = enabled; + update(); +} + + +void Nepomuk::TagCloud::setNewTagButtonEnabled( bool enabled ) +{ + d->newTagButtonEnabled = enabled; + d->rebuildCloud(); +} + + +bool Nepomuk::TagCloud::zoomEnabled() const +{ + return d->zoomEnabled; +} + + +void Nepomuk::TagCloud::setZoomEnabled( bool zoom ) +{ + if ( d->zoomEnabled != zoom ) { + d->zoomEnabled = zoom; + d->rebuildCloud(); + } +} + + +void Nepomuk::TagCloud::setContextMenuEnabled( bool enabled ) +{ +} + + +void Nepomuk::TagCloud::setAlignment( Qt::Alignment alignment ) +{ + d->alignment = alignment; + d->rebuildCloud(); +} + + +void Nepomuk::TagCloud::setSorting( Sorting s ) +{ + d->invalidateCachedValues(); + d->sorting = s; + d->rebuildCloud(); +} + + +void Nepomuk::TagCloud::showAllTags() +{ + showTags( Nepomuk::Tag::allTags() ); + d->showAllTags = true; +} + + +void Nepomuk::TagCloud::showResourceTags( const Resource& resource ) +{ + showTags( resource.tags() ); + d->resource = resource.uri(); +} + + +void Nepomuk::TagCloud::showTags( const QList& tags ) +{ + d->resource = QUrl(); + d->showAllTags = false; + d->invalidateCachedValues(); + d->nodes.clear(); + Q_FOREACH( Tag tag, tags ) { + TagNode node; + node.tag = tag; + node.weight = d->calculateWeight( tag ); + node.text = node.tag.genericLabel(); + + d->nodes.append( node ); + } + d->updateNodeFonts(); + d->rebuildCloud(); +} + + +void Nepomuk::TagCloud::setTagSelected( const Tag& tag, bool selected ) +{ + if ( TagNode* node = d->nodeForTag( tag ) ) { + node->selected = selected; + if ( d->selectionEnabled ) { + update( node->zoomedRect ); + } + } +} + + +QSize Nepomuk::TagCloud::sizeHint() const +{ + // If we have tags d->rebuildCloud() has been called at least once, + // thus, we have proper rects (i.e. needed sizes) + + if ( !d->cachedSizeHint.isValid() ) { + QList > rows; + rows.append( QList() ); + for ( QList::iterator it = d->nodes.begin(); + it != d->nodes.end(); ++it ) { + TagNode& node = *it; + rows.first().append( &node ); + } + if ( d->newTagButtonEnabled ) { + rows.first().append( &d->newTagNode ); + } + + QSize size( rowLength( rows.first() ), rowHeight( rows.first() ) ); + QSize bestSize( size ); + while ( size.height() < size.width() ) { + // find the longest row + int maxRow = 0; + int maxLen = 0; + for ( int i = 0; i < rows.count(); ++i ) { + int rowLen = rowLength( rows[i] ); + if ( rowLen > maxLen ) { + maxLen = rowLen; + maxRow = i; + } + } + + // move the last item from the longest row to the next row + TagNode* node = rows[maxRow].takeLast(); + if ( rows.count() <= maxRow+1 ) { + rows.append( QList() ); + } + rows[maxRow+1].prepend( node ); + + // update the size + size = cloudSize( rows ); + + if ( size.width() < bestSize.width() && + size.width() > size.height() ) { + bestSize = size; + } + } + + d->cachedSizeHint = QSize( bestSize.width() + frameWidth()*2, + bestSize.height() + frameWidth()*2 ); + } + + return d->cachedSizeHint; +} + + +QSize Nepomuk::TagCloud::minimumSizeHint() const +{ + // If we have tags d->rebuildCloud() has been called at least once, + // thus, we have proper rects (i.e. needed sizes) + if ( d->nodes.isEmpty() && !d->newTagButtonEnabled ) { + return QSize( fontMetrics().width( i18n( "No Tags" ) ), fontMetrics().height() ); + } + else { + QSize size; + for ( QList::iterator it = d->nodes.begin(); + it != d->nodes.end(); ++it ) { + size.setWidth( qMax( size.width(), ( *it ).rect.width() ) ); + size.setHeight( qMax( size.height(), ( *it ).rect.height() ) ); + } + if ( d->newTagButtonEnabled ) { + size.setWidth( qMax( size.width(), d->newTagNode.rect.width() ) ); + size.setHeight( qMax( size.height(), d->newTagNode.rect.height() ) ); + } + size.setWidth( size.width() + frameWidth()*2 ); + size.setHeight( size.height() + frameWidth()*2 ); + return size; + } +} + + +int Nepomuk::TagCloud::heightForWidth( int contentsWidth ) const +{ + // If we have tags d->rebuildCloud() has been called at least once, + // thus, we have proper rects (i.e. needed sizes) + + if ( d->cachedHfwWidth != contentsWidth ) { + // have to keep in mind the frame + contentsWidth -= frameWidth()*2; + + QList allNodes; + for ( QList::iterator it = d->nodes.begin(); + it != d->nodes.end(); ++it ) { + TagNode& node = *it; + allNodes.append( &node ); + } + if ( d->newTagButtonEnabled ) { + allNodes.append( &d->newTagNode ); + } + + int h = 0; + bool newRow = true; + int rowW = 0; + int rowH = 0; + for ( int i = 0; i < allNodes.count(); ++i ) { + int w = rowW; + if ( !newRow ) { + w += s_hSpacing; + } + newRow = false; + w += allNodes[i]->rect.width(); + if ( w <= contentsWidth ) { + rowH = qMax( rowH, allNodes[i]->rect.height() ); + rowW = w; + } + else { + if ( h > 0 ) { + h += s_vSpacing; + } + h += rowH; + rowH = allNodes[i]->rect.height(); + rowW = allNodes[i]->rect.width(); + } + } + if ( rowH > 0 ) { + h += s_vSpacing + rowH; + } + + d->cachedHfwWidth = contentsWidth; + d->cachedHfwHeight = h; + } + + return d->cachedHfwHeight + frameWidth()*2; +} + + +void Nepomuk::TagCloud::resizeEvent( QResizeEvent* e ) +{ + QFrame::resizeEvent( e ); + d->rebuildCloud(); + update(); +} + + +void Nepomuk::TagCloud::paintEvent( QPaintEvent* e ) +{ + QFrame::paintEvent( e ); + + KStatefulBrush normalTextBrush( KColorScheme::View, KColorScheme::NormalText ); + KStatefulBrush activeTextBrush( KColorScheme::View, KColorScheme::VisitedText ); + KStatefulBrush hoverTextBrush( KColorScheme::View, KColorScheme::ActiveText ); + + QPainter p( this ); + QRegion paintRegion = e->region(); + + if ( d->nodes.isEmpty() && !d->newTagButtonEnabled ) { + p.drawText( contentsRect(), d->alignment, i18n( "No Tags" ) ); + } + else { + p.save(); + p.setMatrix( d->zoomMatrix ); + + for ( QList::iterator it = d->nodes.begin(); + it != d->nodes.end(); ++it ) { + TagNode& node = *it; + + if ( paintRegion.contains( node.zoomedRect ) ) { + p.setFont( node.font ); + + if ( &node == d->hoverTag ) { + p.setPen( hoverTextBrush.brush( this ).color() ); + } + else if ( d->selectionEnabled && node.selected ) { + p.setPen( activeTextBrush.brush( this ).color() ); + } + else { + p.setPen( normalTextBrush.brush( this ).color() ); + } + p.drawText( node.rect, Qt::AlignCenter, node.text ); + } + } + + if ( d->newTagButtonEnabled ) { + p.setFont( d->newTagNode.font ); + if ( &d->newTagNode == d->hoverTag ) { + p.setPen( hoverTextBrush.brush( this ).color() ); + } + else { + p.setPen( normalTextBrush.brush( this ).color() ); + } + p.drawText( d->newTagNode.rect, Qt::AlignCenter, d->customNewTagAction ? d->customNewTagAction->text() : d->newTagNode.text ); + } + + p.restore(); + } +} + + +void Nepomuk::TagCloud::mousePressEvent( QMouseEvent* e ) +{ + if ( e->button() == Qt::LeftButton ) { + if ( TagNode* node = d->tagAt( e->pos() ) ) { + kDebug() << "clicked" << node->text; + if ( node == &d->newTagNode ) { + if ( d->customNewTagAction ) { + d->customNewTagAction->trigger(); + } + else { + // FIXME: nicer gui + Tag newTag = NewTagDialog::createTag( this ); + if ( newTag.isValid() ) { + emit tagAdded( newTag ); + } + } + } + else { + emit tagClicked( node->tag ); + if ( d->selectionEnabled ) { + kDebug() << "Toggleing tag" << node->text; + node->selected = !node->selected; + emit tagToggled( node->tag, node->selected ); + update( node->zoomedRect ); + } + } + } + } +} + + +void Nepomuk::TagCloud::mouseMoveEvent( QMouseEvent* e ) +{ + if ( e->buttons() == Qt::NoButton ) { + + TagNode* oldHoverTag = d->hoverTag; + + if ( ( d->hoverTag = d->tagAt( e->pos() ) ) && + !d->selectionEnabled ) { + setCursor( Qt::PointingHandCursor ); + } + else if ( d->newTagButtonEnabled && + d->newTagNode.zoomedRect.contains( e->pos() ) ) { + d->hoverTag = &d->newTagNode; + setCursor( Qt::PointingHandCursor ); + } + else { + unsetCursor(); + } + + if ( oldHoverTag || d->hoverTag ) { + QRect updateRect; + if ( d->hoverTag ) + updateRect = updateRect.united( d->hoverTag->zoomedRect ); + if ( oldHoverTag ) + updateRect = updateRect.united( oldHoverTag->zoomedRect ); + + update( updateRect ); + } + } +} + + +void Nepomuk::TagCloud::leaveEvent( QEvent* ) +{ + unsetCursor(); + if ( d->hoverTag ) { + QRect updateRect = d->hoverTag->zoomedRect; + d->hoverTag = 0; + update( updateRect ); + } +} + + +void Nepomuk::TagCloud::slotStatementAdded( const Soprano::Statement& s ) +{ + if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() && + s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) { + // new tag created + if ( d->showAllTags ) { + showAllTags(); + } + } + else if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) { + if ( s.subject().uri() == d->resource ) { + showResourceTags( d->resource ); + } + else { + // weights might have changed + d->updateNodeWeights(); + d->rebuildCloud(); + } + } +} + + +void Nepomuk::TagCloud::slotStatementRemoved( const Soprano::Statement& s ) +{ + // FIXME: In theory might contain empty nodes as wildcards + + if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) { + if ( d->resource.isValid() && + d->resource == s.subject().uri() ) { + showResourceTags( d->resource ); + } + else { + // weights might have changed + d->updateNodeWeights(); + d->rebuildCloud(); + } + } + else if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() && + s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) { + // tag deleted + if ( d->showAllTags ) { + showAllTags(); + } + } +} + + +void Nepomuk::TagCloud::setCustomNewTagAction( QAction* action ) +{ + d->customNewTagAction = action; + setNewTagButtonEnabled( action != 0 ); +} + +#include "tagcloud.moc" diff --git a/src/tagcloud/tagcloud.h b/src/tagcloud/tagcloud.h new file mode 100644 index 000000000..2c641e7fb --- /dev/null +++ b/src/tagcloud/tagcloud.h @@ -0,0 +1,143 @@ +/* + This file is part of the Nepomuk KDE project. + Copyright (C) 2007 Sebastian Trueg + + 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 as published by the Free Software Foundation. + + 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 _NEPOMUK_TAG_CLOUD_H_ +#define _NEPOMUK_TAG_CLOUD_H_ + +#include +#include + +#include +#include + +#include + +class QResizeEvent; +class QPaintEvent; +class QMouseEvent; +class QEvent; + +namespace Nepomuk { + class NEPOMUK_EXPORT TagCloud : public QFrame + { + Q_OBJECT + + public: + TagCloud( QWidget* parent = 0 ); + ~TagCloud(); + + enum Sorting { + SortAlpabetically, + SortByWeight, + SortRandom + }; + + int heightForWidth( int w ) const; + QSize sizeHint() const; + QSize minimumSizeHint() const; + + bool zoomEnabled() const; + + public Q_SLOTS: + /** + * Set the maximum used font size. The default is 0 + * which means to calculate proper values from the KDE + * defaults. + */ + void setMaxFontSize( int size ); + + /** + * Set the minimum used font size. The default is 0 + * which means to calculate proper values from the KDE + * defaults. + */ + void setMinFontSize( int size ); + + /** + * Set the maximum number of displayed tags. The default is 0 + * which means to display all tags. + * + * NOT IMPLEMENTED YET + */ + void setMaxNumberDisplayedTags( int n ); + + /** + * Allow selection of tags, i.e. enabling and disabling of tags. + */ + void setSelectionEnabled( bool enabled ); + + void setNewTagButtonEnabled( bool enabled ); + void setContextMenuEnabled( bool enabled ); + void setAlignment( Qt::Alignment alignment ); + + void setZoomEnabled( bool zoom ); + + /** + * Default: SortAlpabetically + */ + void setSorting( Sorting ); + + /** + * Will reset tags set via showTags() + */ + void showAllTags(); + + /** + * Set the tags to be shown in the tag cloud. + * If the new tag button is enabled (setEnableNewTagButton()) + * new tags will automatically be added to the list of shown tags. + */ + void showTags( const QList& tags ); + + void showResourceTags( const Resource& resource ); + + /** + * Select or deselect a tag. This does only make sense + * if selection is enabled and \p tag is actually + * displayed. + * + * \sa setSelectionEnabled + */ + void setTagSelected( const Tag& tag, bool selected ); + + void setCustomNewTagAction( QAction* action ); + + Q_SIGNALS: + void tagClicked( const Nepomuk::Tag& tag ); + void tagAdded( const Nepomuk::Tag& tag ); + void tagToggled( const Nepomuk::Tag& tag, bool enabled ); + + protected: + void resizeEvent( QResizeEvent* e ); + void paintEvent( QPaintEvent* e ); + void mousePressEvent( QMouseEvent* ); + void mouseMoveEvent( QMouseEvent* ); + void leaveEvent( QEvent* ); + + private Q_SLOTS: + void slotStatementAdded( const Soprano::Statement& s ); + void slotStatementRemoved( const Soprano::Statement& s ); + + private: + class Private; + Private* const d; + }; +} + +#endif diff --git a/src/tagcloud/taggingpopup.cpp b/src/tagcloud/taggingpopup.cpp new file mode 100644 index 000000000..a1024254d --- /dev/null +++ b/src/tagcloud/taggingpopup.cpp @@ -0,0 +1,148 @@ +/* + This file is part of the Nepomuk KDE project. + Copyright (C) 2007 Sebastian Trueg + + 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 as published by the Free Software Foundation. + + 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 "taggingpopup.h" + +#include +#include +#include +#include +#include + +#include +#include + + +class Nepomuk::TaggingPopup::Private +{ +public: + Private( TaggingPopup* parent ) + : eventLoop( 0 ), + m_parent( parent ) { + } + + QEventLoop* eventLoop; + QPoint popupPos; + + QRect geometryForPopupPos( const QPoint& p ) { + QSize size = m_parent->sizeHint(); + + // we want a little margin + const int margin = KDialog::marginHint(); + size.setHeight( size.height() + margin*2 ); + size.setWidth( size.width() + margin*2 ); + + QRect screen = QApplication::desktop()->screenGeometry( QApplication::desktop()->screenNumber( p ) ); + + // calculate popup position + QPoint pos( p.x() - size.width()/2, p.y() - size.height()/2 ); + + // ensure we do not leave the desktop + if ( pos.x() + size.width() > screen.right() ) { + pos.setX( screen.right() - size.width() ); + } + else if ( pos.x() < screen.left() ) { + pos.setX( screen.left() ); + } + + if ( pos.y() + size.height() > screen.bottom() ) { + pos.setY( screen.bottom() - size.height() ); + } + else if ( pos.y() < screen.top() ) { + pos.setY( screen.top() ); + } + + return QRect( pos, size ); + } + +private: + TaggingPopup* m_parent; +}; + + +Nepomuk::TaggingPopup::TaggingPopup( QWidget* parent ) + : TagCloud( parent ), + d( new Private( this ) ) +{ + setFrameStyle( QFrame::Box|QFrame::Plain ); + setWindowFlags( Qt::Popup ); +} + + +Nepomuk::TaggingPopup::~TaggingPopup() +{ + delete d; +} + + +void Nepomuk::TaggingPopup::popup( const QPoint& p ) +{ + setGeometry( d->geometryForPopupPos( p ) ); + d->popupPos = p; + + show(); +} + + +void Nepomuk::TaggingPopup::exec( const QPoint& pos ) +{ + QEventLoop eventLoop; + d->eventLoop = &eventLoop; + popup( pos ); + + QPointer guard = this; + (void) eventLoop.exec(); + if ( !guard.isNull() ) + d->eventLoop = 0; +} + + +void Nepomuk::TaggingPopup::mousePressEvent( QMouseEvent* e ) +{ + if ( !rect().contains( e->pos() ) ) { + hide(); + } + else { + TagCloud::mousePressEvent( e ); + } +} + + +void Nepomuk::TaggingPopup::hideEvent( QHideEvent* e ) +{ + Q_UNUSED( e ); + if ( d->eventLoop ) { + d->eventLoop->exit(); + } +} + + +bool Nepomuk::TaggingPopup::event( QEvent* e ) +{ + if ( e->type() == QEvent::LayoutRequest ) { + if ( isVisible() ) { + setGeometry( d->geometryForPopupPos( d->popupPos ) ); + return true; + } + } + + return TagCloud::event( e ); +} + +#include "taggingpopup.moc" diff --git a/src/tagcloud/taggingpopup.h b/src/tagcloud/taggingpopup.h new file mode 100644 index 000000000..99cee701c --- /dev/null +++ b/src/tagcloud/taggingpopup.h @@ -0,0 +1,50 @@ +/* + This file is part of the Nepomuk KDE project. + Copyright (C) 2007 Sebastian Trueg + + 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 as published by the Free Software Foundation. + + 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 _NEPOMUK_TAGGING_POPUP_H_ +#define _NEPOMUK_TAGGING_POPUP_H_ + +#include "tagcloud.h" + +class QMouseEvent; +class QHideEvent; + +namespace Nepomuk { + class TaggingPopup : public TagCloud + { + public: + TaggingPopup( QWidget* parent = 0 ); + ~TaggingPopup(); + + void popup( const QPoint& pos ); + void exec( const QPoint& pos ); + + bool event( QEvent* e ); + + protected: + void mousePressEvent( QMouseEvent* e ); + void hideEvent( QHideEvent* e ); + + private: + class Private; + Private* const d; + }; +} + +#endif