diff --git a/config.cc b/config.cc index a44d732d..24bfd856 100644 --- a/config.cc +++ b/config.cc @@ -258,6 +258,7 @@ Group loadGroup( QDomElement grp, unsigned * nextId = 0 ) g.name = grp.attribute( "name" ); g.icon = grp.attribute( "icon" ); + g.favoritesFolder = grp.attribute( "favoritesFolder" ); if ( !grp.attribute( "iconData" ).isEmpty() ) g.iconData = QByteArray::fromBase64( grp.attribute( "iconData" ).toLatin1() ); @@ -1029,6 +1030,13 @@ void saveGroup( Group const & data, QDomElement & group ) group.setAttributeNode( name ); + if( data.favoritesFolder.size() ) + { + QDomAttr folder = dd.createAttribute( "favoritesFolder" ); + folder.setValue( data.favoritesFolder ); + group.setAttributeNode( folder ); + } + if ( data.icon.size() ) { QDomAttr icon = dd.createAttribute( "icon" ); @@ -1996,6 +2004,11 @@ QString getHistoryFileName() throw( exError ) return getHomeDir().filePath( "history" ); } +QString getFavoritiesFileName() throw( exError ) +{ + return getHomeDir().filePath( "favorites" ); +} + QString getUserCssFileName() throw( exError ) { return getHomeDir().filePath( "article-style.css" ); diff --git a/config.hh b/config.hh index f51c844f..fa19ae3e 100644 --- a/config.hh +++ b/config.hh @@ -87,6 +87,7 @@ struct Group QString name, icon; QByteArray iconData; QKeySequence shortcut; + QString favoritesFolder; QVector< DictionaryRef > dictionaries; Config::MutedDictionaries mutedDictionaries; // Disabled via dictionary bar Config::MutedDictionaries popupMutedDictionaries; // Disabled via dictionary bar in popup @@ -95,6 +96,7 @@ struct Group bool operator == ( Group const & other ) const { return id == other.id && name == other.name && icon == other.icon && + favoritesFolder == other.favoritesFolder && dictionaries == other.dictionaries && shortcut == other.shortcut && mutedDictionaries == other.mutedDictionaries && popupMutedDictionaries == other.popupMutedDictionaries && @@ -662,6 +664,9 @@ QString getPidFileName() throw( exError ); /// Returns the filename of a history file which stores search history. QString getHistoryFileName() throw( exError ); +/// Returns the filename of a favorities file. +QString getFavoritiesFileName() throw( exError ); + /// Returns the user .css file name. QString getUserCssFileName() throw( exError ); diff --git a/dictgroupwidget.ui b/dictgroupwidget.ui index ac5d5ef4..370255db 100644 --- a/dictgroupwidget.ui +++ b/dictgroupwidget.ui @@ -14,18 +14,6 @@ Form - - 2 - - - 4 - - - 2 - - - 3 - @@ -91,6 +79,20 @@ + + + + + + Favorites folder: + + + + + + + + diff --git a/favoritespanewidget.cc b/favoritespanewidget.cc new file mode 100644 index 00000000..713a1361 --- /dev/null +++ b/favoritespanewidget.cc @@ -0,0 +1,1118 @@ +/* This file is (c) 2017 Abs62 + * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "favoritespanewidget.hh" +#include "gddebug.hh" +#include "atomic_rename.hh" + +/************************************************** FavoritesPaneWidget *********************************************/ + +void FavoritesPaneWidget::setUp( Config::Class * cfg, QMenu * menu ) +{ + m_cfg = cfg; + m_favoritesTree = findChild< QTreeView * >( "favoritesTree" ); + QDockWidget * favoritesPane = qobject_cast< QDockWidget * >( parentWidget() ); + m_favoritesTree->setHeaderHidden( true ); + + // Delete selected items action + m_deleteSelectedAction = new QAction( this ); + m_deleteSelectedAction->setText( tr( "&Delete Selected" ) ); + m_deleteSelectedAction->setShortcut( QKeySequence( QKeySequence::Delete ) ); + m_deleteSelectedAction->setShortcutContext( Qt::WidgetWithChildrenShortcut ); + addAction( m_deleteSelectedAction ); + connect( m_deleteSelectedAction, SIGNAL( triggered() ), + this, SLOT( deleteSelectedItems() ) ); + + // Copy selected items to clipboard + m_copySelectedToClipboard = new QAction( this ); + m_copySelectedToClipboard->setText( tr( "Copy Selected" ) ); + m_copySelectedToClipboard->setShortcut( QKeySequence( QKeySequence::Copy ) ); + m_copySelectedToClipboard->setShortcutContext( Qt::WidgetWithChildrenShortcut ); + addAction( m_copySelectedToClipboard ); + connect( m_copySelectedToClipboard, SIGNAL( triggered() ), + this, SLOT( copySelectedItems() ) ); + + // Add folder to tree view + m_addFolder = new QAction( this ); + m_addFolder->setText( tr( "Add folder" ) ); + addAction( m_addFolder ); + connect( m_addFolder, SIGNAL( triggered() ), this, SLOT( addFolder() ) ); + + + // Handle context menu, reusing some of the top-level window's History menu + m_favoritesMenu = new QMenu( this ); + m_separator = m_favoritesMenu->addSeparator(); + QListIterator< QAction * > actionsIter( menu->actions() ); + while ( actionsIter.hasNext() ) + m_favoritesMenu->addAction( actionsIter.next() ); + + // Make the favorites pane's titlebar + + favoritesLabel.setText( tr( "Favorites:" ) ); + favoritesLabel.setObjectName( "favoritesLabel" ); + if ( layoutDirection() == Qt::LeftToRight ) + { + favoritesLabel.setAlignment( Qt::AlignLeft ); + } + else + { + favoritesLabel.setAlignment( Qt::AlignRight ); + } + + favoritesPaneTitleBarLayout.addWidget( &favoritesLabel ); + favoritesPaneTitleBarLayout.setContentsMargins(5, 5, 5, 5); + favoritesPaneTitleBar.setLayout( &favoritesPaneTitleBarLayout ); + favoritesPaneTitleBar.setObjectName("favoritesPaneTitleBar"); + favoritesPane->setTitleBarWidget( &favoritesPaneTitleBar ); + + // Favorites tree + m_favoritesModel = new FavoritesModel( Config::getFavoritiesFileName(), this ); + m_favoritesTree->setModel( m_favoritesModel ); + + connect( m_favoritesTree, SIGNAL( expanded( QModelIndex ) ), + m_favoritesModel, SLOT( itemExpanded( QModelIndex ) ) ); + + connect( m_favoritesTree, SIGNAL( collapsed( QModelIndex ) ), + m_favoritesModel, SLOT( itemCollapsed( QModelIndex ) ) ); + + connect( m_favoritesModel, SIGNAL( expandItem( QModelIndex) ), + m_favoritesTree, SLOT( expand( QModelIndex ) ) ); + + m_favoritesModel->checkAllNodesForExpand(); + m_favoritesTree->viewport()->setAcceptDrops( true ); + m_favoritesTree->setDragEnabled( true ); +// m_favoritesTree->setDragDropMode( QAbstractItemView::InternalMove ); + m_favoritesTree->setDragDropMode( QAbstractItemView::DragDrop ); + m_favoritesTree->setDefaultDropAction( Qt::MoveAction ); + + m_favoritesTree->setRootIsDecorated( true ); + + m_favoritesTree->setContextMenuPolicy( Qt::CustomContextMenu ); + m_favoritesTree->setSelectionMode( QAbstractItemView::ExtendedSelection ); + + m_favoritesTree->setEditTriggers( QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed ); + + m_favoritesTree->installEventFilter( this ); + m_favoritesTree->viewport()->installEventFilter( this ); + + // list selection and keyboard navigation + connect( m_favoritesTree, SIGNAL( clicked( QModelIndex const & ) ), + this, SLOT( onItemClicked( QModelIndex const & ) ) ); + + connect ( m_favoritesTree->selectionModel(), SIGNAL( selectionChanged ( QItemSelection const & , QItemSelection const & ) ), + this, SLOT( onSelectionChanged( QItemSelection const & ) ) ); + + connect( m_favoritesTree, SIGNAL( customContextMenuRequested( QPoint const & ) ), + this, SLOT( showCustomMenu( QPoint const & ) ) ); + + listItemDelegate = new WordListItemDelegate( m_favoritesTree->itemDelegate() ); + m_favoritesTree->setItemDelegate( listItemDelegate ); +} + +FavoritesPaneWidget::~FavoritesPaneWidget() +{ + if( listItemDelegate ) + delete listItemDelegate; +} + +bool FavoritesPaneWidget::eventFilter( QObject * obj, QEvent * ev ) +{ + // unused for now + + return QWidget::eventFilter( obj, ev ); +} + +void FavoritesPaneWidget::copySelectedItems() +{ + QModelIndexList selectedIdxs = m_favoritesTree->selectionModel()->selectedIndexes(); + + if ( selectedIdxs.isEmpty() ) + { + // nothing to do + return; + } + + FavoritesModel * model = static_cast< FavoritesModel * >( m_favoritesTree->model() ); + QStringList selectedStrings = model->getTextForIndexes( selectedIdxs ); + + QApplication::clipboard()->setText( selectedStrings.join( QString::fromLatin1( "\n" ) ) ); +} + +void FavoritesPaneWidget::deleteSelectedItems() +{ + QModelIndexList selectedIdxs = m_favoritesTree->selectionModel()->selectedIndexes(); + + if ( selectedIdxs.isEmpty() ) + { + // nothing to do + return; + } + + QMessageBox mb( QMessageBox::Warning, "GoldenDict", + tr( "All selected items will be deleted. Continue?" ), + QMessageBox::Yes | QMessageBox::No ); + mb.exec(); + if( mb.result() != QMessageBox::Yes ) + return; + + FavoritesModel * md = static_cast< FavoritesModel * >( m_favoritesTree->model() ); + md->removeItemsForIndexes( selectedIdxs ); +} + +void FavoritesPaneWidget::showCustomMenu(QPoint const & pos) +{ + QModelIndexList selectedIdxs = m_favoritesTree->selectionModel()->selectedIndexes(); + + m_favoritesMenu->removeAction( m_copySelectedToClipboard ); + m_favoritesMenu->removeAction( m_deleteSelectedAction ); + m_favoritesMenu->removeAction( m_addFolder ); + + m_separator->setVisible( !selectedIdxs.isEmpty() ); + + if ( !selectedIdxs.isEmpty() ) + { + m_favoritesMenu->insertAction( m_separator, m_copySelectedToClipboard ); + m_favoritesMenu->insertAction( m_separator, m_deleteSelectedAction ); + } + + if( selectedIdxs.size() == 1 ) + { + m_favoritesMenu->insertAction( m_separator, m_addFolder ); + } + + m_favoritesMenu->exec( m_favoritesTree->mapToGlobal( pos ) ); +} + +void FavoritesPaneWidget::onSelectionChanged( QItemSelection const & selection ) +{ + if ( selection.size() != 1 ) + return; + + itemSelectionChanged = true; + emitFavoritesItemRequested( selection.indexes().front() ); +} + +void FavoritesPaneWidget::onItemClicked( QModelIndex const & idx ) +{ + if ( !itemSelectionChanged && m_favoritesTree->selectionModel()->selectedIndexes().size() == 1 ) + { + emitFavoritesItemRequested( idx ); + } + itemSelectionChanged = false; +} + +void FavoritesPaneWidget::emitFavoritesItemRequested( QModelIndex const & idx ) +{ + FavoritesModel * md = static_cast< FavoritesModel * >( m_favoritesTree->model() ); + if( md->itemType( idx ) != TreeItem::Word ) + { + // Item is not headword + return; + } + + QString headword = md->data( idx, Qt::DisplayRole ).toString(); + QString path = md->pathToItem( idx ); + + if( !headword.isEmpty() ) + emit favoritesItemRequested( headword, path ); +} + +void FavoritesPaneWidget::addFolder() +{ + QModelIndexList selectedIdx = m_favoritesTree->selectionModel()->selectedIndexes(); + if( selectedIdx.size() != 1 ) + return; + + FavoritesModel * md = static_cast< FavoritesModel * >( m_favoritesTree->model() ); + QModelIndex folderIdx = md->addNewFolder( selectedIdx.front() ); + if( folderIdx.isValid() ) + m_favoritesTree->edit( folderIdx ); +} + +void FavoritesPaneWidget::addHeadword( QString const & path, QString const & headword ) +{ + FavoritesModel * md = static_cast< FavoritesModel * >( m_favoritesTree->model() ); + md->addNewHeadword( path, headword ); +} + +void FavoritesPaneWidget::getDataInXml( QByteArray & dataStr ) +{ + FavoritesModel * md = static_cast< FavoritesModel * >( m_favoritesTree->model() ); + md->getDataInXml( dataStr ); +} + +void FavoritesPaneWidget::getDataInPlainText( QString & dataStr ) +{ + FavoritesModel * md = static_cast< FavoritesModel * >( m_favoritesTree->model() ); + md->getDataInPlainText( dataStr ); +} + +bool FavoritesPaneWidget::setDataFromXml( QString const & dataStr ) +{ + FavoritesModel * md = static_cast< FavoritesModel * >( m_favoritesTree->model() ); + return md->setDataFromXml( dataStr ); +} + +/************************************************** TreeItem *********************************************/ + +TreeItem::TreeItem( const QVariant &data, TreeItem *parent, Type type ) : + itemData( data ), + parentItem( parent ), + m_type( type ), + m_expanded( false ) +{ +} + +TreeItem::~TreeItem() +{ + qDeleteAll( childItems ); +} + +void TreeItem::appendChild( TreeItem *item ) +{ + childItems.append( item ); +} + +void TreeItem::insertChild( int row, TreeItem * item ) +{ + if( row > childItems.count() ) + row = childItems.count(); + childItems.insert( row, item ); +} + +TreeItem *TreeItem::child( int row ) const +{ + return childItems.value( row ); +} + +void TreeItem::deleteChild( int row ) +{ + if( row < 0 || row >= childItems.count() ) + return; + + TreeItem *it = childItems.at( row ); + childItems.removeAt( row ); + delete it; +} + +int TreeItem::childCount() const +{ + return childItems.count(); +} + +QVariant TreeItem::data() const +{ + return itemData; +} + +void TreeItem::setData( const QVariant & newData ) +{ + itemData = newData; +} + +int TreeItem::row() const +{ + if( parentItem ) + return parentItem->childItems.indexOf( const_cast< TreeItem * >( this ) ); + + return 0; +} + +TreeItem *TreeItem::parent() +{ + return parentItem; +} + +Qt::ItemFlags TreeItem::flags() const +{ + Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable | + Qt::ItemIsDragEnabled; + if( m_type == Folder ) + f |= Qt::ItemIsEditable | Qt::ItemIsDropEnabled; + else + if( m_type == Root ) + f |= Qt::ItemIsDropEnabled; + + return f; +} + +QString TreeItem::fullPath() const +{ + // Get full path from root item + QString path; + TreeItem * par = parentItem; + for( ; ; ) + { + if( !par ) + break; + path = par->data().toString() + "/" + path; + par = par->parentItem; + } + return path; +} + +TreeItem * TreeItem::duplicateItem( TreeItem * newParent ) const +{ + TreeItem * newItem = new TreeItem( itemData, newParent, m_type ); + if( m_type == Folder ) + { + QList< TreeItem * >::const_iterator it = childItems.begin(); + for( ; it != childItems.end(); ++it ) + newItem->appendChild( (*it)->duplicateItem( newItem ) ); + } + return newItem; +} + +bool TreeItem::haveAncestor( TreeItem * item ) +{ + TreeItem *par = parentItem; + for( ; ; ) + { + if( !par ) + break; + if( par == item ) + return true; + par = par->parent(); + } + return false; +} + +bool TreeItem::haveSameItem( TreeItem * item, bool allowSelf ) +{ + QList< TreeItem * >::const_iterator it = childItems.begin(); + QString name = item->data().toString(); + for( ; it != childItems.end(); ++it ) + { + if( *it == item && !allowSelf ) + return true; + if( (*it)->data().toString() == name && (*it)->type() == item->type() && (*it) != item ) + return true; + } + + return false; +} + +QStringList TreeItem::getTextFromAllChilds() const +{ + QStringList list; + QList< TreeItem * >::const_iterator it = childItems.begin(); + for( ; it != childItems.end(); ++it ) + { + if( (*it)->type() == Word ) + { + QString txt = (*it)->data().toString(); + list.append( txt ); + } + else // Folder + { + QStringList childList = (*it)->getTextFromAllChilds(); + list.append( childList ); + } + } + return list; +} + +/************************************************** FavoritesModel *********************************************/ + +FavoritesModel::FavoritesModel( QString favoritesFilename, QObject * parent ) : + QAbstractItemModel( parent ), + m_favoritesFilename( favoritesFilename ), + rootItem( 0 ) +{ + readData(); +} + +FavoritesModel::~FavoritesModel() +{ + if( rootItem ) + { + saveData(); + delete rootItem; + } +} + +Qt::ItemFlags FavoritesModel::flags( const QModelIndex &idx ) const +{ + TreeItem * item = getItem( idx ); + return item->flags(); +} + +QVariant FavoritesModel::headerData( int , Qt::Orientation, + int ) const +{ + return QVariant(); +} + +QModelIndex FavoritesModel::index( int row, int column, const QModelIndex &parentIdx ) const +{ +// if(!hasIndex(row, column, parent)) +// return QModelIndex(); + + TreeItem *parentItem = getItem( parentIdx ); + + TreeItem *childItem = parentItem->child(row); + if( childItem ) + return createIndex( row, column, childItem ); + + return QModelIndex(); +} + +QModelIndex FavoritesModel::parent( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return QModelIndex(); + + TreeItem *childItem = getItem( index ); + if( childItem == rootItem ) + return QModelIndex(); + + TreeItem *parentItem = childItem->parent(); + + if( parentItem == rootItem ) + return QModelIndex(); + + return createIndex( parentItem->row(), 0, parentItem ); +} + +int FavoritesModel::rowCount(const QModelIndex &parent) const +{ + if ( parent.column() > 0 ) + return 0; + + TreeItem *parentItem = getItem( parent ); + + return parentItem->childCount(); +} + +int FavoritesModel::columnCount(const QModelIndex & ) const +{ + return 1; +} + +bool FavoritesModel::removeRows( int row, int count, const QModelIndex &parent ) +{ + TreeItem * parentItem = getItem( parent ); + + beginRemoveRows( parent, row, row + count - 1 ); + + for( int i = 0; i < count; i++ ) + parentItem->deleteChild( row ); + + endRemoveRows(); + + return true; +} + +bool FavoritesModel::setData( const QModelIndex & index, const QVariant & value, int role ) +{ + if( role != Qt::EditRole || !index.isValid() || value.toString().isEmpty() ) + return false; + + QModelIndex parentIdx = parent( index ); + if( findItemInFolder( value.toString(), TreeItem::Folder, parentIdx ).isValid() ) + { + // Such folder is already presented in parent folder + return false; + } + + TreeItem * item = getItem( index ); + item->setData( value ); + + return true; +} + +QVariant FavoritesModel::data( QModelIndex const & index, int role ) const +{ + if( !index.isValid() ) + return QVariant(); + + TreeItem *item = getItem( index ); + if( item == rootItem ) + return QVariant(); + + if ( role == Qt::DisplayRole || role == Qt::ToolTipRole ) + { + return item->data(); + } + else + if( role == Qt::DecorationRole ) + { + if( item->type() == TreeItem::Folder || item->type() == TreeItem::Root ) + return QIcon( ":/icons/folder.png" ); + + return QVariant(); + } + if( role == Qt::EditRole ) + { + if( item->type() == TreeItem::Folder ) + return item->data(); + + return QVariant(); + } + + return QVariant(); +} + +Qt::DropActions FavoritesModel::supportedDropActions() const +{ + return Qt::MoveAction | Qt::CopyAction; +} + +void FavoritesModel::readData() +{ + // Read data from "favorities" file + + beginResetModel(); + + if( rootItem ) + delete rootItem; + + rootItem = new TreeItem( QVariant(), 0, TreeItem::Root ); + + QFile favoritesFile( m_favoritesFilename ); + if( !favoritesFile.open( QFile::ReadOnly ) ) + { + gdWarning( "No favorities file found" ); + return; + } + + QString errorStr; + int errorLine, errorColumn; + dom.clear(); + + if ( !dom.setContent( &favoritesFile, false, &errorStr, &errorLine, &errorColumn ) ) + { + // Mailformed file + QString errStr; + errStr.sprintf( "Error: %s at %d,%d\n", errorStr.toLocal8Bit().constData(), errorLine, errorColumn ); + gdWarning( errStr.toUtf8().data() ); + + QMessageBox mb( QMessageBox::Warning, "GoldenDict", + tr( "Error in favorities file" ), + QMessageBox::Ok ); + mb.exec(); + + dom.clear(); + favoritesFile.close(); + renameAtomically( m_favoritesFilename, m_favoritesFilename + ".bak" ); + } + else + favoritesFile.close(); + + QDomNode rootNode = dom.documentElement(); + addFolder( rootItem, rootNode ); + dom.clear(); + + endResetModel(); +} + +void FavoritesModel::saveData() +{ + QFile tmpFile( m_favoritesFilename + ".tmp" ); + if( !tmpFile.open( QFile::WriteOnly ) ) + { + gdWarning( "Can't write favorites file, error: %s", tmpFile.errorString().toUtf8().data() ); + return; + } + + dom.clear(); + + QDomElement el = dom.createElement( "root" ); + dom.appendChild( el ); + storeFolder( rootItem, el ); + + QByteArray result( dom.toByteArray() ); + + if ( tmpFile.write( result ) != result.size() ) + { + gdWarning( "Can't write favorites file, error: %s", tmpFile.errorString().toUtf8().data() ); + return; + } + + tmpFile.close(); + + renameAtomically( tmpFile.fileName(), m_favoritesFilename ); + + dom.clear(); +} + +void FavoritesModel::addFolder( TreeItem *parent, QDomNode &node ) +{ + QDomNodeList nodes = node.childNodes(); + for( int i = 0; i < nodes.count(); i++ ) + { + QDomElement el = nodes.at( i ).toElement(); + if( el.nodeName() == "folder" ) + { + // New subfolder + QString name = el.attribute( "name", "" ); + TreeItem *item = new TreeItem( name, parent, TreeItem::Folder ); + item->setExpanded( el.attribute( "expanded", "0" ) == "1" ); + parent->appendChild( item ); + addFolder( item, el ); + } + else + { + QString word = el.text(); + parent->appendChild( new TreeItem( word, parent, TreeItem::Word ) ); + } + } +} + +void FavoritesModel::storeFolder( TreeItem * folder, QDomNode & node ) +{ + int n = folder->childCount(); + for( int i = 0; i < n; i++ ) + { + TreeItem * child = folder->child( i ); + QString name = child->data().toString(); + if( child->type() == TreeItem::Folder ) + { + QDomElement el = dom.createElement( "folder" ); + el.setAttribute( "name", name ); + el.setAttribute( "expanded", child->isExpanded() ? "1" : "0" ); + node.appendChild( el ); + storeFolder( child, el ); + } + else + { + QDomElement el = dom.createElement( "headword" ); + el.appendChild( dom.createTextNode( name ) ); + node.appendChild( el ); + } + } +} + +void FavoritesModel::itemExpanded( const QModelIndex & index ) +{ + if( index.isValid() ) + { + TreeItem *item = getItem( index ); + item->setExpanded( true ); + } +} + +void FavoritesModel::itemCollapsed( const QModelIndex & index ) +{ + if( index.isValid() ) + { + TreeItem *item = getItem( index ); + item->setExpanded( false ); + } +} + +void FavoritesModel::checkAllNodesForExpand() +{ + checkNodeForExpand( rootItem, QModelIndex() ); +} + +void FavoritesModel::checkNodeForExpand( const TreeItem * item, const QModelIndex & parent ) +{ + for( int i = 0; i < item->childCount(); i++ ) + { + TreeItem * ch = item->child( i ); + if( ch->type() == TreeItem::Folder && ch->isExpanded() ) + { + // We need to expand this node... + QModelIndex idx = index( i, 0, parent ); + emit expandItem( idx ); + + // ...and check it for children nodes + checkNodeForExpand( item->child( i ), idx ); + } + } +} + +QStringList FavoritesModel::mimeTypes() const +{ + return QStringList( QString::fromLatin1( "text/plain" ) ); +} + +QMimeData *FavoritesModel::mimeData( const QModelIndexList &indexes ) const +{ + QString headwords; + + QList< QModelIndex >::const_iterator it = indexes.begin(); + for ( ; it != indexes.end(); ++it ) + { + TreeItem *item = getItem( *it ); + headwords += item->fullPath() + "\n\r"; + headwords += item->data().toString() + "\n\r"; + headwords += QString::number( item->type() ) + "\n\r"; + } + + QMimeData *data = new QMimeData(); + data->setText( headwords ); + return data; +} + +bool FavoritesModel::dropMimeData(const QMimeData *data, Qt::DropAction action, + int row, int, const QModelIndex &par) +{ + if( action == Qt::MoveAction || action == Qt::CopyAction ) + { + QList< QModelIndex > list; + list = itemsListFromText( data->text() ); + + if( list.isEmpty() ) + return false; + + TreeItem * parentItem = getItem( par ); + QModelIndex parentIdx = par; + + if( row < 0 ) + row = 0; + + QList< QModelIndex >::const_iterator it = list.begin(); + QList< TreeItem * > movedItems; + for( ; it != list.end(); ++it ) + { + TreeItem * item = getItem( *it ); + + // Check if we can copy/move this item + if( parentItem->haveAncestor( item ) || parentItem->haveSameItem( item, action == Qt::MoveAction ) ) + return false; + + movedItems.append( item ); + } + + // Insert items to new place + + beginInsertRows( parentIdx, row, row + movedItems.count() - 1 ); + for( int i = 0; i < movedItems.count(); i++ ) + { + TreeItem * item = movedItems.at( i ); + TreeItem *newItem = item->duplicateItem( parentItem ); + parentItem->insertChild( row + i, newItem ); + } + endInsertRows(); + + return true; + } + return false; +} + +QModelIndex FavoritesModel::findItemInFolder( const QString & itemName, int itemType, + const QModelIndex & parentIdx ) +{ + TreeItem * parentItem = getItem( parentIdx ); + for( int i = 0; i < parentItem->childCount(); i++ ) + { + TreeItem * item = parentItem->child( i ); + if( item->data().toString() == itemName && item->type() == itemType ) + return createIndex( i, 0, item ); + } + return QModelIndex(); +} + +QModelIndex FavoritesModel::findItem( const QString & path, + const QString & headword, + int type ) +{ + TreeItem *par = rootItem; + + if( !path.isEmpty() ) + { + // Find target folder + + QStringList folders = path.split( "/", QString::SkipEmptyParts ); + QStringList::const_iterator it = folders.begin(); + for( ; it != folders.end(); ++it ) + { + int i; + for( i = 0; i < par->childCount(); i++ ) + { + TreeItem * item = par->child( i ); + QString name = item->data().toString(); + if( *it == name && item->type() == TreeItem::Folder ) + break; + } + if( i >= par->childCount() ) // Folder not found + return QModelIndex(); + + par = par->child( i ); + } + } + + // Find headword/folder in target folder + + for( int i = 0; i < par->childCount(); i++ ) + { + TreeItem * item = par->child( i ); + QString name = item->data().toString(); + if( name == headword && item->type() == type ) + { + // Item found, create index + return createIndex( i, 0, item ); + } + } + return QModelIndex(); +} + +QList< QModelIndex > FavoritesModel::itemsListFromText( const QString & text ) +{ + int pos = 0; + QString delim = QString::fromLatin1( "\n\r" ); + QList< QModelIndex > list; + + for( ; ; ) + { + int delimPos = text.indexOf( delim, pos ); + if( delimPos < 0 ) + break; + QString path = text.mid( pos, delimPos - pos ); + + pos = delimPos + 2; + delimPos = text.indexOf( delim, pos ); + if( delimPos < 0 ) + break; + QString headword = text.mid( pos, delimPos - pos ); + + pos = delimPos + 2; + delimPos = text.indexOf( delim, pos ); + if( delimPos < 0 ) + break; + int type = text.mid( pos, delimPos - pos ).toInt(); + + QModelIndex idx = findItem( path, headword, type ); + if( idx.isValid() ) + list.append( idx ); + + pos = delimPos + 2; + } + + return list; +} + +TreeItem *FavoritesModel::getItem( const QModelIndex &index ) const +{ + if( index.isValid() ) + { + TreeItem *item = static_cast< TreeItem * >( index.internalPointer() ); + if (item) + return item; + } + return rootItem; +} + +QStringList FavoritesModel::getTextForIndexes( const QModelIndexList & idxList ) const +{ + QStringList list; + QModelIndexList::const_iterator it = idxList.begin(); + for( ; it != idxList.end(); ++it ) + { + TreeItem *item = getItem( *it ); + if( item->type() == TreeItem::Word ) + list.append( item->data().toString() ); + else + list.append( item->getTextFromAllChilds() ); + } + return list; +} + +void FavoritesModel::removeItemsForIndexes( const QModelIndexList & idxList ) +{ + // We should delete items from lowest tree level and in decreasing order + // so that first deletions won't affect the indexes for subsequent deletions. + + QMap< int, QModelIndexList > itemsToDelete; + int lowestLevel = 0; + + QModelIndexList::const_iterator it = idxList.begin(); + for( ; it != idxList.end(); ++it ) + { + int n = level( *it ); + if( n > lowestLevel ) + lowestLevel = n; + itemsToDelete[ n ].append( *it ); + } + + for( int i = lowestLevel; i >= 0; i-- ) + { + QModelIndexList idxSublist = itemsToDelete[ i ]; + qSort( idxSublist.begin(), idxSublist.end(), qGreater< QModelIndex >() ); + + it = idxSublist.begin(); + for( ; it != idxSublist.end(); ++it ) + { + QModelIndex parentIdx = parent( *it ); + removeRows( (*it).row(), 1, parentIdx ); + } + } +} + +QModelIndex FavoritesModel::addNewFolder( const QModelIndex & idx ) +{ + QModelIndex parentIdx = parent( idx ); + QString baseName = QString::fromLatin1( "New folder" ); + + // Create unique name + + QString name = baseName; + if( findItemInFolder( name, TreeItem::Folder, parentIdx ).isValid() ) + { + int i; + for( i = 1; i < 1000; i++ ) + { + name = baseName + QString::number( i ); + if( !findItemInFolder( name, TreeItem::Folder, parentIdx ).isValid() ) + break; + } + if( i >= 1000 ) + return QModelIndex(); + } + + // Create folder with unique name + + TreeItem *parentItem = getItem( parentIdx ); + int row = idx.row() + 1; + + beginInsertRows( parentIdx, row, row ); + TreeItem * newFolder = new TreeItem( name, parentItem, TreeItem::Folder ); + parentItem->insertChild( row, newFolder ); + endInsertRows(); + + return createIndex( row, 0, newFolder ); +} + +bool FavoritesModel::addNewHeadword( const QString & path, const QString & headword ) +{ + QModelIndex parentIdx; + + // Find or create target folder + + QStringList folders = path.split( "/", QString::SkipEmptyParts ); + QStringList::const_iterator it = folders.begin(); + for( ; it != folders.end(); ++it ) + parentIdx = forceFolder( *it, parentIdx ); + + // Add headword + + return addHeadword( headword, parentIdx ); +} + +QModelIndex FavoritesModel::forceFolder( QString const & name, const QModelIndex & parentIdx ) +{ + QModelIndex idx = findItemInFolder( name, TreeItem::Folder, parentIdx ); + if( idx.isValid() ) + return idx; + + // Folder not found, create it + TreeItem * parentItem = getItem( parentIdx ); + TreeItem * newItem = new TreeItem( name, parentItem, TreeItem::Folder ); + int row = parentItem->childCount(); + + beginInsertRows( parentIdx, row, row ); + parentItem->appendChild( newItem ); + endInsertRows(); + + return createIndex( row, 0, newItem ); +} + +bool FavoritesModel::addHeadword( const QString & word, const QModelIndex & parentIdx ) +{ + QModelIndex idx = findItemInFolder( word, TreeItem::Word, parentIdx ); + if( idx.isValid() ) + return false; + + // Headword not found, append it + TreeItem * parentItem = getItem( parentIdx ); + TreeItem * newItem = new TreeItem( word, parentItem, TreeItem::Word ); + int row = parentItem->childCount(); + + beginInsertRows( parentIdx, row, row ); + parentItem->appendChild( newItem ); + endInsertRows(); + + return true; +} + +int FavoritesModel::level( QModelIndex const & idx ) +{ + int n = 0; + QModelIndex parentIdx = parent( idx ); + while( parentIdx.isValid() ) + { + n++; + parentIdx = parent( parentIdx ); + } + return n; +} + +QString FavoritesModel::pathToItem( QModelIndex const & idx ) +{ + QString path; + QModelIndex parentIdx = parent( idx ); + while( parentIdx.isValid() ) + { + if( !path.isEmpty() ) + path = "/" + path; + + path = data( parentIdx, Qt::DisplayRole ).toString() + path; + + parentIdx = parent( parentIdx ); + } + return path; +} + +void FavoritesModel::getDataInXml( QByteArray & dataStr ) +{ + dom.clear(); + + QDomElement el = dom.createElement( "root" ); + dom.appendChild( el ); + storeFolder( rootItem, el ); + + dataStr = dom.toByteArray(); + dom.clear(); +} + +void FavoritesModel::getDataInPlainText( QString & dataStr ) +{ + QModelIndexList list; + list.append( QModelIndex() ); + dataStr = getTextForIndexes( list ).join( QString::fromLatin1( "\n" ) ); +} + +bool FavoritesModel::setDataFromXml( QString const & dataStr ) +{ + QString errorStr; + int errorLine, errorColumn; + dom.clear(); + + if ( !dom.setContent( dataStr, false, &errorStr, &errorLine, &errorColumn ) ) + { + // Mailformed data + QString errStr; + errStr.sprintf( "Error: %s at %d,%d\n", errorStr.toLocal8Bit().constData(), errorLine, errorColumn ); + gdWarning( errStr.toUtf8().data() ); + dom.clear(); + return false; + } + + beginResetModel(); + + if( rootItem ) + delete rootItem; + + rootItem = new TreeItem( QVariant(), 0, TreeItem::Root ); + + QDomNode rootNode = dom.documentElement(); + addFolder( rootItem, rootNode ); + + endResetModel(); + + dom.clear(); + return true; +} diff --git a/favoritespanewidget.hh b/favoritespanewidget.hh new file mode 100644 index 00000000..f74ec817 --- /dev/null +++ b/favoritespanewidget.hh @@ -0,0 +1,236 @@ +/* This file is (c) 2017 Abs62 + * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */ + +#ifndef __FAVORITIESPANEWIDGET_HH__INCLUDED__ +#define __FAVORITIESPANEWIDGET_HH__INCLUDED__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "delegate.hh" + +class FavoritesModel; + +class FavoritesPaneWidget : public QWidget +{ + Q_OBJECT +public: + FavoritesPaneWidget( QWidget * parent = 0 ): QWidget( parent ), + itemSelectionChanged( false ) + , listItemDelegate( 0 ) + , m_favoritesModel( 0 ) + {} + + virtual ~FavoritesPaneWidget(); + + virtual QSize sizeHint() const + { return QSize( 204, 204 ); } + + void setUp( Config::Class * cfg, QMenu * menu ); + + void addHeadword( QString const & path, QString const & headword ); + + // Export/import Favorites + void getDataInXml( QByteArray & dataStr ); + void getDataInPlainText( QString & dataStr ); + bool setDataFromXml( QString const & dataStr ); + +signals: + void favoritesItemRequested( QString const & word, QString const & faforitesFolder ); + +private slots: + void emitFavoritesItemRequested(QModelIndex const &); + void onSelectionChanged(QItemSelection const & selection); + void onItemClicked(QModelIndex const & idx); + void showCustomMenu(QPoint const & pos); + void deleteSelectedItems(); + void copySelectedItems(); + void addFolder(); + +private: + virtual bool eventFilter( QObject *, QEvent * ); + + Config::Class * m_cfg ; + QTreeView * m_favoritesTree; + QMenu * m_favoritesMenu; + QAction * m_deleteSelectedAction; + QAction * m_separator; + QAction * m_copySelectedToClipboard; + QAction * m_addFolder; + + QWidget favoritesPaneTitleBar; + QHBoxLayout favoritesPaneTitleBarLayout; + QLabel favoritesLabel; + + /// needed to avoid multiple notifications + /// when selecting items via mouse and keyboard + bool itemSelectionChanged; + + WordListItemDelegate * listItemDelegate; + FavoritesModel * m_favoritesModel; +}; + + +class TreeItem +{ +public: + enum Type { Word, Folder, Root }; + + TreeItem( const QVariant &data, TreeItem *parent = 0, Type type_ = Word ); + ~TreeItem(); + + void appendChild( TreeItem * child ); + + void insertChild( int row, TreeItem * item ); + + // Remove child from list and delete it + void deleteChild( int row ); + + TreeItem * child( int row ) const; + int childCount() const; + QVariant data() const; + void setData( const QVariant & newData ); + int row() const; + TreeItem * parent(); + + Type type() const + { return m_type; } + + Qt::ItemFlags flags() const; + + void setExpanded( bool expanded ) + { m_expanded = expanded; } + + bool isExpanded() const + { return m_expanded; } + + // Full path from root folder + QString fullPath() const; + + // Duplicate item with all childs + TreeItem * duplicateItem( TreeItem * newParent ) const; + + // Check if item is ancestor of this element + bool haveAncestor( TreeItem * item ); + + // Check if same item already presented between childs + bool haveSameItem( TreeItem * item, bool allowSelf ); + + // Retrieve text from all childs + QStringList getTextFromAllChilds() const; + +private: + QList< TreeItem * > childItems; + QVariant itemData; + TreeItem *parentItem; + Type m_type; + bool m_expanded; +}; + +class FavoritesModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit FavoritesModel( QString favoritesFilename, QObject * parent = 0 ); + ~FavoritesModel(); + + QVariant data( const QModelIndex &index, int role ) const; + Qt::ItemFlags flags( const QModelIndex &index ) const; + QVariant headerData( int section, Qt::Orientation orientation, + int role = Qt::DisplayRole ) const; + QModelIndex index( int row, int column, + const QModelIndex &parent = QModelIndex() ) const; + QModelIndex parent( const QModelIndex &index ) const; + int rowCount( const QModelIndex &parent = QModelIndex() ) const; + int columnCount( const QModelIndex &parent = QModelIndex() ) const; + bool removeRows( int row, int count, const QModelIndex &parent ); + bool setData( const QModelIndex &index, const QVariant &value, int role ); + + // Drag & drop support + Qt::DropActions supportedDropActions() const; + QStringList mimeTypes() const; + QMimeData *mimeData(const QModelIndexList &indexes) const; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, + int row, int column, const QModelIndex &par); + + // Restore nodes expanded state after data loading + void checkNodeForExpand( const TreeItem * item, const QModelIndex &parent ); + void checkAllNodesForExpand(); + + // Retrieve text data for indexes + QStringList getTextForIndexes( QModelIndexList const & idxList ) const; + + // Delete items for indexes + void removeItemsForIndexes( QModelIndexList const & idxList ); + + // Add new folder beside item and return its index + // or empty index if fail + QModelIndex addNewFolder( QModelIndex const & idx ); + + // Add new headword to given folder + // return false if it already exists there + bool addNewHeadword( QString const & path, QString const & headword ); + + // Return path in the tree to item + QString pathToItem( QModelIndex const & idx ); + + TreeItem::Type itemType( QModelIndex const & idx ) + { return getItem( idx )->type(); } + + // Export/import Favorites + void getDataInXml( QByteArray & dataStr ); + void getDataInPlainText( QString & dataStr ); + bool setDataFromXml( QString const & dataStr ); + +public slots: + void itemCollapsed ( const QModelIndex & index ); + void itemExpanded ( const QModelIndex & index ); + +signals: + void expandItem( const QModelIndex & index ); + +protected: + void readData(); + void saveData(); + void addFolder( TreeItem * parent, QDomNode & node ); + void storeFolder( TreeItem * folder, QDomNode & node ); + + // Find item in folder + QModelIndex findItemInFolder( QString const & itemName, int itemType, + QModelIndex const & parentIdx ); + + // Find item by it params + QModelIndex findItem( QString const & path, + QString const & headword, + int type ); + + // Create items list from mime "plait/text" data + QList< QModelIndex > itemsListFromText( QString const & text ); + + TreeItem *getItem( const QModelIndex &index ) const; + + // Find folder with given name or create it if folder not exist + QModelIndex forceFolder( QString const & name, QModelIndex const & parentIdx ); + + // Add headword to given folder + // return false if such headwordalready exists + bool addHeadword( QString const & word, QModelIndex const & parentIdx ); + + // Return tree level for item + int level( QModelIndex const & idx ); + +private: + QString m_favoritesFilename; + TreeItem * rootItem; + QDomDocument dom; +}; + +#endif // __FAVORITIESPANEWIDGET_HH__INCLUDED__ diff --git a/goldendict.pro b/goldendict.pro index cde83454..6fcfdbee 100644 --- a/goldendict.pro +++ b/goldendict.pro @@ -356,7 +356,8 @@ HEADERS += folding.hh \ slob.hh \ ripemd.hh \ gls.hh \ - splitfile.hh + splitfile.hh \ + favoritespanewidget.hh FORMS += groups.ui \ dictgroupwidget.ui \ @@ -482,7 +483,8 @@ SOURCES += folding.cc \ slob.cc \ ripemd.cc \ gls.cc \ - splitfile.cc + splitfile.cc \ + favoritespanewidget.cc win32 { FORMS += texttospeechsource.ui diff --git a/groups_widgets.cc b/groups_widgets.cc index 8932e606..510e96c5 100644 --- a/groups_widgets.cc +++ b/groups_widgets.cc @@ -63,6 +63,8 @@ DictGroupWidget::DictGroupWidget( QWidget * parent, ui.shortcut->setHotKey( Config::HotKey( group.shortcut ) ); + ui.favoritesFolder->setText( group.favoritesFolder ); + connect( ui.groupIcon, SIGNAL(activated(int)),this,SLOT(groupIconActivated(int)), Qt::QueuedConnection ); @@ -129,6 +131,8 @@ Config::Group DictGroupWidget::makeGroup() const g.shortcut = ui.shortcut->getHotKey().toKeySequence(); + g.favoritesFolder = ui.favoritesFolder->text().replace( '\\', '/' ); + return g.makeConfigGroup(); } diff --git a/history.cc b/history.cc index 2239afe5..866b3d1c 100644 --- a/history.cc +++ b/history.cc @@ -9,6 +9,8 @@ History::History( unsigned size, unsigned maxItemLength_ ): maxSize( size ), maxItemLength( maxItemLength_ ), addingEnabled( true ) +, dirty( false ) +, timerId ( 0 ) { } diff --git a/icons/folder.png b/icons/folder.png new file mode 100644 index 00000000..80d51b3e Binary files /dev/null and b/icons/folder.png differ diff --git a/icons/star.png b/icons/star.png new file mode 100644 index 00000000..26d8eb0d Binary files /dev/null and b/icons/star.png differ diff --git a/instances.cc b/instances.cc index a9af71ae..000bbd1b 100644 --- a/instances.cc +++ b/instances.cc @@ -15,6 +15,7 @@ Group::Group( Config::Group const & cfgGroup, id( cfgGroup.id ), name( cfgGroup.name ), icon( cfgGroup.icon ), + favoritesFolder( cfgGroup.favoritesFolder ), shortcut( cfgGroup.shortcut ) { if ( !cfgGroup.iconData.isEmpty() ) @@ -82,6 +83,7 @@ Config::Group Group::makeConfigGroup() result.name = name; result.icon = icon; result.shortcut = shortcut; + result.favoritesFolder = favoritesFolder; if ( !iconData.isNull() ) { diff --git a/instances.hh b/instances.hh index 58dd31df..1cac20df 100644 --- a/instances.hh +++ b/instances.hh @@ -21,7 +21,7 @@ using std::vector; struct Group { unsigned id; - QString name, icon; + QString name, icon, favoritesFolder; QIcon iconData; QKeySequence shortcut; vector< sptr< Dictionary::Class > > dictionaries; diff --git a/mainwindow.cc b/mainwindow.cc index 8e1cc592..c826eb75 100644 --- a/mainwindow.cc +++ b/mainwindow.cc @@ -238,6 +238,14 @@ MainWindow::MainWindow( Config::Class & cfg_ ): navToolbar->addAction( ui.print ); navToolbar->widgetForAction( ui.print )->setObjectName( "printButton" ); + navToolbar->widgetForAction( navToolbar->addSeparator() )->setObjectName( "separatorBeforeAddToFavorites" ); + + addToFavorites = navToolbar->addAction( QIcon( ":/icons/star.png" ), tr( "Add current tab to Favorites" ) ); + navToolbar->widgetForAction( addToFavorites )->setObjectName( "addToFavoritesButton" ); + + connect( addToFavorites, SIGNAL( triggered() ), this, SLOT( addCurrentTabToFavorites() ) ); + connect( ui.actionAddToFavorites, SIGNAL( triggered() ), this, SLOT( addCurrentTabToFavorites() ) ); + beforeOptionsSeparator = navToolbar->addSeparator(); navToolbar->widgetForAction( beforeOptionsSeparator )->setObjectName( "beforeOptionsSeparator" ); beforeOptionsSeparator->setVisible( cfg.preferences.hideMenubar); @@ -246,6 +254,7 @@ MainWindow::MainWindow( Config::Class & cfg_ ): buttonMenu->addAction( ui.dictionaries ); buttonMenu->addAction( ui.preferences ); buttonMenu->addSeparator(); + buttonMenu->addMenu( ui.menuFavorites ); buttonMenu->addMenu( ui.menuHistory ); buttonMenu->addSeparator(); buttonMenu->addMenu( ui.menuFile ); @@ -478,6 +487,8 @@ MainWindow::MainWindow( Config::Class & cfg_ ): ui.searchPane->toggleViewAction()->setShortcut( QKeySequence( "Ctrl+S" ) ); ui.menuView->addAction( ui.dictsPane->toggleViewAction() ); ui.dictsPane->toggleViewAction()->setShortcut( QKeySequence( "Ctrl+R" ) ); + ui.menuView->addAction( ui.favoritesPane->toggleViewAction() ); + ui.favoritesPane->toggleViewAction()->setShortcut( QKeySequence( "Ctrl+I" ) ); ui.menuView->addAction( ui.historyPane->toggleViewAction() ); ui.historyPane->toggleViewAction()->setShortcut( QKeySequence( "Ctrl+H" ) ); ui.menuView->addSeparator(); @@ -536,6 +547,19 @@ MainWindow::MainWindow( Config::Class & cfg_ ): connect( &dictionaryBar, SIGNAL( openDictionaryFolder( QString const & ) ), this, SLOT( openDictionaryFolder( QString const & ) ) ); + // Favorites + + ui.favoritesPaneWidget->setUp( &cfg, ui.menuFavorites ); + + connect( ui.favoritesPane, SIGNAL( visibilityChanged( bool ) ), + this, SLOT( updateFavoritesMenu() ) ); + + connect( ui.menuFavorites, SIGNAL( aboutToShow() ), + this, SLOT( updateFavoritesMenu() ) ); + + connect( ui.favoritesPaneWidget, SIGNAL( favoritesItemRequested( QString, QString ) ), + this, SLOT( headwordFromFavorites( QString, QString ) ) ); + // History ui.historyPaneWidget->setUp( &cfg, &history, ui.menuHistory ); history.enableAdd( cfg.preferences.storeHistory ); @@ -972,6 +996,7 @@ void MainWindow::addGlobalAction( QAction * action, const char * slot ) ui.centralWidget->addAction( action ); ui.dictsPane->addAction( action ); ui.searchPaneWidget->addAction( action ); + ui.favoritesPane->addAction( action ); ui.historyPane->addAction( action ); groupList->addAction( action ); translateBox->addAction( action ); @@ -2261,6 +2286,15 @@ bool MainWindow::eventFilter( QObject * obj, QEvent * ev ) return true; } + // Handle Ctrl+I to show the Favorities Pane. + if ( ke->key() == Qt::Key_I && ke->modifiers() == Qt::ControlModifier ) + { + if( ev->type() == QEvent::KeyPress ) + on_showHideFavorites_triggered(); + ev->accept(); + return true; + } + // Handle F3/Shift+F3 shortcuts if ( ke->key() == Qt::Key_F3 ) { @@ -3606,6 +3640,18 @@ void MainWindow::headwordReceived( const QString & word, const QString & ID ) translateInputFinished( false, QString( "gdfrom-" )+ ID ); } +void MainWindow::updateFavoritesMenu() +{ + if ( ui.favoritesPane->toggleViewAction()->isChecked() ) + { + ui.showHideFavorites->setText( tr( "&Hide" ) ); + } + else + { + ui.showHideFavorites->setText( tr( "&Show" ) ); + } +} + void MainWindow::updateHistoryMenu() { if ( ui.historyPane->toggleViewAction()->isChecked() ) @@ -3618,6 +3664,12 @@ void MainWindow::updateHistoryMenu() } } +void MainWindow::on_showHideFavorites_triggered() +{ + ui.favoritesPane->toggleViewAction()->trigger(); + ui.favoritesPane->raise(); // useful when the Pane is tabbed. +} + void MainWindow::on_showHideHistory_triggered() { ui.historyPane->toggleViewAction()->trigger(); @@ -3759,6 +3811,145 @@ void MainWindow::on_importHistory_triggered() mainStatusBar->showMessage( errStr, 10000, QPixmap( ":/icons/error.png" ) ); } +void MainWindow::on_exportFavorites_triggered() +{ + QString exportPath; + if( cfg.historyExportPath.isEmpty() ) + exportPath = QDir::homePath(); + else + { + exportPath = QDir::fromNativeSeparators( cfg.historyExportPath ); + if( !QDir( exportPath ).exists() ) + exportPath = QDir::homePath(); + } + + QString fileName = QFileDialog::getSaveFileName( this, tr( "Export Favorites to file" ), + exportPath, + tr( "XML files (*.xml);;All files (*.*)" ) ); + if( fileName.size() == 0) + return; + + cfg.historyExportPath = QDir::toNativeSeparators( QFileInfo( fileName ).absoluteDir().absolutePath() ); + QFile file( fileName ); + + for(;;) + { + if ( !file.open( QFile::WriteOnly | QIODevice::Text ) ) + break; + + QByteArray data; + ui.favoritesPaneWidget->getDataInXml( data ); + + if( file.write( data ) != data.size() ) + break; + + file.close(); + mainStatusBar->showMessage( tr( "Favorites export complete" ), 5000 ); + return; + } + QString errStr = QString( tr( "Export error: " ) ) + file.errorString(); + file.close(); + mainStatusBar->showMessage( errStr, 10000, QPixmap( ":/icons/error.png" ) ); +} + +void MainWindow::on_ExportFavoritesToList_triggered() +{ + QString exportPath; + if( cfg.historyExportPath.isEmpty() ) + exportPath = QDir::homePath(); + else + { + exportPath = QDir::fromNativeSeparators( cfg.historyExportPath ); + if( !QDir( exportPath ).exists() ) + exportPath = QDir::homePath(); + } + + QString fileName = QFileDialog::getSaveFileName( this, tr( "Export Favorites to file as plain list" ), + exportPath, + tr( "Text files (*.txt);;All files (*.*)" ) ); + if( fileName.size() == 0) + return; + + cfg.historyExportPath = QDir::toNativeSeparators( QFileInfo( fileName ).absoluteDir().absolutePath() ); + QFile file( fileName ); + + for(;;) + { + if ( !file.open( QFile::WriteOnly | QIODevice::Text ) ) + break; + + // Write UTF-8 BOM + QByteArray line; + line.append( 0xEF ).append( 0xBB ).append( 0xBF ); + if ( file.write( line ) != line.size() ) + break; + + // Write Favorites + QString data; + ui.favoritesPaneWidget->getDataInPlainText( data ); + + line = data.toUtf8(); + if( file.write( line ) != line.size() ) + break; + + file.close(); + mainStatusBar->showMessage( tr( "Favorites export complete" ), 5000 ); + return; + } + QString errStr = QString( tr( "Export error: " ) ) + file.errorString(); + file.close(); + mainStatusBar->showMessage( errStr, 10000, QPixmap( ":/icons/error.png" ) ); +} + +void MainWindow::on_importFavorites_triggered() +{ + QString importPath; + if( cfg.historyExportPath.isEmpty() ) + importPath = QDir::homePath(); + else + { + importPath = QDir::fromNativeSeparators( cfg.historyExportPath ); + if( !QDir( importPath ).exists() ) + importPath = QDir::homePath(); + } + + QString fileName = QFileDialog::getOpenFileName( this, tr( "Import Favorites from file" ), + importPath, + tr( "XML files (*.xml);;All files (*.*)" ) ); + if( fileName.size() == 0) + return; + + QFileInfo fileInfo( fileName ); + cfg.historyExportPath = QDir::toNativeSeparators( fileInfo.absoluteDir().absolutePath() ); + QString errStr; + QFile file( fileName ); + + for(;;) + { + if ( !file.open( QFile::ReadOnly | QIODevice::Text ) ) + break; + + if( file.error() != QFile::NoError ) + break; + + QByteArray data = file.readAll(); + + if( !ui.favoritesPaneWidget->setDataFromXml( QString::fromUtf8( data.data(), data.size() ) ) ) + break; + + file.close(); + mainStatusBar->showMessage( tr( "Favorites import complete" ), 5000 ); + return; + } + if( file.error() != QFile::NoError ) + errStr = QString( tr( "Import error: " ) ) + file.errorString(); + else + errStr = QString( tr( "Data parsing error" ) ); + + file.close(); + mainStatusBar->showMessage( errStr, 10000, QPixmap( ":/icons/error.png" ) ); +} + void MainWindow::fillWordListFromHistory() { ui.wordList->setUpdatesEnabled( false ); @@ -4240,6 +4431,18 @@ void MainWindow::showFTSIndexingName( QString const & name ) mainStatusBar->setBackgroundMessage( tr( "Now indexing for full-text search: " ) + name ); } +void MainWindow::addCurrentTabToFavorites() +{ + QString folder; + Instances::Group const * igrp = groupInstances.findGroup( cfg.lastMainGroupId ); + if( igrp ) + folder = igrp->favoritesFolder; + + QString headword = ui.tabWidget->tabText( ui.tabWidget->currentIndex() ); + + ui.favoritesPaneWidget->addHeadword( folder, headword ); +} + void MainWindow::setGroupByName( QString const & name, bool main_window ) { if( main_window ) @@ -4262,6 +4465,27 @@ void MainWindow::setGroupByName( QString const & name, bool main_window ) } } +void MainWindow::headwordFromFavorites( QString const & headword, + QString const & favoritesFolder ) +{ + if( !favoritesFolder.isEmpty() ) + { + // Find group by it Favorites folder + for( Instances::Groups::size_type i = 0; i < groupInstances.size(); i++ ) + { + if( groupInstances[ i ].favoritesFolder == favoritesFolder ) + { + // Group found. Select it and stop search. + if( groupList->currentIndex() != (int)i ) + groupList->setCurrentIndex( i ); + break; + } + } + } + + wordReceived( headword ); +} + #ifdef Q_OS_WIN32 bool MainWindow::handleGDMessage( MSG * message, long * result ) diff --git a/mainwindow.hh b/mainwindow.hh index 4b2ce1ff..84cc6bbb 100644 --- a/mainwindow.hh +++ b/mainwindow.hh @@ -84,6 +84,7 @@ public slots: void wordReceived( QString const & ); void headwordReceived( QString const &, QString const & ); void setExpandMode( bool expand ); + void headwordFromFavorites( QString const &, QString const & ); private: void addGlobalAction( QAction * action, const char * slot ); @@ -127,6 +128,7 @@ private: QAction * beforeScanPopupSeparator, * afterScanPopupSeparator, * beforeOptionsSeparator; QAction * zoomIn, * zoomOut, * zoomBase; QAction * wordsZoomIn, * wordsZoomOut, * wordsZoomBase; + QAction * addToFavorites, * beforeAddToFavoritesSeparator; QMenu trayIconMenu; QMenu * tabMenu; QAction * menuButtonAction; @@ -276,6 +278,8 @@ private slots: void showFTSIndexingName( QString const & name ); + void addCurrentTabToFavorites(); + private slots: // Executed in response to a user click on an 'add tab' tool button @@ -412,14 +416,20 @@ private slots: void on_rescanFiles_triggered(); + void on_showHideFavorites_triggered(); void on_showHideHistory_triggered(); void on_exportHistory_triggered(); void on_importHistory_triggered(); void on_alwaysOnTop_triggered( bool checked ); void focusWordList(); + void on_exportFavorites_triggered(); + void on_importFavorites_triggered(); + void on_ExportFavoritesToList_triggered(); + void updateSearchPaneAndBar( bool searchInDock ); + void updateFavoritesMenu(); void updateHistoryMenu(); /// Add word to history diff --git a/mainwindow.ui b/mainwindow.ui index 71ba3540..c6890c49 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -138,11 +138,23 @@ + + + Fa&vorites + + + + + + + + + @@ -299,6 +311,33 @@ + + + Favor&ites Pane + + + 2 + + + + + 2 + + + 1 + + + 2 + + + 1 + + + + + + + &History Pane @@ -307,7 +346,7 @@ 2 - + 2 @@ -594,6 +633,37 @@ F1 + + + Show + + + Ctrl+I + + + + + Export + + + + + Import + + + + + Add + + + Add current tab to Favorites + + + + + Export to list + + @@ -625,6 +695,12 @@ QListWidget
wordlist.hh
+ + FavoritesPaneWidget + QWidget +
favoritespanewidget.hh
+ 1 +
translateLine diff --git a/qt-style-st-babylon.css b/qt-style-st-babylon.css index d80a7760..f438c21b 100644 --- a/qt-style-st-babylon.css +++ b/qt-style-st-babylon.css @@ -1,5 +1,5 @@ MainWindow #translateLine, ScanPopup #translateLine, MainWindow #searchPane #wordList, MainWindow #dictsPane #dictsList, -MainWindow #historyPane #historyList +MainWindow #historyPane #historyList, MainWindow #favoritesPane #favoritesTree { background: white; } diff --git a/qt-style-st-lingoes-blue.css b/qt-style-st-lingoes-blue.css index ccb1ad96..c7e5b1e8 100644 --- a/qt-style-st-lingoes-blue.css +++ b/qt-style-st-lingoes-blue.css @@ -73,7 +73,7 @@ QMainWindow::separator { margin: 2px; } -MainWindow #historyPane #historyList { +MainWindow #historyPane #historyList, MainWindow #favoritesPane #favoritesTree { background: #EAF0F8; color: #52627C; } diff --git a/qt-style-st-lingvo.css b/qt-style-st-lingvo.css index 04fa8ea9..64e87003 100644 --- a/qt-style-st-lingvo.css +++ b/qt-style-st-lingvo.css @@ -1,5 +1,5 @@ MainWindow #translateLine, ScanPopup #translateLine, MainWindow #wordList, MainWindow #dictsPane #dictsList, -MainWindow #historyPane #historyList +MainWindow #historyPane #historyList, MainWindow #favoritesPane #favoritesTree { background: white; } diff --git a/qt-style-st-modern.css b/qt-style-st-modern.css index 04c925bc..0f70b1e8 100644 --- a/qt-style-st-modern.css +++ b/qt-style-st-modern.css @@ -1,5 +1,5 @@ MainWindow #translateLine, ScanPopup #translateLine, MainWindow #wordList, MainWindow #dictsPane #dictsList, -MainWindow #historyPane #historyList +MainWindow #historyPane #historyList, MainWindow #favoritesPane #favoritesTree { background: white; } diff --git a/qt-style.css b/qt-style.css index 382dfec7..ea1120b6 100644 --- a/qt-style.css +++ b/qt-style.css @@ -1,4 +1,4 @@ -MainWindow #translateLine, ScanPopup #translateLine, MainWindow #wordList, MainWindow #dictsPane #dictsList, MainWindow #historyPane #historyList +MainWindow #translateLine, ScanPopup #translateLine, MainWindow #wordList, MainWindow #dictsPane #dictsList, MainWindow #historyPane #historyList, MainWindow #favoritesPane #favoritesTree { background: #fefdeb; color: black; diff --git a/resources.qrc b/resources.qrc index 9ffb7f99..ce07306e 100644 --- a/resources.qrc +++ b/resources.qrc @@ -80,5 +80,7 @@ icons/icon32_epwing.png icons/icon32_slob.png icons/icon32_gls.png + icons/star.png + icons/folder.png