/*
Copyright (c) 2017, Lukas Holecek <hluk@email.cz>
This file is part of CopyQ.
CopyQ 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 3 of the License, or
(at your option) any later version.
CopyQ 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 CopyQ. If not, see <http://www.gnu.org/licenses/>.
*/
#include "tabtree.h"
#include "common/common.h"
#include "common/display.h"
#include "common/mimetypes.h"
#include "gui/iconfactory.h"
#include "gui/iconfont.h"
#include "gui/tabicons.h"
#include <QApplication>
#include <QLabel>
#include <QList>
#include <QMimeData>
#include <QMouseEvent>
#include <QScrollBar>
#include <QHBoxLayout>
#include <QTreeWidgetItemIterator>
namespace {
enum {
DataText = Qt::UserRole,
DataItemCount
};
void updateItemSize(QTreeWidgetItem *item)
{
QSize size(0, 0);
QWidget *w = item->treeWidget()->itemWidget(item, 0);
if (w) {
size = w->minimumSizeHint();
if (!item->icon(0).isNull()) {
const QSize iconSize = item->treeWidget()->iconSize();
size = QSize(
size.width() + iconSize.width() + 8,
qMax(size.height(), iconSize.height())
);
}
}
item->setSizeHint(0, size);
}
void setItemWidgetSelected(QTreeWidgetItem *item)
{
if (item == nullptr)
return;
QTreeWidget *parent = item->treeWidget();
if (parent == nullptr)
return;
QWidget *w = parent->itemWidget(item, 0);
if (w) {
QStyle *style = w->style();
style->unpolish(w);
style->polish(w);
bool selected = parent->currentItem() == item;
for (auto child : w->findChildren<QWidget *>()) {
child->setProperty("CopyQ_selected", selected);
style->unpolish(child);
style->polish(child);
}
}
updateItemSize(item);
}
QTreeWidgetItem *findLastTreeItem(const QTreeWidget &tree, QStringList *pathComponents)
{
QTreeWidgetItem *parentItem = nullptr;
if ( !pathComponents->isEmpty() ) {
const QString &text = pathComponents->first();
for (int i = 0; i < tree.topLevelItemCount(); ++i) {
if ( tree.topLevelItem(i)->data(0, DataText).toString() == text ) {
parentItem = tree.topLevelItem(i);
break;
}
}
}
if (parentItem != nullptr) {
pathComponents->pop_front();
while ( !pathComponents->isEmpty() ) {
const QString &text = pathComponents->first();
QTreeWidgetItem *item = nullptr;
for (int i = 0; i < parentItem->childCount(); ++i) {
if ( parentItem->child(i)->data(0, DataText).toString() == text ) {
item = parentItem->child(i);
break;
}
}
if (item == nullptr)
break;
parentItem = item;
pathComponents->pop_front();
}
}
return parentItem;
}
QTreeWidgetItem *dropItemsTarget(const QDropEvent &event, const QTreeWidget &parent)
{
return canDropToTab(event) ? parent.itemAt( event.pos() ) : nullptr;
}
int itemLabelPadding()
{
return iconFontSizePixels() / 4;
}
QLabel *createLabel(const QString &objectName, QWidget *parent)
{
QLabel *label = new QLabel(parent);
label->setMargin(itemLabelPadding());
label->setObjectName(objectName);
return label;
}
class ItemLabel : public QWidget {
public:
explicit ItemLabel(QTreeWidgetItem *item)
: QWidget(item->treeWidget())
, m_treeWidget(item->treeWidget())
, m_label(createLabel("tab_tree_item", this))
, m_labelItemCount(nullptr)
, m_layout(new QHBoxLayout(this))
{
m_label->setBuddy(m_treeWidget);
m_label->installEventFilter(this);
m_layout->addWidget(m_label);
m_layout->setMargin(0);
m_layout->addStretch(1);
updateFromItem(item);
}
void updateFromItem(QTreeWidgetItem *item)
{
const QString text = item->data(0, DataText).toString();
const QString itemCount = item->data(0, DataItemCount).toString();
setText(text);
setItemCountLabel(itemCount);
}
void setText(const QString &text)
{
m_label->setText(text);
}
void setItemCountLabel(const QString &itemCount)
{
if (itemCount.isEmpty()) {
delete m_labelItemCount;
m_labelItemCount = nullptr;
} else {
if (!m_labelItemCount) {
m_labelItemCount = createLabel("tab_item_counter", this);
setDefaultTabItemCounterStyle(m_labelItemCount);
m_layout->insertWidget(1, m_labelItemCount);
m_labelItemCount->show();
}
m_labelItemCount->setProperty("text", itemCount);
}
}
protected:
bool eventFilter(QObject *, QEvent *event) override
{
if ( event->type() == QEvent::Shortcut ) {
for ( QTreeWidgetItemIterator it(m_treeWidget->topLevelItem(0)); *it; ++it ) {
auto item = *it;
if ( m_treeWidget->itemWidget(item, 0) == this ) {
m_treeWidget->setCurrentItem(item);
return true;
}
}
}
return false;
}
private:
QTreeWidget *m_treeWidget;
QLabel *m_label;
QLabel *m_labelItemCount;
QHBoxLayout *m_layout;
};
ItemLabel *itemLabel(QTreeWidgetItem *item)
{
return static_cast<ItemLabel*>( item->treeWidget()->itemWidget(item, 0) );
}
void labelItem(QTreeWidgetItem *item)
{
ItemLabel *label = itemLabel(item);
if (label) {
label->updateFromItem(item);
return;
}
QTreeWidget *parent = item->treeWidget();
label = new ItemLabel(item);
label->installEventFilter(parent);
item->setTextAlignment(0, Qt::AlignCenter);
parent->setItemWidget(item, 0, label);
setItemWidgetSelected(item);
}
bool isInside(QWidget *child, QWidget *parent)
{
const QPoint scrollBarPosition = child->mapTo(parent, QPoint(0,0));
return parent->contentsRect().contains(scrollBarPosition);
}
} // namespace
TabTree::TabTree(QWidget *parent)
: QTreeWidget(parent)
{
connect( this, SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)),
this, SLOT(onCurrentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)) );
setDragEnabled(true);
setDragDropMode(QAbstractItemView::InternalMove);
setDragDropOverwriteMode(false);
setDefaultDropAction(Qt::CopyAction); // otherwise tab is lost if moved outside tree
setFrameShape(QFrame::NoFrame);
setHeaderHidden(true);
setSelectionMode(QAbstractItemView::SingleSelection);
const int x = smallIconSize();
setIconSize(QSize(x, x));
setMinimumHeight(fontMetrics().lineSpacing() * 3);
verticalScrollBar()->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Ignored);
verticalScrollBar()->installEventFilter(this);
connect( this, SIGNAL(itemCollapsed(QTreeWidgetItem*)), SLOT(updateSize()) );
connect( this, SIGNAL(itemExpanded(QTreeWidgetItem*)), SLOT(updateSize()) );
initSingleShotTimer( &m_timerUpdate, 0, this, SLOT(doUpdateSize()) );
}
QString TabTree::getCurrentTabPath() const
{
return getTabPath( currentItem() );
}
bool TabTree::isTabGroup(const QString &tab) const
{
return isTabGroup( findTreeItem(tab) );
}
QString TabTree::tabText(int tabIndex) const
{
return getTabPath( findTreeItem(tabIndex) );
}
void TabTree::setTabText(int tabIndex, const QString &tabText)
{
QTreeWidgetItem *item = findTreeItem(tabIndex);
Q_ASSERT(item);
if (getTabPath(item) == tabText)
return;
const QString itemCount = item->data(0, DataItemCount).toString();
insertTab(tabIndex, tabText);
if (item == currentItem())
setCurrentTab(tabIndex);
// Remove old item if it's an empty group.
m_tabs.removeOne(item);
if ( isEmptyTabGroup(item) )
deleteItem(item);
item = findTreeItem(tabIndex);
Q_ASSERT(item);
Q_ASSERT(getTabPath(item) == tabText);
if ( !itemCount.isEmpty() )
setTabItemCount(tabText, itemCount);
updateItemSize(item);
updateSize();
}
void TabTree::setTabItemCount(const QString &tabName, const QString &itemCount)
{
QTreeWidgetItem *item = findTreeItem(tabName);
if (!item)
return;
item->setData(0, DataItemCount, itemCount);
ItemLabel *label = itemLabel(item);
Q_ASSERT(label);
label->setItemCountLabel(itemCount);
updateItemSize(item);
updateSize();
}
void TabTree::updateTabIcon(const QString &tabName)
{
QTreeWidgetItem *item = findTreeItem(tabName);
if (!item)
return;
const QIcon icon = getIconForTabName(tabName);
item->setIcon(0, icon);
updateItemSize(item);
updateSize();
}
void TabTree::insertTab(int index, const QString &path)
{
QStringList pathComponents = path.split('/');
QTreeWidgetItem *item = findLastTreeItem(*this, &pathComponents);
const bool selectTab = topLevelItemCount() == 0;
for (const auto &text : pathComponents) {
QTreeWidgetItem *parent = item;
if (parent != nullptr) {
int to = 0;
for ( ; to < parent->childCount(); ++to ) {
const int index2 = getTabIndex(parent->child(to));
if (index2 != -1 && index < index2)
break;
}
int from = parent->childCount();
item = new QTreeWidgetItem(parent);
if (from != to)
parent->insertChild(to, parent->takeChild(from));
} else {
int to = 0;
for ( ; to < topLevelItemCount(); ++to ) {
const int index2 = getTabIndex(topLevelItem(to));
if (index2 != -1 && index < index2)
break;
}
int from = topLevelItemCount();
item = new QTreeWidgetItem(this);
if (from != to)
insertTopLevelItem(to, takeTopLevelItem(from));
}
item->setExpanded(true);
item->setData(0, DataText, text);
const QIcon icon = getIconForTabName( getTabPath(item) );
item->setIcon(0, icon);
labelItem(item);
}
Q_ASSERT(item != nullptr);
m_tabs.insert(index, item);
if (selectTab)
setCurrentItem(item);
updateSize();
}
void TabTree::removeTab(int index)
{
QTreeWidgetItem *item = findTreeItem(index);
if (item == nullptr)
return;
m_tabs.removeOne(item);
if (item->childCount() == 0)
deleteItem(item);
updateSize();
}
void TabTree::moveTab(int from, int to)
{
if (from == to)
return;
QTreeWidgetItem *item = findTreeItem(from);
if (item == nullptr)
return;
m_tabs.removeOne(item);
m_tabs.insert(to, item);
}
void TabTree::updateCollapsedTabs(QStringList *tabs) const
{
tabs->clear();
for ( QTreeWidgetItemIterator it(topLevelItem(0)); *it; ++it ) {
auto item = *it;
if ( isTabGroup(item) && !item->isExpanded() )
tabs->append( getTabPath(item) );
}
}
void TabTree::setCollapsedTabs(const QStringList &collapsedPaths)
{
for (const auto &path : collapsedPaths) {
QTreeWidgetItem *item = findTreeItem(path);
if ( isTabGroup(item) )
item->setExpanded(false);
}
}
void TabTree::updateTabIcons()
{
for ( QTreeWidgetItemIterator it(topLevelItem(0)); *it; ++it )
updateTabIcon( getTabPath(*it) );
}
void TabTree::nextTab()
{
QTreeWidgetItem *item = currentItem();
if (item != nullptr)
item = itemBelow(item);
if (item == nullptr)
item = topLevelItem(0);
if (item != nullptr)
setCurrentItem(item);
}
void TabTree::previousTab()
{
QTreeWidgetItem *item = currentItem();
if (item != nullptr)
item = itemAbove(item);
if (item == nullptr) {
item = topLevelItem( topLevelItemCount() - 1 );
while ( isTabGroup(item) && item->isExpanded() )
item = item->child( item->childCount() - 1 );
}
if (item != nullptr)
setCurrentItem(item);
}
void TabTree::setCurrentTab(int index)
{
if (index < 0)
return;
QTreeWidgetItem *item = findTreeItem(index);
if (item != nullptr)
setCurrentItem(item);
}
void TabTree::adjustSize()
{
updateSize();
}
QTreeWidgetItem *TabTree::findTreeItem(int index) const
{
return m_tabs.value(index);
}
QTreeWidgetItem *TabTree::findTreeItem(const QString &path) const
{
QStringList pathComponents = path.split('/');
QTreeWidgetItem *parentItem = findLastTreeItem(*this, &pathComponents);
return pathComponents.isEmpty() ? parentItem : nullptr;
}
int TabTree::getTabIndex(const QTreeWidgetItem *item) const
{
return (item == nullptr) ? -1 : m_tabs.indexOf( const_cast<QTreeWidgetItem*>(item) );
}
QString TabTree::getTabPath(const QTreeWidgetItem *item) const
{
QString result;
const QTreeWidgetItem *parent = item;
while (parent != nullptr) {
const QString part = parent->data(0, DataText).toString();
result.prepend('/');
result.prepend(part);
parent = parent->parent();
}
result.chop(1);
return result;
}
bool TabTree::isTabGroup(const QTreeWidgetItem *item) const
{
return item != nullptr && item->childCount() > 0;
}
bool TabTree::isEmptyTabGroup(const QTreeWidgetItem *item) const
{
return item->childCount() == 0 && getTabIndex(item) < 0;
}
QSize TabTree::sizeHint() const
{
return minimumSizeHint();
}
void TabTree::contextMenuEvent(QContextMenuEvent *event)
{
requestTabMenu(event->pos(), event->globalPos());
event->accept();
}
void TabTree::dragEnterEvent(QDragEnterEvent *event)
{
if ( canDropToTab(*event) ) {
acceptDrag(event);
} else {
QTreeWidget::dragEnterEvent(event);
// Workaround for QTBUG-44939 (Qt 5.4): Don't ignore successive drag move events.
event->acceptProposedAction();
}
}
void TabTree::dragMoveEvent(QDragMoveEvent *event)
{
if ( dropItemsTarget(*event, *this) )
acceptDrag(event);
else if ( itemAt(event->pos()) )
QTreeWidget::dragMoveEvent(event);
else
event->ignore();
}
void TabTree::dropEvent(QDropEvent *event)
{
const auto current = currentItem();
if (current == nullptr)
return;
const auto targetItem = dropItemsTarget(*event, *this);
if (targetItem) {
acceptDrag(event);
emit dropItems( getTabPath(targetItem), event->mimeData() );
} else if ( itemAt(event->pos()) ) {
const QString oldPrefix = getTabPath(current);
blockSignals(true);
QTreeWidget::dropEvent(event);
setCurrentItem(current);
setItemWidgetSelected(current);
blockSignals(false);
// Rename moved item if non-unique.
QStringList uniqueTabNames;
const auto parent = current->parent();
for (int i = 0, count = parent ? parent->childCount() : topLevelItemCount(); i < count; ++i) {
QTreeWidgetItem *sibling = parent ? parent->child(i) : topLevelItem(i);
if (sibling != current)
uniqueTabNames.append( getTabPath(sibling) );
}
auto newPrefix = getTabPath(current);
if ( uniqueTabNames.contains(newPrefix) ) {
renameToUnique(&newPrefix, uniqueTabNames);
const QString text = newPrefix.mid( newPrefix.lastIndexOf(QChar('/')) + 1 );
current->setData(0, DataText, text);
labelItem(current);
}
QList<QTreeWidgetItem*> newTabs;
QList<int> indexes;
for ( QTreeWidgetItemIterator it(topLevelItem(0)); *it; ++it ) {
auto item = *it;
// Remove empty groups.
if ( isEmptyTabGroup(item) ) {
deleteItem(item);
} else {
const int oldIndex = getTabIndex(item);
if (oldIndex != -1) {
newTabs.append(item);
indexes.append(oldIndex);
}
}
}
m_tabs = std::move(newTabs);
emit tabsMoved(oldPrefix, newPrefix, indexes);
updateSize();
} else {
event->ignore();
}
}
bool TabTree::eventFilter(QObject *obj, QEvent *event)
{
if ( obj == verticalScrollBar() ) {
if ( event->type() == QEvent::Show || event->type() == QEvent::Hide )
updateSize();
}
return QTreeWidget::eventFilter(obj, event);
}
void TabTree::rowsInserted(const QModelIndex &parent, int start, int end)
{
QTreeWidget::rowsInserted(parent, start, end);
for (int row = start; row <= end; ++row) {
QList<QTreeWidgetItem *> items;
items.append( parent.isValid() ? itemFromIndex(parent.child(row, 0))
: topLevelItem(row) );
while ( !items.isEmpty() ) {
QTreeWidgetItem *item = items.takeLast();
labelItem(item);
const int n = item->childCount();
items.reserve(items.size() + n);
for (int i = 0; i < n; ++i)
items.append(item->child(i));
}
}
}
void TabTree::showEvent(QShowEvent *event)
{
QTreeWidget::showEvent(event);
updateSize();
}
void TabTree::onCurrentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous)
{
emit currentTabChanged( getTabIndex(current) );
setItemWidgetSelected(current);
setItemWidgetSelected(previous);
}
void TabTree::updateSize()
{
m_timerUpdate.start();
}
void TabTree::doUpdateSize()
{
doItemsLayout();
const QMargins margins = contentsMargins();
int w = margins.left() + margins.right();
// Some styles put scrollbar outside of parent's contents so there is no need
// to resize parent widget.
if ( verticalScrollBar()->isVisible() && isInside(verticalScrollBar(), this) )
w += verticalScrollBar()->width();
resizeColumnToContents(0);
w += sizeHintForColumn(0);
resizeColumnToContents(1);
w += sizeHintForColumn(1);
setFixedWidth(w);
}
void TabTree::requestTabMenu(const QPoint &itemPosition, const QPoint &menuPosition)
{
QTreeWidgetItem *item = itemAt(itemPosition);
QString tabPath = getTabPath(item);
emit tabMenuRequested(menuPosition, tabPath);
}
void TabTree::deleteItem(QTreeWidgetItem *item)
{
// Recursively remove empty parent item.
QTreeWidgetItem *parent = item->parent();
while (parent != nullptr && parent->childCount() == 1 && getTabIndex(parent) < 0) {
item = parent;
parent = item->parent();
}
delete item;
}