blob: 4272b612ec12e0ab8910b069f0a16e93863be816 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qsidebar_p.h"
#include "qfilesystemmodel.h"
#include <qaction.h>
#include <qurl.h>
#if QT_CONFIG(menu)
#include <qmenu.h>
#endif
#include <qmimedata.h>
#include <qevent.h>
#include <qdebug.h>
#include <qfileiconprovider.h>
#include <qfiledialog.h>
QT_BEGIN_NAMESPACE
void QSideBarDelegate::initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const
{
QStyledItemDelegate::initStyleOption(option,index);
QVariant value = index.data(QUrlModel::EnabledRole);
if (value.isValid()) {
//If the bookmark/entry is not enabled then we paint it in gray
if (!qvariant_cast<bool>(value))
option->state &= ~QStyle::State_Enabled;
}
}
/*!
\internal
\class QUrlModel
QUrlModel lets you have indexes from a QFileSystemModel to a list. When QFileSystemModel
changes them QUrlModel will automatically update.
Example usage: File dialog sidebar and combo box
*/
QUrlModel::QUrlModel(QObject *parent) : QStandardItemModel(parent), showFullPath(false), fileSystemModel(nullptr)
{
}
/*!
\reimp
*/
QStringList QUrlModel::mimeTypes() const
{
return QStringList(QLatin1String("text/uri-list"));
}
/*!
\reimp
*/
Qt::ItemFlags QUrlModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags flags = QStandardItemModel::flags(index);
if (index.isValid()) {
flags &= ~Qt::ItemIsEditable;
// ### some future version could support "moving" urls onto a folder
flags &= ~Qt::ItemIsDropEnabled;
}
if (index.data(Qt::DecorationRole).isNull())
flags &= ~Qt::ItemIsEnabled;
return flags;
}
/*!
\reimp
*/
QMimeData *QUrlModel::mimeData(const QModelIndexList &indexes) const
{
QList<QUrl> list;
for (const auto &index : indexes) {
if (index.column() == 0)
list.append(index.data(UrlRole).toUrl());
}
QMimeData *data = new QMimeData();
data->setUrls(list);
return data;
}
#if QT_CONFIG(draganddrop)
/*!
Decide based upon the data if it should be accepted or not
We only accept dirs and not files
*/
bool QUrlModel::canDrop(QDragEnterEvent *event)
{
if (!event->mimeData()->formats().contains(mimeTypes().constFirst()))
return false;
const QList<QUrl> list = event->mimeData()->urls();
for (const auto &url : list) {
const QModelIndex idx = fileSystemModel->index(url.toLocalFile());
if (!fileSystemModel->isDir(idx))
return false;
}
return true;
}
/*!
\reimp
*/
bool QUrlModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int column, const QModelIndex &parent)
{
if (!data->formats().contains(mimeTypes().constFirst()))
return false;
Q_UNUSED(action);
Q_UNUSED(column);
Q_UNUSED(parent);
addUrls(data->urls(), row);
return true;
}
#endif // QT_CONFIG(draganddrop)
/*!
\reimp
If the role is the UrlRole then handle otherwise just pass to QStandardItemModel
*/
bool QUrlModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (value.userType() == QMetaType::QUrl) {
QUrl url = value.toUrl();
QModelIndex dirIndex = fileSystemModel->index(url.toLocalFile());
//On windows the popup display the "C:\", convert to nativeSeparators
if (showFullPath)
QStandardItemModel::setData(index, QDir::toNativeSeparators(fileSystemModel->data(dirIndex, QFileSystemModel::FilePathRole).toString()));
else {
QStandardItemModel::setData(index, QDir::toNativeSeparators(fileSystemModel->data(dirIndex, QFileSystemModel::FilePathRole).toString()), Qt::ToolTipRole);
QStandardItemModel::setData(index, fileSystemModel->data(dirIndex).toString());
}
QStandardItemModel::setData(index, fileSystemModel->data(dirIndex, Qt::DecorationRole),
Qt::DecorationRole);
QStandardItemModel::setData(index, url, UrlRole);
return true;
}
return QStandardItemModel::setData(index, value, role);
}
void QUrlModel::setUrl(const QModelIndex &index, const QUrl &url, const QModelIndex &dirIndex)
{
setData(index, url, UrlRole);
if (url.path().isEmpty()) {
setData(index, fileSystemModel->myComputer());
setData(index, fileSystemModel->myComputer(Qt::DecorationRole), Qt::DecorationRole);
} else {
QString newName;
if (showFullPath) {
//On windows the popup display the "C:\", convert to nativeSeparators
newName = QDir::toNativeSeparators(dirIndex.data(QFileSystemModel::FilePathRole).toString());
} else {
newName = dirIndex.data().toString();
}
QIcon newIcon = qvariant_cast<QIcon>(dirIndex.data(Qt::DecorationRole));
if (!dirIndex.isValid()) {
const QFileIconProvider *provider = fileSystemModel->iconProvider();
if (provider)
newIcon = provider->icon(QFileIconProvider::Folder);
newName = QFileInfo(url.toLocalFile()).fileName();
if (!invalidUrls.contains(url))
invalidUrls.append(url);
//The bookmark is invalid then we set to false the EnabledRole
setData(index, false, EnabledRole);
} else {
//The bookmark is valid then we set to true the EnabledRole
setData(index, true, EnabledRole);
}
// Make sure that we have at least 32x32 images
const QSize size = newIcon.actualSize(QSize(32,32));
if (size.width() < 32) {
QPixmap smallPixmap = newIcon.pixmap(QSize(32, 32));
newIcon.addPixmap(smallPixmap.scaledToWidth(32, Qt::SmoothTransformation));
}
if (index.data().toString() != newName)
setData(index, newName);
QIcon oldIcon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
if (oldIcon.cacheKey() != newIcon.cacheKey())
setData(index, newIcon, Qt::DecorationRole);
}
}
void QUrlModel::setUrls(const QList<QUrl> &list)
{
removeRows(0, rowCount());
invalidUrls.clear();
watching.clear();
addUrls(list, 0);
}
/*!
Add urls \a list into the list at \a row. If move then movie
existing ones to row.
\sa dropMimeData()
*/
void QUrlModel::addUrls(const QList<QUrl> &list, int row, bool move)
{
if (row == -1)
row = rowCount();
row = qMin(row, rowCount());
for (int i = list.count() - 1; i >= 0; --i) {
QUrl url = list.at(i);
if (!url.isValid() || url.scheme() != QLatin1String("file"))
continue;
//this makes sure the url is clean
const QString cleanUrl = QDir::cleanPath(url.toLocalFile());
if (!cleanUrl.isEmpty())
url = QUrl::fromLocalFile(cleanUrl);
for (int j = 0; move && j < rowCount(); ++j) {
QString local = index(j, 0).data(UrlRole).toUrl().toLocalFile();
#if defined(Q_OS_WIN)
const Qt::CaseSensitivity cs = Qt::CaseInsensitive;
#else
const Qt::CaseSensitivity cs = Qt::CaseSensitive;
#endif
if (!cleanUrl.compare(local, cs)) {
removeRow(j);
if (j <= row)
row--;
break;
}
}
row = qMax(row, 0);
QModelIndex idx = fileSystemModel->index(cleanUrl);
if (!fileSystemModel->isDir(idx))
continue;
insertRows(row, 1);
setUrl(index(row, 0), url, idx);
watching.append({idx, cleanUrl});
}
}
/*!
Return the complete list of urls in a QList.
*/
QList<QUrl> QUrlModel::urls() const
{
QList<QUrl> list;
const int numRows = rowCount();
list.reserve(numRows);
for (int i = 0; i < numRows; ++i)
list.append(data(index(i, 0), UrlRole).toUrl());
return list;
}
/*!
QFileSystemModel to get index's from, clears existing rows
*/
void QUrlModel::setFileSystemModel(QFileSystemModel *model)
{
if (model == fileSystemModel)
return;
if (fileSystemModel != nullptr) {
disconnect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
this, SLOT(dataChanged(QModelIndex,QModelIndex)));
disconnect(model, SIGNAL(layoutChanged()),
this, SLOT(layoutChanged()));
disconnect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
this, SLOT(layoutChanged()));
}
fileSystemModel = model;
if (fileSystemModel != nullptr) {
connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
this, SLOT(dataChanged(QModelIndex,QModelIndex)));
connect(model, SIGNAL(layoutChanged()),
this, SLOT(layoutChanged()));
connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
this, SLOT(layoutChanged()));
}
clear();
insertColumns(0, 1);
}
/*
If one of the index's we are watching has changed update our internal data
*/
void QUrlModel::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
{
QModelIndex parent = topLeft.parent();
for (int i = 0; i < watching.count(); ++i) {
QModelIndex index = watching.at(i).index;
if (index.model() && topLeft.model()) {
Q_ASSERT(index.model() == topLeft.model());
}
if ( index.row() >= topLeft.row()
&& index.row() <= bottomRight.row()
&& index.column() >= topLeft.column()
&& index.column() <= bottomRight.column()
&& index.parent() == parent) {
changed(watching.at(i).path);
}
}
}
/*!
Re-get all of our data, anything could have changed!
*/
void QUrlModel::layoutChanged()
{
QStringList paths;
const int numPaths = watching.count();
paths.reserve(numPaths);
for (int i = 0; i < numPaths; ++i)
paths.append(watching.at(i).path);
watching.clear();
for (int i = 0; i < numPaths; ++i) {
QString path = paths.at(i);
QModelIndex newIndex = fileSystemModel->index(path);
watching.append({newIndex, path});
if (newIndex.isValid())
changed(path);
}
}
/*!
The following path changed data update our copy of that data
\sa layoutChanged(), dataChanged()
*/
void QUrlModel::changed(const QString &path)
{
for (int i = 0; i < rowCount(); ++i) {
QModelIndex idx = index(i, 0);
if (idx.data(UrlRole).toUrl().toLocalFile() == path) {
setData(idx, idx.data(UrlRole).toUrl());
}
}
}
QSidebar::QSidebar(QWidget *parent) : QListView(parent)
{
}
void QSidebar::setModelAndUrls(QFileSystemModel *model, const QList<QUrl> &newUrls)
{
setUniformItemSizes(true);
urlModel = new QUrlModel(this);
urlModel->setFileSystemModel(model);
setModel(urlModel);
setItemDelegate(new QSideBarDelegate(this));
connect(selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
this, SLOT(clicked(QModelIndex)));
#if QT_CONFIG(draganddrop)
setDragDropMode(QAbstractItemView::DragDrop);
#endif
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, SIGNAL(customContextMenuRequested(QPoint)),
this, SLOT(showContextMenu(QPoint)));
urlModel->setUrls(newUrls);
setCurrentIndex(this->model()->index(0,0));
}
QSidebar::~QSidebar()
{
}
#if QT_CONFIG(draganddrop)
void QSidebar::dragEnterEvent(QDragEnterEvent *event)
{
if (urlModel->canDrop(event))
QListView::dragEnterEvent(event);
}
#endif // QT_CONFIG(draganddrop)
QSize QSidebar::sizeHint() const
{
if (model())
return QListView::sizeHintForIndex(model()->index(0, 0)) + QSize(2 * frameWidth(), 2 * frameWidth());
return QListView::sizeHint();
}
void QSidebar::selectUrl(const QUrl &url)
{
disconnect(selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
this, SLOT(clicked(QModelIndex)));
selectionModel()->clear();
for (int i = 0; i < model()->rowCount(); ++i) {
if (model()->index(i, 0).data(QUrlModel::UrlRole).toUrl() == url) {
selectionModel()->select(model()->index(i, 0), QItemSelectionModel::Select);
break;
}
}
connect(selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
this, SLOT(clicked(QModelIndex)));
}
#if QT_CONFIG(menu)
/*!
\internal
\sa removeEntry()
*/
void QSidebar::showContextMenu(const QPoint &position)
{
QList<QAction *> actions;
if (indexAt(position).isValid()) {
QAction *action = new QAction(QFileDialog::tr("Remove"), this);
if (indexAt(position).data(QUrlModel::UrlRole).toUrl().path().isEmpty())
action->setEnabled(false);
connect(action, SIGNAL(triggered()), this, SLOT(removeEntry()));
actions.append(action);
}
if (actions.count() > 0)
QMenu::exec(actions, mapToGlobal(position));
}
#endif // QT_CONFIG(menu)
/*!
\internal
\sa showContextMenu()
*/
void QSidebar::removeEntry()
{
QList<QModelIndex> idxs = selectionModel()->selectedIndexes();
QList<QPersistentModelIndex> indexes;
const int numIndexes = idxs.count();
indexes.reserve(numIndexes);
for (int i = 0; i < numIndexes; i++)
indexes.append(idxs.at(i));
for (int i = 0; i < numIndexes; ++i) {
if (!indexes.at(i).data(QUrlModel::UrlRole).toUrl().path().isEmpty())
model()->removeRow(indexes.at(i).row());
}
}
/*!
\internal
\sa goToUrl()
*/
void QSidebar::clicked(const QModelIndex &index)
{
QUrl url = model()->index(index.row(), 0).data(QUrlModel::UrlRole).toUrl();
emit goToUrl(url);
selectUrl(url);
}
/*!
\reimp
Don't automatically select something
*/
void QSidebar::focusInEvent(QFocusEvent *event)
{
QAbstractScrollArea::focusInEvent(event);
viewport()->update();
}
/*!
\reimp
*/
bool QSidebar::event(QEvent * event)
{
if (event->type() == QEvent::KeyRelease) {
QKeyEvent* ke = (QKeyEvent*) event;
if (ke->key() == Qt::Key_Delete) {
removeEntry();
return true;
}
}
return QListView::event(event);
}
QT_END_NAMESPACE
#include "moc_qsidebar_p.cpp"