// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "widgetboxtreewidget.h"
#include "widgetboxcategorylistview.h"

// shared
#include <iconloader_p.h>
#include <sheet_delegate_p.h>
#include <QtDesigner/private/ui4_p.h>
#include <qdesigner_utils_p.h>
#include <pluginmanager_p.h>

// sdk
#include <QtDesigner/abstractformeditor.h>
#include <QtDesigner/abstractdnditem.h>
#include <QtDesigner/abstractsettings.h>

#include <QtUiPlugin/customwidget.h>

#include <QtWidgets/qapplication.h>
#include <QtWidgets/qheaderview.h>
#include <QtWidgets/qmenu.h>
#include <QtWidgets/qscrollbar.h>
#include <QtWidgets/qtreewidget.h>

#include <QtGui/qaction.h>
#include <QtGui/qactiongroup.h>
#include <QtGui/qevent.h>

#include <QtCore/qfile.h>
#include <QtCore/qtimer.h>
#include <QtCore/qdebug.h>

static const char widgetBoxRootElementC[] = "widgetbox";
static const char wbWidgetElementC[] = "widget";
static const char uiElementC[] = "ui";
static const char categoryElementC[] = "category";
static const char categoryEntryElementC[] = "categoryentry";
static const char wbNameAttributeC[] = "name";
static const char typeAttributeC[] = "type";
static const char iconAttributeC[] = "icon";
static const char defaultTypeValueC[] = "default";
static const char customValueC[] = "custom";
static const char iconPrefixC[] = "__qt_icon__";
static const char scratchPadValueC[] = "scratchpad";
static const char invisibleNameC[] = "[invisible]";

enum TopLevelRole  { NORMAL_ITEM, SCRATCHPAD_ITEM, CUSTOM_ITEM };

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

static void setTopLevelRole(TopLevelRole tlr, QTreeWidgetItem *item)
{
    item->setData(0, Qt::UserRole, QVariant(tlr));
}

static TopLevelRole topLevelRole(const  QTreeWidgetItem *item)
{
    return static_cast<TopLevelRole>(item->data(0, Qt::UserRole).toInt());
}

namespace qdesigner_internal {

WidgetBoxTreeWidget::WidgetBoxTreeWidget(QDesignerFormEditorInterface *core, QWidget *parent) :
    QTreeWidget(parent),
    m_core(core),
    m_iconMode(false),
    m_scratchPadDeleteTimer(nullptr)
{
    setFocusPolicy(Qt::NoFocus);
    setIndentation(0);
    setRootIsDecorated(false);
    setColumnCount(1);
    header()->hide();
    header()->setSectionResizeMode(QHeaderView::Stretch);
    setTextElideMode(Qt::ElideMiddle);
    setVerticalScrollMode(ScrollPerPixel);

    setItemDelegate(new SheetDelegate(this, this));

    connect(this, &QTreeWidget::itemPressed,
            this, &WidgetBoxTreeWidget::handleMousePress);
}

QIcon WidgetBoxTreeWidget::iconForWidget(const QString &iconName) const
{
    if (iconName.isEmpty())
        return qdesigner_internal::qtLogoIcon();

    if (iconName.startsWith(QLatin1StringView(iconPrefixC))) {
        const auto it = m_pluginIcons.constFind(iconName);
        if (it != m_pluginIcons.constEnd())
            return it.value();
    }
    return createIconSet(iconName);
}

WidgetBoxCategoryListView *WidgetBoxTreeWidget::categoryViewAt(int idx) const
{
    WidgetBoxCategoryListView *rc = nullptr;
    if (QTreeWidgetItem *cat_item = topLevelItem(idx))
        if (QTreeWidgetItem *embedItem = cat_item->child(0))
            rc = qobject_cast<WidgetBoxCategoryListView*>(itemWidget(embedItem, 0));
    Q_ASSERT(rc);
    return rc;
}

static const char widgetBoxSettingsGroupC[] = "WidgetBox";
static const char widgetBoxExpandedKeyC[] = "Closed categories";
static const char widgetBoxViewModeKeyC[] = "View mode";

void WidgetBoxTreeWidget::saveExpandedState() const
{
    QStringList closedCategories;
    if (const int numCategories = categoryCount()) {
        for (int i = 0; i < numCategories; ++i) {
            const QTreeWidgetItem *cat_item = topLevelItem(i);
            if (!cat_item->isExpanded())
                closedCategories.append(cat_item->text(0));
        }
    }
    QDesignerSettingsInterface *settings = m_core->settingsManager();
    settings->beginGroup(QLatin1StringView(widgetBoxSettingsGroupC));
    settings->setValue(QLatin1StringView(widgetBoxExpandedKeyC), closedCategories);
    settings->setValue(QLatin1StringView(widgetBoxViewModeKeyC), m_iconMode);
    settings->endGroup();
}

void  WidgetBoxTreeWidget::restoreExpandedState()
{
    using StringSet = QSet<QString>;
    QDesignerSettingsInterface *settings = m_core->settingsManager();
    const QString groupKey = QLatin1StringView(widgetBoxSettingsGroupC) + u'/';
    m_iconMode = settings->value(groupKey + QLatin1StringView(widgetBoxViewModeKeyC)).toBool();
    updateViewMode();
    const auto &closedCategoryList = settings->value(groupKey + QLatin1StringView(widgetBoxExpandedKeyC), QStringList()).toStringList();
    const StringSet closedCategories(closedCategoryList.cbegin(), closedCategoryList.cend());
    expandAll();
    if (closedCategories.isEmpty())
        return;

    if (const int numCategories = categoryCount()) {
        for (int i = 0; i < numCategories; ++i) {
            QTreeWidgetItem *item = topLevelItem(i);
            if (closedCategories.contains(item->text(0)))
                item->setExpanded(false);
            }
    }
}

WidgetBoxTreeWidget::~WidgetBoxTreeWidget()
{
    saveExpandedState();
}

void WidgetBoxTreeWidget::setFileName(const QString &file_name)
{
    m_file_name = file_name;
}

QString WidgetBoxTreeWidget::fileName() const
{
    return m_file_name;
}

bool WidgetBoxTreeWidget::save()
{
    if (fileName().isEmpty())
        return false;

    QFile file(fileName());
    if (!file.open(QIODevice::WriteOnly))
        return false;

    CategoryList cat_list;
    const int count = categoryCount();
    for (int i = 0; i < count; ++i)
        cat_list.append(category(i));

    QXmlStreamWriter writer(&file);
    writer.setAutoFormatting(true);
    writer.setAutoFormattingIndent(1);
    writer.writeStartDocument();
    writeCategories(writer, cat_list);
    writer.writeEndDocument();

    return true;
}

void WidgetBoxTreeWidget::slotSave()
{
    save();
}

void WidgetBoxTreeWidget::handleMousePress(QTreeWidgetItem *item)
{
    if (item == nullptr)
        return;

    if (QApplication::mouseButtons() != Qt::LeftButton)
        return;

    if (item->parent() == nullptr) {
        item->setExpanded(!item->isExpanded());
        return;
    }
}

int WidgetBoxTreeWidget::ensureScratchpad()
{
    const int existingIndex = indexOfScratchpad();
    if (existingIndex != -1)
         return existingIndex;

    QTreeWidgetItem *scratch_item = new QTreeWidgetItem(this);
    scratch_item->setText(0, tr("Scratchpad"));
    setTopLevelRole(SCRATCHPAD_ITEM, scratch_item);
    addCategoryView(scratch_item, false); // Scratchpad in list mode.
    return categoryCount() - 1;
}

WidgetBoxCategoryListView *WidgetBoxTreeWidget::addCategoryView(QTreeWidgetItem *parent, bool iconMode)
{
    QTreeWidgetItem *embed_item = new QTreeWidgetItem(parent);
    embed_item->setFlags(Qt::ItemIsEnabled);
    WidgetBoxCategoryListView *categoryView = new WidgetBoxCategoryListView(m_core, this);
    categoryView->setViewMode(iconMode ? QListView::IconMode : QListView::ListMode);
    connect(categoryView, &WidgetBoxCategoryListView::scratchPadChanged,
            this, &WidgetBoxTreeWidget::slotSave);
    connect(categoryView, &WidgetBoxCategoryListView::widgetBoxPressed,
            this, &WidgetBoxTreeWidget::widgetBoxPressed);
    connect(categoryView, &WidgetBoxCategoryListView::itemRemoved,
            this, &WidgetBoxTreeWidget::slotScratchPadItemDeleted);
    connect(categoryView, &WidgetBoxCategoryListView::lastItemRemoved,
            this, &WidgetBoxTreeWidget::slotLastScratchPadItemDeleted);
    setItemWidget(embed_item, 0, categoryView);
    return categoryView;
}

int WidgetBoxTreeWidget::indexOfScratchpad() const
{
    if (const int numTopLevels =  topLevelItemCount()) {
        for (int i = numTopLevels - 1; i >= 0; --i) {
            if (topLevelRole(topLevelItem(i)) == SCRATCHPAD_ITEM)
                return i;
        }
    }
    return -1;
}

int WidgetBoxTreeWidget::indexOfCategory(const QString &name) const
{
    const int topLevelCount = topLevelItemCount();
    for (int i = 0; i < topLevelCount; ++i) {
        if (topLevelItem(i)->text(0) == name)
            return i;
    }
    return -1;
}

bool WidgetBoxTreeWidget::load(QDesignerWidgetBox::LoadMode loadMode)
{
    switch (loadMode) {
    case QDesignerWidgetBox::LoadReplace:
        clear();
        break;
    case QDesignerWidgetBox::LoadCustomWidgetsOnly:
        addCustomCategories(true);
        updateGeometries();
        return true;
    default:
        break;
    }

    const QString name = fileName();

    QFile f(name);
    if (!f.open(QIODevice::ReadOnly)) // Might not exist at first startup
        return false;

    const QString contents = QString::fromUtf8(f.readAll());
    if (!loadContents(contents))
        return false;
    if (topLevelItemCount() > 0) {
        // QTBUG-93099: Set the single step to the item height to have some
        // size-related value.
        const auto itemHeight = visualItemRect(topLevelItem(0)).height();
        verticalScrollBar()->setSingleStep(itemHeight);
    }
    return true;
}

bool WidgetBoxTreeWidget::loadContents(const QString &contents)
{
    QString errorMessage;
    CategoryList cat_list;
    if (!readCategories(m_file_name, contents, &cat_list, &errorMessage)) {
        qdesigner_internal::designerWarning(errorMessage);
        return false;
    }

    for (const Category &cat : std::as_const(cat_list))
        addCategory(cat);

    addCustomCategories(false);
    // Restore which items are expanded
    restoreExpandedState();
    return true;
}

void WidgetBoxTreeWidget::addCustomCategories(bool replace)
{
    if (replace) {
        // clear out all existing custom widgets
        if (const int numTopLevels =  topLevelItemCount()) {
            for (int t = 0; t < numTopLevels ; ++t)
                categoryViewAt(t)->removeCustomWidgets();
        }
    }
    // re-add
    const CategoryList customList = loadCustomCategoryList();
    for (const auto &c : customList)
        addCategory(c);
}

static inline QString msgXmlError(const QString &fileName, const QXmlStreamReader &r)
{
    return QDesignerWidgetBox::tr("An error has been encountered at line %1 of %2: %3")
            .arg(r.lineNumber()).arg(fileName, r.errorString());
}

bool WidgetBoxTreeWidget::readCategories(const QString &fileName, const QString &contents,
                                       CategoryList *cats, QString *errorMessage)
{
    // Read widget box XML:
    //
    //<widgetbox version="4.5">
    // <category name="Layouts">
    //  <categoryentry name="Vertical Layout" icon="win/editvlayout.png" type="default">
    //   <widget class="QListWidget" ...>
    // ...

    QXmlStreamReader reader(contents);


    // Entries of category with name="invisible" should be ignored
    bool ignoreEntries = false;

    while (!reader.atEnd()) {
        switch (reader.readNext()) {
        case QXmlStreamReader::StartElement: {
            const auto tag = reader.name();
            if (tag == QLatin1StringView(widgetBoxRootElementC)) {
                //<widgetbox version="4.5">
                continue;
            }
            if (tag == QLatin1StringView(categoryElementC)) {
                // <category name="Layouts">
                const QXmlStreamAttributes attributes = reader.attributes();
                const QString categoryName = attributes.value(QLatin1StringView(wbNameAttributeC)).toString();
                if (categoryName == QLatin1StringView(invisibleNameC)) {
                    ignoreEntries = true;
                } else {
                    Category category(categoryName);
                    if (attributes.value(QLatin1StringView(typeAttributeC)) == QLatin1StringView(scratchPadValueC))
                        category.setType(Category::Scratchpad);
                    cats->push_back(category);
                }
                continue;
            }
            if (tag == QLatin1StringView(categoryEntryElementC)) {
                //  <categoryentry name="Vertical Layout" icon="win/editvlayout.png" type="default">
                if (!ignoreEntries) {
                    QXmlStreamAttributes attr = reader.attributes();
                    const QString widgetName = attr.value(QLatin1StringView(wbNameAttributeC)).toString();
                    const QString widgetIcon = attr.value(QLatin1StringView(iconAttributeC)).toString();
                    const WidgetBoxTreeWidget::Widget::Type widgetType =
                        attr.value(QLatin1StringView(typeAttributeC)).toString()
                            == QLatin1StringView(customValueC) ?
                        WidgetBoxTreeWidget::Widget::Custom :
                        WidgetBoxTreeWidget::Widget::Default;

                    Widget w;
                    w.setName(widgetName);
                    w.setIconName(widgetIcon);
                    w.setType(widgetType);
                    if (!readWidget(&w, contents, reader))
                        continue;

                    cats->back().addWidget(w);
                } // ignoreEntries
                continue;
            }
            break;
        }
        case QXmlStreamReader::EndElement: {
           const auto tag = reader.name();
           if (tag == QLatin1StringView(widgetBoxRootElementC)) {
               continue;
           }
           if (tag == QLatin1StringView(categoryElementC)) {
               ignoreEntries = false;
               continue;
           }
           if (tag == QLatin1StringView(categoryEntryElementC)) {
               continue;
           }
           break;
        }
        default: break;
        }
    }

    if (reader.hasError()) {
        *errorMessage = msgXmlError(fileName, reader);
        return false;
    }

    return true;
}

/*!
 * Read out a widget within a category. This can either be
 * enclosed in a <ui> element or a (legacy) <widget> element which may
 * contain nested <widget> elements.
 *
 * Examples:
 *
 * <ui language="c++">
 *  <widget class="MultiPageWidget" name="multipagewidget"> ... </widget>
 *  <customwidgets>...</customwidgets>
 * <ui>
 *
 * or
 *
 * <widget>
 *   <widget> ... </widget>
 *   ...
 * <widget>
 *
 * Returns true on success, false if end was reached or an error has been encountered
 * in which case the reader has its error flag set. If successful, the current item
 * of the reader will be the closing element (</ui> or </widget>)
 */
bool WidgetBoxTreeWidget::readWidget(Widget *w, const QString &xml, QXmlStreamReader &r)
{
    qint64 startTagPosition =0, endTagPosition = 0;

    int nesting = 0;
    bool endEncountered = false;
    bool parsedWidgetTag = false;
    while (!endEncountered) {
        const qint64 currentPosition = r.characterOffset();
        switch(r.readNext()) {
        case QXmlStreamReader::StartElement:
            if (nesting++ == 0) {
                // First element must be <ui> or (legacy) <widget>
                const auto name = r.name();
                if (name == QLatin1StringView(uiElementC)) {
                    startTagPosition = currentPosition;
                } else {
                    if (name == QLatin1StringView(wbWidgetElementC)) {
                        startTagPosition = currentPosition;
                        parsedWidgetTag = true;
                    } else {
                        r.raiseError(QDesignerWidgetBox::tr("Unexpected element <%1> encountered when parsing for <widget> or <ui>").arg(name.toString()));
                        return false;
                    }
                }
            } else {
                // We are within <ui> looking for the first <widget> tag
                if (!parsedWidgetTag && r.name() == QLatin1StringView(wbWidgetElementC)) {
                    parsedWidgetTag = true;
                }
            }
            break;
        case QXmlStreamReader::EndElement:
            // Reached end of widget?
            if (--nesting == 0)  {
                endTagPosition = r.characterOffset();
                endEncountered = true;
            }
            break;
        case QXmlStreamReader::EndDocument:
            r.raiseError(QDesignerWidgetBox::tr("Unexpected end of file encountered when parsing widgets."));
            return false;
        case QXmlStreamReader::Invalid:
            return false;
        default:
            break;
        }
    }
    if (!parsedWidgetTag) {
        r.raiseError(QDesignerWidgetBox::tr("A widget element could not be found."));
        return false;
    }
    // Oddity: Startposition is 1 off
    QString widgetXml = xml.mid(startTagPosition, endTagPosition - startTagPosition);
    if (!widgetXml.startsWith(u'<'))
        widgetXml.prepend(u'<');
    w->setDomXml(widgetXml);
    return true;
}

void WidgetBoxTreeWidget::writeCategories(QXmlStreamWriter &writer, const CategoryList &cat_list) const
{
    const QString widgetbox = QLatin1StringView(widgetBoxRootElementC);
    const QString name = QLatin1StringView(wbNameAttributeC);
    const QString type = QLatin1StringView(typeAttributeC);
    const QString icon = QLatin1StringView(iconAttributeC);
    const QString defaultType = QLatin1StringView(defaultTypeValueC);
    const QString category = QLatin1StringView(categoryElementC);
    const QString categoryEntry = QLatin1StringView(categoryEntryElementC);
    const QString iconPrefix = QLatin1StringView(iconPrefixC);

    //
    // <widgetbox>
    //   <category name="Layouts">
    //     <categoryEntry name="Vertical Layout" type="default" icon="win/editvlayout.png">
    //       <ui>
    //        ...
    //       </ui>
    //     </categoryEntry>
    //     ...
    //   </category>
    //   ...
    // </widgetbox>
    //

    writer.writeStartElement(widgetbox);

    for (const Category &cat : cat_list) {
        writer.writeStartElement(category);
        writer.writeAttribute(name, cat.name());
        if (cat.type() == Category::Scratchpad)
            writer.writeAttribute(type, QLatin1StringView(scratchPadValueC));

        const int widgetCount = cat.widgetCount();
        for (int i = 0; i < widgetCount; ++i) {
           const  Widget wgt = cat.widget(i);
            if (wgt.type() == Widget::Custom)
                continue;

            writer.writeStartElement(categoryEntry);
            writer.writeAttribute(name, wgt.name());
            if (!wgt.iconName().startsWith(iconPrefix))
                writer.writeAttribute(icon, wgt.iconName());
            writer.writeAttribute(type, defaultType);

            const DomUI *domUI = QDesignerWidgetBox::xmlToUi(wgt.name(), WidgetBoxCategoryListView::widgetDomXml(wgt), false);
            if (domUI) {
                domUI->write(writer);
                delete domUI;
            }

            writer.writeEndElement(); // categoryEntry
        }
        writer.writeEndElement(); // categoryEntry
    }

    writer.writeEndElement(); // widgetBox
}

static int findCategory(const QString &name, const WidgetBoxTreeWidget::CategoryList &list)
{
    int idx = 0;
    for (const WidgetBoxTreeWidget::Category &cat : list) {
        if (cat.name() == name)
            return idx;
        ++idx;
    }
    return -1;
}

static inline bool isValidIcon(const QIcon &icon)
{
    if (!icon.isNull()) {
        const auto availableSizes = icon.availableSizes();
        return !availableSizes.isEmpty() && !availableSizes.constFirst().isEmpty();
    }
    return false;
}

WidgetBoxTreeWidget::CategoryList WidgetBoxTreeWidget::loadCustomCategoryList() const
{
    CategoryList result;

    const QDesignerPluginManager *pm = m_core->pluginManager();
    const QDesignerPluginManager::CustomWidgetList customWidgets = pm->registeredCustomWidgets();
    if (customWidgets.isEmpty())
        return result;

    static const QString customCatName = tr("Custom Widgets");

    const QString invisible = QLatin1StringView(invisibleNameC);
    const QString iconPrefix = QLatin1StringView(iconPrefixC);

    for (QDesignerCustomWidgetInterface *c : customWidgets) {
        const QString dom_xml = c->domXml();
        if (dom_xml.isEmpty())
            continue;

        const QString pluginName = c->name();
        const QDesignerCustomWidgetData data = pm->customWidgetData(c);
        QString displayName = data.xmlDisplayName();
        if (displayName.isEmpty())
            displayName = pluginName;

        QString cat_name = c->group();
        if (cat_name.isEmpty())
            cat_name = customCatName;
        else if (cat_name == invisible)
            continue;

        int idx = findCategory(cat_name, result);
        if (idx == -1) {
            result.append(Category(cat_name));
            idx = result.size() - 1;
        }
        Category &cat = result[idx];

        const QIcon icon = c->icon();

        QString icon_name;
        if (isValidIcon(icon)) {
            icon_name = iconPrefix;
            icon_name += pluginName;
            m_pluginIcons.insert(icon_name, icon);
        }

        cat.addWidget(Widget(displayName, dom_xml, icon_name, Widget::Custom));
    }

    return result;
}

void WidgetBoxTreeWidget::adjustSubListSize(QTreeWidgetItem *cat_item)
{
    QTreeWidgetItem *embedItem = cat_item->child(0);
    if (embedItem == nullptr)
        return;

    WidgetBoxCategoryListView *list_widget = static_cast<WidgetBoxCategoryListView*>(itemWidget(embedItem, 0));
    list_widget->setFixedWidth(header()->width());
    list_widget->doItemsLayout();
    const int height = qMax(list_widget->contentsSize().height() ,1);
    list_widget->setFixedHeight(height);
    embedItem->setSizeHint(0, QSize(-1, height - 1));
}

int WidgetBoxTreeWidget::categoryCount() const
{
    return topLevelItemCount();
}

WidgetBoxTreeWidget::Category WidgetBoxTreeWidget::category(int cat_idx) const
{
    if (cat_idx >= topLevelItemCount())
        return Category();

    QTreeWidgetItem *cat_item = topLevelItem(cat_idx);

    QTreeWidgetItem *embedItem = cat_item->child(0);
    WidgetBoxCategoryListView *categoryView = static_cast<WidgetBoxCategoryListView*>(itemWidget(embedItem, 0));

    Category result = categoryView->category();
    result.setName(cat_item->text(0));

    switch (topLevelRole(cat_item)) {
    case SCRATCHPAD_ITEM:
        result.setType(Category::Scratchpad);
        break;
    default:
        result.setType(Category::Default);
        break;
    }
    return result;
}

void WidgetBoxTreeWidget::addCategory(const Category &cat)
{
    if (cat.widgetCount() == 0)
        return;

    const bool isScratchPad = cat.type() == Category::Scratchpad;
    WidgetBoxCategoryListView *categoryView;
    QTreeWidgetItem *cat_item;

    if (isScratchPad) {
        const int idx = ensureScratchpad();
        categoryView = categoryViewAt(idx);
        cat_item = topLevelItem(idx);
    } else {
        const int existingIndex = indexOfCategory(cat.name());
        if (existingIndex == -1) {
            cat_item = new QTreeWidgetItem();
            cat_item->setText(0, cat.name());
            setTopLevelRole(NORMAL_ITEM, cat_item);
            // insert before scratchpad
            const int scratchPadIndex = indexOfScratchpad();
            if (scratchPadIndex == -1) {
                addTopLevelItem(cat_item);
            } else {
                insertTopLevelItem(scratchPadIndex, cat_item);
            }
            cat_item->setExpanded(true);
            categoryView = addCategoryView(cat_item, m_iconMode);
        } else {
            categoryView = categoryViewAt(existingIndex);
            cat_item = topLevelItem(existingIndex);
        }
    }
    // The same categories are read from the file $HOME, avoid duplicates
    const int widgetCount = cat.widgetCount();
    for (int i = 0; i < widgetCount; ++i) {
        const Widget w = cat.widget(i);
        if (!categoryView->containsWidget(w.name()))
            categoryView->addWidget(w, iconForWidget(w.iconName()), isScratchPad);
    }
    adjustSubListSize(cat_item);
}

void WidgetBoxTreeWidget::removeCategory(int cat_idx)
{
    if (cat_idx >= topLevelItemCount())
        return;
    delete takeTopLevelItem(cat_idx);
}

int WidgetBoxTreeWidget::widgetCount(int cat_idx) const
{
    if (cat_idx >= topLevelItemCount())
        return 0;
    // SDK functions want unfiltered access
    return categoryViewAt(cat_idx)->count(WidgetBoxCategoryListView::UnfilteredAccess);
}

WidgetBoxTreeWidget::Widget WidgetBoxTreeWidget::widget(int cat_idx, int wgt_idx) const
{
    if (cat_idx >= topLevelItemCount())
        return Widget();
    // SDK functions want unfiltered access
    WidgetBoxCategoryListView *categoryView = categoryViewAt(cat_idx);
    return categoryView->widgetAt(WidgetBoxCategoryListView::UnfilteredAccess, wgt_idx);
}

void WidgetBoxTreeWidget::addWidget(int cat_idx, const Widget &wgt)
{
    if (cat_idx >= topLevelItemCount())
        return;

    QTreeWidgetItem *cat_item = topLevelItem(cat_idx);
    WidgetBoxCategoryListView *categoryView = categoryViewAt(cat_idx);

    const bool scratch = topLevelRole(cat_item) == SCRATCHPAD_ITEM;
    categoryView->addWidget(wgt, iconForWidget(wgt.iconName()), scratch);
    adjustSubListSize(cat_item);
}

void WidgetBoxTreeWidget::removeWidget(int cat_idx, int wgt_idx)
{
    if (cat_idx >= topLevelItemCount())
        return;

    WidgetBoxCategoryListView *categoryView = categoryViewAt(cat_idx);

    // SDK functions want unfiltered access
    const WidgetBoxCategoryListView::AccessMode am = WidgetBoxCategoryListView::UnfilteredAccess;
    if (wgt_idx >= categoryView->count(am))
        return;

    categoryView->removeRow(am, wgt_idx);
}

void WidgetBoxTreeWidget::slotScratchPadItemDeleted()
{
    const int scratch_idx = indexOfScratchpad();
    QTreeWidgetItem *scratch_item = topLevelItem(scratch_idx);
    adjustSubListSize(scratch_item);
    save();
}

void WidgetBoxTreeWidget::slotLastScratchPadItemDeleted()
{
    // Remove the scratchpad in the next idle loop
    if (!m_scratchPadDeleteTimer) {
        m_scratchPadDeleteTimer = new QTimer(this);
        m_scratchPadDeleteTimer->setSingleShot(true);
        m_scratchPadDeleteTimer->setInterval(0);
        connect(m_scratchPadDeleteTimer, &QTimer::timeout,
                this, &WidgetBoxTreeWidget::deleteScratchpad);
    }
    if (!m_scratchPadDeleteTimer->isActive())
        m_scratchPadDeleteTimer->start();
}

void WidgetBoxTreeWidget::deleteScratchpad()
{
    const int idx = indexOfScratchpad();
    if (idx == -1)
        return;
    delete takeTopLevelItem(idx);
    save();
}


void WidgetBoxTreeWidget::slotListMode()
{
    m_iconMode = false;
    updateViewMode();
}

void WidgetBoxTreeWidget::slotIconMode()
{
    m_iconMode = true;
    updateViewMode();
}

void WidgetBoxTreeWidget::updateViewMode()
{
    if (const int numTopLevels = topLevelItemCount()) {
        for (int i = numTopLevels - 1; i >= 0; --i) {
            QTreeWidgetItem *topLevel = topLevelItem(i);
            // Scratch pad stays in list mode.
            const QListView::ViewMode viewMode  = m_iconMode && (topLevelRole(topLevel) != SCRATCHPAD_ITEM) ? QListView::IconMode : QListView::ListMode;
            WidgetBoxCategoryListView *categoryView = categoryViewAt(i);
            if (viewMode != categoryView->viewMode()) {
                categoryView->setViewMode(viewMode);
                adjustSubListSize(topLevelItem(i));
            }
        }
    }

    updateGeometries();
}

void WidgetBoxTreeWidget::resizeEvent(QResizeEvent *e)
{
    QTreeWidget::resizeEvent(e);
    if (const int numTopLevels = topLevelItemCount()) {
        for (int i = numTopLevels - 1; i >= 0; --i)
            adjustSubListSize(topLevelItem(i));
    }
}

void WidgetBoxTreeWidget::contextMenuEvent(QContextMenuEvent *e)
{
    QTreeWidgetItem *item = itemAt(e->pos());

    const bool scratchpad_menu = item != nullptr
                            && item->parent() != nullptr
                            && topLevelRole(item->parent()) ==  SCRATCHPAD_ITEM;

    QMenu menu;
    menu.addAction(tr("Expand all"), this, &WidgetBoxTreeWidget::expandAll);
    menu.addAction(tr("Collapse all"), this, &WidgetBoxTreeWidget::collapseAll);
    menu.addSeparator();

    QAction *listModeAction = menu.addAction(tr("List View"));
    QAction *iconModeAction = menu.addAction(tr("Icon View"));
    listModeAction->setCheckable(true);
    iconModeAction->setCheckable(true);
    QActionGroup *viewModeGroup = new QActionGroup(&menu);
    viewModeGroup->addAction(listModeAction);
    viewModeGroup->addAction(iconModeAction);
    if (m_iconMode)
        iconModeAction->setChecked(true);
    else
        listModeAction->setChecked(true);
    connect(listModeAction, &QAction::triggered, this, &WidgetBoxTreeWidget::slotListMode);
    connect(iconModeAction, &QAction::triggered, this, &WidgetBoxTreeWidget::slotIconMode);

    if (scratchpad_menu) {
        menu.addSeparator();
        WidgetBoxCategoryListView *listView = qobject_cast<WidgetBoxCategoryListView *>(itemWidget(item, 0));
        Q_ASSERT(listView);
        menu.addAction(tr("Remove"), listView, &WidgetBoxCategoryListView::removeCurrentItem);
        if (!m_iconMode)
            menu.addAction(tr("Edit name"), listView, &WidgetBoxCategoryListView::editCurrentItem);
    }
    e->accept();
    menu.exec(mapToGlobal(e->pos()));
}

void WidgetBoxTreeWidget::dropWidgets(const QList<QDesignerDnDItemInterface*> &item_list)
{
    QTreeWidgetItem *scratch_item = nullptr;
    WidgetBoxCategoryListView *categoryView = nullptr;
    bool added = false;

    for (QDesignerDnDItemInterface *item : item_list) {
        QWidget *w = item->widget();
        if (w == nullptr)
            continue;

        DomUI *dom_ui = item->domUi();
        if (dom_ui == nullptr)
            continue;

        const int scratch_idx = ensureScratchpad();
        scratch_item = topLevelItem(scratch_idx);
        categoryView = categoryViewAt(scratch_idx);

        // Temporarily remove the fake toplevel in-between
        DomWidget *fakeTopLevel = dom_ui->takeElementWidget();
        DomWidget *firstWidget = nullptr;
        if (fakeTopLevel && !fakeTopLevel->elementWidget().isEmpty()) {
            firstWidget = fakeTopLevel->elementWidget().constFirst();
            dom_ui->setElementWidget(firstWidget);
        } else {
            dom_ui->setElementWidget(fakeTopLevel);
            continue;
        }

        // Serialize to XML
        QString xml;
        {
            QXmlStreamWriter writer(&xml);
            writer.setAutoFormatting(true);
            writer.setAutoFormattingIndent(1);
            writer.writeStartDocument();
            dom_ui->write(writer);
            writer.writeEndDocument();
        }

        // Insert fake toplevel again
        dom_ui->takeElementWidget();
        dom_ui->setElementWidget(fakeTopLevel);

        const Widget wgt = Widget(w->objectName(), xml);
        categoryView->addWidget(wgt, iconForWidget(wgt.iconName()), true);
        scratch_item->setExpanded(true);
        added = true;
    }

    if (added) {
        save();
        activateWindow();
        // Is the new item visible in filtered mode?
        const WidgetBoxCategoryListView::AccessMode am = WidgetBoxCategoryListView::FilteredAccess;
        if (const int count = categoryView->count(am))
            categoryView->setCurrentItem(am, count - 1);
        categoryView->adjustSize(); // XXX
        adjustSubListSize(scratch_item);
        doItemsLayout();
        scrollToItem(scratch_item, PositionAtTop);
    }
}

void WidgetBoxTreeWidget::filter(const QString &f)
{
    const bool empty = f.isEmpty();
    const int numTopLevels = topLevelItemCount();
    bool changed = false;
    for (int i = 0; i < numTopLevels; i++) {
        QTreeWidgetItem *tl = topLevelItem(i);
        WidgetBoxCategoryListView *categoryView = categoryViewAt(i);
        // Anything changed? -> Enable the category
        const int oldCount = categoryView->count(WidgetBoxCategoryListView::FilteredAccess);
        categoryView->filter(f, Qt::CaseInsensitive);
        const int newCount = categoryView->count(WidgetBoxCategoryListView::FilteredAccess);
        if (oldCount != newCount) {
            changed = true;
            const bool categoryEnabled = newCount > 0 || empty;
            if (categoryEnabled) {
                categoryView->adjustSize();
                adjustSubListSize(tl);
            }
            setRowHidden (i, QModelIndex(), !categoryEnabled);
        }
    }
    if (changed)
        updateGeometries();
}

}  // namespace qdesigner_internal

QT_END_NAMESPACE
