4c75f9e
/*
4c75f9e
    Copyright (c) 2014, Lukas Holecek <hluk@email.cz>
4c75f9e
4c75f9e
    This file is part of CopyQ.
4c75f9e
4c75f9e
    CopyQ is free software: you can redistribute it and/or modify
4c75f9e
    it under the terms of the GNU General Public License as published by
4c75f9e
    the Free Software Foundation, either version 3 of the License, or
4c75f9e
    (at your option) any later version.
4c75f9e
4c75f9e
    CopyQ is distributed in the hope that it will be useful,
4c75f9e
    but WITHOUT ANY WARRANTY; without even the implied warranty of
4c75f9e
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4c75f9e
    GNU General Public License for more details.
4c75f9e
4c75f9e
    You should have received a copy of the GNU General Public License
4c75f9e
    along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
4c75f9e
*/
4c75f9e
4c75f9e
#include "itempinned.h"
4c75f9e
#include "ui_itempinnedsettings.h"
4c75f9e
4c75f9e
#include "common/command.h"
4c75f9e
#include "common/contenttype.h"
4c75f9e
#include "common/display.h"
4c75f9e
4c75f9e
#ifdef HAS_TESTS
4c75f9e
#   include "tests/itempinnedtests.h"
4c75f9e
#endif
4c75f9e
4c75f9e
#include <QApplication>
4c75f9e
#include <QBoxLayout>
4c75f9e
#include <QMessageBox>
4c75f9e
#include <QModelIndex>
4c75f9e
4c75f9e
#include <algorithm>
4c75f9e
4c75f9e
namespace {
4c75f9e
4c75f9e
const char mimePinned[] = "application/x-copyq-item-pinned";
4c75f9e
4c75f9e
bool isPinned(const QModelIndex &index)
4c75f9e
{
4c75f9e
    const auto dataMap = index.data(contentType::data).toMap();
4c75f9e
    return dataMap.contains(mimePinned);
4c75f9e
}
4c75f9e
4c75f9e
Command dummyPinCommand()
4c75f9e
{
4c75f9e
    Command c;
4c75f9e
    c.icon = QString(QChar(IconThumbTack));
4c75f9e
    c.inMenu = true;
4c75f9e
    c.shortcuts = QStringList()
4c75f9e
            << ItemPinnedLoader::tr("Ctrl+Shift+P", "Shortcut to pin and unpin items");
4c75f9e
    return c;
4c75f9e
}
4c75f9e
4c75f9e
} // namespace
4c75f9e
4c75f9e
ItemPinned::ItemPinned(ItemWidget *childItem)
4c75f9e
    : QWidget( childItem->widget()->parentWidget() )
4c75f9e
    , ItemWidget(this)
4c75f9e
    , m_border(new QWidget(this))
4c75f9e
    , m_childItem(childItem)
4c75f9e
{
4c75f9e
    m_childItem->widget()->setObjectName("item_child");
4c75f9e
    m_childItem->widget()->setParent(this);
4c75f9e
4c75f9e
    m_border->setFixedWidth( pointsToPixels(6) );
4c75f9e
4c75f9e
    // Set pinned item border color.
4c75f9e
    const auto *parent = parentWidget();
4c75f9e
    auto color = parent->palette().color(QPalette::Background);
4c75f9e
    const int lightThreshold = 100;
4c75f9e
    const bool menuBackgrounIsLight = color.lightness() > lightThreshold;
4c75f9e
    color.setHsl(
4c75f9e
                color.hue(),
4c75f9e
                color.saturation(),
4c75f9e
                qMax(0, qMin(255, color.lightness() + (menuBackgrounIsLight ? -200 : 50)))
4c75f9e
                );
4c75f9e
    const auto styleSheet = QString("background-color: rgba(%1,%2,%3,15\\%)")
4c75f9e
            .arg(color.red())
4c75f9e
            .arg(color.green())
4c75f9e
            .arg(color.blue());
4c75f9e
    m_border->setStyleSheet(styleSheet);
4c75f9e
4c75f9e
    QBoxLayout *layout;
4c75f9e
    layout = new QHBoxLayout(this);
4c75f9e
    layout->setContentsMargins(0, 0, 0, 0);
4c75f9e
    layout->setSpacing( pointsToPixels(5) );
4c75f9e
4c75f9e
    layout->addWidget(m_childItem->widget());
4c75f9e
    layout->addStretch();
4c75f9e
    layout->addWidget(m_border);
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinned::highlight(const QRegExp &re, const QFont &highlightFont, const QPalette &highlightPalette)
4c75f9e
{
4c75f9e
    m_childItem->setHighlight(re, highlightFont, highlightPalette);
4c75f9e
}
4c75f9e
4c75f9e
QWidget *ItemPinned::createEditor(QWidget *parent) const
4c75f9e
{
4c75f9e
    return m_childItem->createEditor(parent);
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinned::setEditorData(QWidget *editor, const QModelIndex &index) const
4c75f9e
{
4c75f9e
    return m_childItem->setEditorData(editor, index);
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinned::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
4c75f9e
{
4c75f9e
    return m_childItem->setModelData(editor, model, index);
4c75f9e
}
4c75f9e
4c75f9e
bool ItemPinned::hasChanges(QWidget *editor) const
4c75f9e
{
4c75f9e
    return m_childItem->hasChanges(editor);
4c75f9e
}
4c75f9e
4c75f9e
QObject *ItemPinned::createExternalEditor(const QModelIndex &index, QWidget *parent) const
4c75f9e
{
4c75f9e
    return m_childItem->createExternalEditor(index, parent);
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinned::updateSize(const QSize &maximumSize, int idealWidth)
4c75f9e
{
4c75f9e
    setMinimumWidth(idealWidth);
4c75f9e
    setMaximumWidth(maximumSize.width());
4c75f9e
    const int width = m_border->width() + layout()->spacing();
4c75f9e
    const int childItemWidth = idealWidth - width;
4c75f9e
    const auto childItemMaximumSize = QSize(maximumSize.width() - width, maximumSize.height());
4c75f9e
    m_childItem->updateSize(childItemMaximumSize, childItemWidth);
4c75f9e
    adjustSize();
4c75f9e
}
4c75f9e
4c75f9e
bool ItemPinnedScriptable::isPinned()
4c75f9e
{
4c75f9e
    const auto args = currentArguments();
4c75f9e
    for (const auto &arg : args) {
4c75f9e
        bool ok;
4c75f9e
        const int row = arg.toInt(&ok;;
4c75f9e
        if (ok) {
4c75f9e
            const auto result = call("read", QVariantList() << "?" << row);
4c75f9e
            if ( result.toByteArray().contains(mimePinned) )
4c75f9e
                return true;
4c75f9e
        }
4c75f9e
    }
4c75f9e
4c75f9e
    return false;
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedScriptable::pin()
4c75f9e
{
4c75f9e
    const auto args = currentArguments();
4c75f9e
    for (const auto &arg : args) {
4c75f9e
        bool ok;
4c75f9e
        const int row = arg.toInt(&ok;;
4c75f9e
        if (ok)
4c75f9e
            call("change", QVariantList() << row << mimePinned << QString());
4c75f9e
    }
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedScriptable::unpin()
4c75f9e
{
4c75f9e
    const auto args = currentArguments();
4c75f9e
    for (const auto &arg : args) {
4c75f9e
        bool ok;
4c75f9e
        const int row = arg.toInt(&ok;;
4c75f9e
        if (ok)
4c75f9e
            call("change", QVariantList() << row << mimePinned << QVariant());
4c75f9e
    }
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedScriptable::pinData()
4c75f9e
{
4c75f9e
    call("setData", QVariantList() << mimePinned << QString());
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedScriptable::unpinData()
4c75f9e
{
4c75f9e
    call("removeData", QVariantList() << mimePinned);
4c75f9e
}
4c75f9e
4c75f9e
ItemPinnedSaver::ItemPinnedSaver(QAbstractItemModel *model, QVariantMap &settings, const ItemSaverPtr &saver)
4c75f9e
    : m_model(model)
4c75f9e
    , m_settings(settings)
4c75f9e
    , m_saver(saver)
4c75f9e
{
4c75f9e
    connect( model, SIGNAL(rowsInserted(QModelIndex,int,int)),
4c75f9e
             SLOT(onRowsInserted(QModelIndex,int,int)) );
4c75f9e
    connect( model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
4c75f9e
             SLOT(onRowsRemoved(QModelIndex,int,int)) );
4c75f9e
    connect( model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),
4c75f9e
             SLOT(onRowsMoved(QModelIndex,int,int,QModelIndex,int)) );
4c75f9e
    connect( model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
4c75f9e
             SLOT(onDataChanged(QModelIndex,QModelIndex)) );
4c75f9e
4c75f9e
    updateLastPinned( 0, m_model->rowCount() );
4c75f9e
}
4c75f9e
4c75f9e
bool ItemPinnedSaver::saveItems(const QString &tabName, const QAbstractItemModel &model, QIODevice *file)
4c75f9e
{
4c75f9e
    return m_saver->saveItems(tabName, model, file);
4c75f9e
}
4c75f9e
4c75f9e
bool ItemPinnedSaver::canRemoveItems(const QList<QModelIndex> &indexList, QString *error)
4c75f9e
{
4c75f9e
    const bool containsPinnedItems = std::any_of(
4c75f9e
                std::begin(indexList), std::end(indexList), isPinned);
4c75f9e
4c75f9e
    if (!containsPinnedItems)
4c75f9e
        return m_saver->canRemoveItems(indexList, error);
4c75f9e
4c75f9e
    if (error) {
4c75f9e
        *error = "Removing pinned item is not allowed (unpin item first)";
4c75f9e
        return false;
4c75f9e
    }
4c75f9e
4c75f9e
    QMessageBox::information(
4c75f9e
                QApplication::activeWindow(),
4c75f9e
                ItemPinnedLoader::tr("Cannot Remove Pinned Items"),
4c75f9e
                ItemPinnedLoader::tr("Unpin items first to remove them.") );
4c75f9e
    return false;
4c75f9e
}
4c75f9e
4c75f9e
bool ItemPinnedSaver::canMoveItems(const QList<QModelIndex> &indexList)
4c75f9e
{
4c75f9e
    return m_saver->canMoveItems(indexList);
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedSaver::itemsRemovedByUser(const QList<QModelIndex> &indexList)
4c75f9e
{
4c75f9e
    m_saver->itemsRemovedByUser(indexList);
4c75f9e
}
4c75f9e
4c75f9e
QVariantMap ItemPinnedSaver::copyItem(const QAbstractItemModel &model, const QVariantMap &itemData)
4c75f9e
{
4c75f9e
    return m_saver->copyItem(model, itemData);
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedSaver::onRowsInserted(const QModelIndex &, int start, int end)
4c75f9e
{
4c75f9e
    if (!m_model || m_lastPinned < start) {
4c75f9e
        updateLastPinned(start, end);
4c75f9e
        return;
4c75f9e
    }
4c75f9e
4c75f9e
    disconnect( m_model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),
4c75f9e
                this, SLOT(onRowsMoved(QModelIndex,int,int,QModelIndex,int)) );
4c75f9e
4c75f9e
    // Shift rows below inserted up.
4c75f9e
    const int rowCount = end - start + 1;
4c75f9e
    for (int row = end + 1; row <= m_lastPinned + rowCount; ++row) {
4c75f9e
        const auto index = m_model->index(row, 0);
4c75f9e
        if ( isPinned(index) )
4c75f9e
            moveRow(row, row - rowCount);
4c75f9e
    }
4c75f9e
4c75f9e
    connect( m_model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),
4c75f9e
             SLOT(onRowsMoved(QModelIndex,int,int,QModelIndex,int)) );
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedSaver::onRowsRemoved(const QModelIndex &, int start, int end)
4c75f9e
{
4c75f9e
    if (!m_model || m_lastPinned < start)
4c75f9e
        return;
4c75f9e
4c75f9e
    disconnect( m_model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),
4c75f9e
                this, SLOT(onRowsMoved(QModelIndex,int,int,QModelIndex,int)) );
4c75f9e
4c75f9e
    // Shift rows below removed down.
4c75f9e
    const int rowCount = end - start + 1;
4c75f9e
    for (int row = m_lastPinned - rowCount; row >= start; --row) {
4c75f9e
        const auto index = m_model->index(row, 0);
4c75f9e
        if ( isPinned(index) )
4c75f9e
            moveRow(row, row + rowCount + 1);
4c75f9e
    }
4c75f9e
4c75f9e
    connect( m_model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),
4c75f9e
             SLOT(onRowsMoved(QModelIndex,int,int,QModelIndex,int)) );
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedSaver::onRowsMoved(const QModelIndex &, int start, int end, const QModelIndex &, int destinationRow)
4c75f9e
{
4c75f9e
    if ( (m_lastPinned < start && m_lastPinned < destinationRow)
4c75f9e
         || (end < m_lastPinned && destinationRow < m_lastPinned) )
4c75f9e
    {
4c75f9e
        return;
4c75f9e
    }
4c75f9e
4c75f9e
    if (start < destinationRow)
4c75f9e
        updateLastPinned(start, destinationRow + end - start + 1);
4c75f9e
    else
4c75f9e
        updateLastPinned(destinationRow, end);
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedSaver::onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
4c75f9e
{
4c75f9e
    if ( bottomRight.row() < m_lastPinned )
4c75f9e
        return;
4c75f9e
4c75f9e
    updateLastPinned( topLeft.row(), bottomRight.row() );
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedSaver::moveRow(int from, int to)
4c75f9e
{
4c75f9e
#if QT_VERSION < 0x050000
4c75f9e
    QMetaObject::invokeMethod(m_model, "moveRow", Q_ARG(int, from), Q_ARG(int, to));
4c75f9e
#else
4c75f9e
    m_model->moveRow(QModelIndex(), from, QModelIndex(), to);
4c75f9e
#endif
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedSaver::updateLastPinned(int from, int to)
4c75f9e
{
4c75f9e
    for (int row = to; row >= from; --row) {
4c75f9e
        const auto index = m_model->index(row, 0);
4c75f9e
        if ( isPinned(index) ) {
4c75f9e
            m_lastPinned = row;
4c75f9e
            break;
4c75f9e
        }
4c75f9e
    }
4c75f9e
}
4c75f9e
4c75f9e
ItemPinnedLoader::ItemPinnedLoader()
4c75f9e
{
4c75f9e
}
4c75f9e
4c75f9e
ItemPinnedLoader::~ItemPinnedLoader() = default;
4c75f9e
4c75f9e
QStringList ItemPinnedLoader::formatsToSave() const
4c75f9e
{
4c75f9e
    return QStringList() << mimePinned;
4c75f9e
}
4c75f9e
4c75f9e
QVariantMap ItemPinnedLoader::applySettings()
4c75f9e
{
4c75f9e
    return m_settings;
4c75f9e
}
4c75f9e
4c75f9e
QWidget *ItemPinnedLoader::createSettingsWidget(QWidget *parent)
4c75f9e
{
4c75f9e
    ui.reset(new Ui::ItemPinnedSettings);
4c75f9e
    QWidget *w = new QWidget(parent);
4c75f9e
    ui->setupUi(w);
4c75f9e
4c75f9e
    connect( ui->pushButtonAddCommands, SIGNAL(clicked()),
4c75f9e
             this, SLOT(addCommands()) );
4c75f9e
4c75f9e
    return w;
4c75f9e
}
4c75f9e
4c75f9e
ItemWidget *ItemPinnedLoader::transform(ItemWidget *itemWidget, const QModelIndex &index)
4c75f9e
{
4c75f9e
    return isPinned(index) ? new ItemPinned(itemWidget) : nullptr;
4c75f9e
}
4c75f9e
4c75f9e
ItemSaverPtr ItemPinnedLoader::transformSaver(const ItemSaverPtr &saver, QAbstractItemModel *model)
4c75f9e
{
4c75f9e
    return std::make_shared<ItemPinnedSaver>(model, m_settings, saver);
4c75f9e
}
4c75f9e
4c75f9e
QObject *ItemPinnedLoader::tests(const TestInterfacePtr &test) const
4c75f9e
{
4c75f9e
#ifdef HAS_TESTS
4c75f9e
    QObject *tests = new ItemPinnedTests(test);
4c75f9e
    return tests;
4c75f9e
#else
4c75f9e
    Q_UNUSED(test);
4c75f9e
    return nullptr;
4c75f9e
#endif
4c75f9e
}
4c75f9e
4c75f9e
ItemScriptable *ItemPinnedLoader::scriptableObject(QObject *parent)
4c75f9e
{
4c75f9e
    return new ItemPinnedScriptable(parent);
4c75f9e
}
4c75f9e
4c75f9e
QList<Command> ItemPinnedLoader::commands() const
4c75f9e
{
4c75f9e
    QList<Command> commands;
4c75f9e
4c75f9e
    Command c;
4c75f9e
4c75f9e
    c = dummyPinCommand();
4c75f9e
    c.name = tr("Pin");
4c75f9e
    c.input = "!OUTPUT";
4c75f9e
    c.output = mimePinned;
4c75f9e
    c.cmd = "copyq: plugins.itempinned.pinData()";
4c75f9e
    commands.append(c);
4c75f9e
4c75f9e
    c = dummyPinCommand();
4c75f9e
    c.name = tr("Unpin");
4c75f9e
    c.input = mimePinned;
4c75f9e
    c.cmd = "copyq: plugins.itempinned.unpinData()";
4c75f9e
    commands.append(c);
4c75f9e
4c75f9e
    return commands;
4c75f9e
}
4c75f9e
4c75f9e
void ItemPinnedLoader::addCommands()
4c75f9e
{
4c75f9e
    emit addCommands(commands());
4c75f9e
}
4c75f9e
4c75f9e
Q_EXPORT_PLUGIN2(itempinned, ItemPinnedLoader)