diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d167f920..11fda26c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,109 +1,110 @@ project(kirigami) if (NOT STATIC_LIBRARY) ecm_create_qm_loader(kirigami_QM_LOADER libkirigami2plugin_qt) else() set(KIRIGAMI_STATIC_FILES libkirigami/basictheme.cpp libkirigami/platformtheme.cpp libkirigami/tabletmodewatcher.cpp libkirigami/kirigamipluginfactory.cpp) endif() include_directories(${CMAKE_CURRENT_SOURCE_DIR}/libkirigami ${CMAKE_CURRENT_BINARY_DIR}/libkirigami) set(kirigami_SRCS kirigamiplugin.cpp + columnview.cpp enums.cpp delegaterecycler.cpp desktopicon.cpp settings.cpp formlayoutattached.cpp scenepositionattached.cpp mnemonicattached.cpp ${kirigami_QM_LOADER} ${KIRIGAMI_STATIC_FILES} ) add_subdirectory(libkirigami) if(STATIC_LIBRARY) # `rcc` is a bit dumb and isn't designed to use auto generated files, to # avoid poluting the source directory, use absolute paths set(kirigami_QML_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../) # First, pre-process the QRC to add the files associated with the right Qt # version. configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/../kirigami.qrc.in ${CMAKE_CURRENT_BINARY_DIR}/../kirigami.qrc @ONLY ) # When using the static library, all QML files need to be shipped within the # .a file. qt5_add_resources( RESOURCES ${CMAKE_CURRENT_BINARY_DIR}/../kirigami.qrc ) endif(STATIC_LIBRARY) add_library(kirigamiplugin ${kirigami_SRCS} ${RESOURCES}) if(STATIC_LIBRARY) SET_TARGET_PROPERTIES(kirigamiplugin PROPERTIES AUTOMOC_MOC_OPTIONS -Muri=org.kde.kirigami) if (UNIX AND NOT CMAKE_SYSTEM_NAME STREQUAL "Android" AND NOT(APPLE) AND NOT(DISABLE_DBUS)) set(Kirigami_EXTRA_LIBS Qt5::DBus) else() set(Kirigami_EXTRA_LIBS "") endif() else(STATIC_LIBRARY) set(Kirigami_EXTRA_LIBS KF5::Kirigami2) endif(STATIC_LIBRARY) target_link_libraries(kirigamiplugin PUBLIC Qt5::Core PRIVATE ${Kirigami_EXTRA_LIBS} Qt5::Qml Qt5::Quick Qt5::QuickControls2 ) if (NOT STATIC_LIBRARY) add_custom_target(copy) file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/bin/org/kde/kirigami.2) add_custom_command(TARGET copy PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/controls ${CMAKE_BINARY_DIR}/bin/org/kde/kirigami.2/) add_custom_command(TARGET copy PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/styles ${CMAKE_BINARY_DIR}/bin/org/kde/kirigami.2/styles) add_dependencies(kirigamiplugin copy) install(DIRECTORY controls/ DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2) if (PLASMA_ENABLED) install(DIRECTORY styles/Plasma DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2/styles) endif() if (DESKTOP_ENABLED) install(DIRECTORY styles/org.kde.desktop DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2/styles) endif() if (PLASMA_ENABLED AND DESKTOP_ENABLED) install(DIRECTORY styles/org.kde.desktop.plasma DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2/styles) endif() install(DIRECTORY styles/Material DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2/styles) install(FILES ${platformspecific} DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2) include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME Kirigami2 LIB_NAME KF5Kirigami2 DEPS "core qml quick svg" FILENAME_VAR PRI_FILENAME ) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) endif(NOT STATIC_LIBRARY) install(TARGETS kirigamiplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2) diff --git a/src/columnview.cpp b/src/columnview.cpp new file mode 100644 index 00000000..8196346c --- /dev/null +++ b/src/columnview.cpp @@ -0,0 +1,1276 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program 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 Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "columnview.h" +#include "columnview_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +QHash ColumnView::m_attachedObjects = QHash(); + +class QmlComponentsPoolSingleton +{ +public: + QmlComponentsPoolSingleton() + {} + + QmlComponentsPool self; +}; + +Q_GLOBAL_STATIC(QmlComponentsPoolSingleton, privateQmlComponentsPoolSelf) + + +QmlComponentsPool::QmlComponentsPool(QObject *parent) + : QObject(parent) +{} + +void QmlComponentsPool::initialize(QQmlEngine *engine) +{ + if (!engine || m_instance) { + return; + } + + QQmlComponent *component = new QQmlComponent(engine, this); + + component->setData(QByteArrayLiteral("import QtQuick 2.7\n" + "import org.kde.kirigami 2.7 as Kirigami\n" + "QtObject {\n" + "id: root\n" + "readonly property Kirigami.Units units: Kirigami.Units\n" + "readonly property Component separator: Kirigami.Separator {" + "property Item column\n" + "visible: column.Kirigami.ColumnView.view.contentX < column.x;" + "anchors.top: column.top;" + "anchors.bottom: column.bottom;" + "}" + "}"), QUrl()); + + m_instance = component->create(); + //qWarning()<errors(); + Q_ASSERT(m_instance); + + m_separatorComponent = m_instance->property("separator").value(); + Q_ASSERT(m_separatorComponent); + + m_units = m_instance->property("units").value(); + Q_ASSERT(m_units); + + connect(m_units, SIGNAL(gridUnitChanged()), this, SIGNAL(gridUnitChanged())); + connect(m_units, SIGNAL(longDurationChanged()), this, SIGNAL(longDurationChanged())); +} + +QmlComponentsPool::~QmlComponentsPool() +{} + + +///////// + +ColumnViewAttached::ColumnViewAttached(QObject *parent) + : QObject(parent) +{} + +ColumnViewAttached::~ColumnViewAttached() +{} + +void ColumnViewAttached::setIndex(int index) +{ + if (!m_customFillWidth && m_view) { + const bool oldFillWidth = m_fillWidth; + m_fillWidth = index == m_view->count() - 1; + if (oldFillWidth != m_fillWidth) { + emit fillWidthChanged(); + } + } + + if (index == m_index) { + return; + } + + m_index = index; + emit indexChanged(); +} + +int ColumnViewAttached::index() const +{ + return m_index; +} + +void ColumnViewAttached::setFillWidth(bool fill) +{ + if (m_view) { + disconnect(m_view.data(), &ColumnView::countChanged, this, nullptr); + } + m_customFillWidth = true; + + if (fill == m_fillWidth) { + return; + } + + m_fillWidth = fill; + emit fillWidthChanged(); +} + +bool ColumnViewAttached::fillWidth() const +{ + return m_fillWidth; +} + +qreal ColumnViewAttached::reservedSpace() const +{ + return m_reservedSpace; +} + +void ColumnViewAttached::setReservedSpace(qreal space) +{ + if (m_view) { + disconnect(m_view.data(), &ColumnView::columnWidthChanged, this, nullptr); + } + m_customReservedSpace = true; + + if (qFuzzyCompare(space, m_reservedSpace)) { + return; + } + + m_reservedSpace = space; + emit reservedSpaceChanged(); +} + +ColumnView *ColumnViewAttached::view() +{ + return m_view; +} + +void ColumnViewAttached::setView(ColumnView *view) +{ + if (view == m_view) { + return; + } + + if (m_view) { + disconnect(m_view.data(), nullptr, this, nullptr); + } + m_view = view; + + if (!m_customFillWidth && m_view) { + m_fillWidth = m_index == m_view->count() - 1; + connect(m_view.data(), &ColumnView::countChanged, this, [this]() { + m_fillWidth = m_index == m_view->count() - 1; + emit fillWidthChanged(); + }); + } + if (!m_customReservedSpace && m_view) { + m_reservedSpace = m_view->columnWidth(); + connect(m_view.data(), &ColumnView::columnWidthChanged, this, [this]() { + m_reservedSpace = m_view->columnWidth(); + emit reservedSpaceChanged(); + }); + } + + emit viewChanged(); +} + +QQuickItem *ColumnViewAttached::originalParent() const +{ + return m_originalParent; +} + +void ColumnViewAttached::setOriginalParent(QQuickItem *parent) +{ + m_originalParent = parent; +} + +bool ColumnViewAttached::shouldDeleteOnRemove() const +{ + return m_shouldDeleteOnRemove; +} + +void ColumnViewAttached::setShouldDeleteOnRemove(bool del) +{ + m_shouldDeleteOnRemove = del; +} + +bool ColumnViewAttached::preventStealing() const +{ + return m_preventStealing; +} + +void ColumnViewAttached::setPreventStealing(bool prevent) +{ + if (prevent == m_preventStealing) { + return; + } + + m_preventStealing = prevent; + emit preventStealingChanged(); +} + + + +///////// + +ContentItem::ContentItem(ColumnView *parent) + : QQuickItem(parent), + m_view(parent) +{ + m_slideAnim = new QPropertyAnimation(this); + m_slideAnim->setTargetObject(this); + m_slideAnim->setPropertyName("x"); + //NOTE: the duration will be taked from kirigami units upon classBegin + m_slideAnim->setDuration(0); + m_slideAnim->setEasingCurve(QEasingCurve(QEasingCurve::InOutQuad)); + connect(m_slideAnim, &QPropertyAnimation::finished, this, [this] () { + if (!m_view->currentItem()) { + m_view->setCurrentIndex(m_items.indexOf(m_viewAnchorItem)); + } else { + QRectF mapped = m_view->currentItem()->mapRectToItem(this, QRectF(QPointF(0, 0), m_view->currentItem()->size())); + if (!QRectF(QPointF(0, 0), size()).intersects(mapped)) { + m_view->setCurrentIndex(m_items.indexOf(m_viewAnchorItem)); + } + } + }); +} + +ContentItem::~ContentItem() +{} + +void ContentItem::setBoundedX(qreal x) +{ + if (!parentItem()) { + return; + } + m_slideAnim->stop(); + setX(qRound(qBound(qMin(0.0, -width()+parentItem()->width()), x, 0.0))); +} + +void ContentItem::animateX(qreal newX) +{ + if (!parentItem()) { + return; + } + + const qreal to = qRound(qBound(qMin(0.0, -width()+parentItem()->width()), newX, 0.0)); + + m_slideAnim->stop(); + m_slideAnim->setStartValue(x()); + m_slideAnim->setEndValue(to); + m_slideAnim->start(); +} + +void ContentItem::snapToItem() +{ + QQuickItem *firstItem = childAt(-x(), 0); + if (!firstItem) { + return; + } + QQuickItem *nextItem = childAt(firstItem->x() + firstItem->width() + 1, 0); + + //need to make the last item visible? + if (nextItem && width() - (-x() + m_view->width()) < -x() - firstItem->x()) { + m_viewAnchorItem = nextItem; + animateX(-nextItem->x()); + + //The first one found? + } else if (-x() <= firstItem->x() + firstItem->width()/2 || !nextItem) { + m_viewAnchorItem = firstItem; + animateX(-firstItem->x()); + + //the second? + } else { + m_viewAnchorItem = nextItem; + animateX(-nextItem->x()); + } +} + +qreal ContentItem::childWidth(QQuickItem *child) +{ + if (!parentItem()) { + return 0.0; + } + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(child, true)); + + if (m_columnResizeMode == ColumnView::SingleColumn) { + return qRound(parentItem()->width()); + + } else if (attached->fillWidth()) { + return qRound(qBound(m_columnWidth, (parentItem()->width() - attached->reservedSpace()), parentItem()->width())); + + } else if (m_columnResizeMode == ColumnView::FixedColumns) { + return qRound(qMin(parentItem()->width(), m_columnWidth)); + + // DynamicColumns + } else { + //TODO:look for Layout size hints + qreal width = child->implicitWidth(); + + if (width < 1.0) { + width = m_columnWidth; + } + + return qRound(qMin(m_view->width(), width)); + } +} + +void ContentItem::layoutItems() +{ + qreal partialWidth = 0; + int i = 0; + for (QQuickItem *child : m_items) { + if (child->isVisible()) { + child->setSize(QSizeF(childWidth(child), height())); + child->setPosition(QPointF(partialWidth, 0.0)); + partialWidth += child->width(); + } + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(child, true)); + attached->setIndex(i++); + } + setWidth(partialWidth); + + const qreal newContentX = m_viewAnchorItem ? -m_viewAnchorItem->x() : 0.0; + if (m_shouldAnimate) { + animateX(newContentX); + } else { + setBoundedX(newContentX); + } + setY(0); + updateVisibleItems(); +} + +void ContentItem::updateVisibleItems() +{ + QList newItems; + + for (auto *item : m_items) { + if (item->isVisible() && item->x() + x() < width() && item->x() + item->width() + x() > 0) { + newItems << item; + } + } + + const QQuickItem *oldFirstVisibleItem = m_visibleItems.isEmpty() ? nullptr : qobject_cast(m_visibleItems.first()); + const QQuickItem *oldLastVisibleItem = m_visibleItems.isEmpty() ? nullptr : qobject_cast(m_visibleItems.last()); + + if (newItems != m_visibleItems) { + m_visibleItems = newItems; + emit m_view->visibleItemsChanged(); + if (!newItems.isEmpty() && m_visibleItems.first() != oldFirstVisibleItem) { + emit m_view->firstVisibleItemChanged(); + } + if (!newItems.isEmpty() && m_visibleItems.last() != oldLastVisibleItem) { + emit m_view->lastVisibleItemChanged(); + } + } +} + +void ContentItem::forgetItem(QQuickItem *item) +{ + if (!m_items.contains(item)) { + return; + } + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setView(nullptr); + attached->setIndex(-1); + + disconnect(attached, nullptr, this, nullptr); + disconnect(item, nullptr, this, nullptr); + disconnect(item, nullptr, m_view, nullptr); + + QQuickItem *separatorItem = m_separators.take(item); + if (separatorItem) { + separatorItem->deleteLater(); + } + + const int index = m_items.indexOf(item); + m_items.removeAll(item); + updateVisibleItems(); + m_shouldAnimate = true; + m_view->polish(); + item->setVisible(false); + + if (index <= m_view->currentIndex()) { + m_view->setCurrentIndex(qBound(0, index - 1, m_items.count() - 1)); + } + emit m_view->countChanged(); +} + +QQuickItem *ContentItem::ensureSeparator(QQuickItem *item) +{ + QQuickItem *separatorItem = m_separators.value(item); + + if (!separatorItem) { + separatorItem = qobject_cast(privateQmlComponentsPoolSelf->self.m_separatorComponent->beginCreate(QQmlEngine::contextForObject(item))); + if (separatorItem) { + separatorItem->setParentItem(item); + separatorItem->setZ(9999); + separatorItem->setProperty("column", QVariant::fromValue(item)); + privateQmlComponentsPoolSelf->self.m_separatorComponent->completeCreate(); + m_separators[item] = separatorItem; + } + } + + return separatorItem; +} + +void ContentItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + switch (change) { + case QQuickItem::ItemChildAddedChange: { + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(value.item, true)); + attached->setView(m_view); + + //connect(attached, &ColumnViewAttached::fillWidthChanged, m_view, &ColumnView::polish); + connect(attached, &ColumnViewAttached::fillWidthChanged, this, [this, attached](){ + m_view->polish(); + + }); + connect(attached, &ColumnViewAttached::reservedSpaceChanged, m_view, &ColumnView::polish); + + value.item->setVisible(true); + + if (!m_items.contains(value.item)) { + connect(value.item, &QQuickItem::widthChanged, m_view, &ColumnView::polish); + m_items << value.item; + } + + if (m_view->separatorVisible()) { + ensureSeparator(value.item); + } + + m_shouldAnimate = true; + m_view->polish(); + emit m_view->countChanged(); + break; + } + case QQuickItem::ItemChildRemovedChange: { + forgetItem(value.item); + break; + } + case QQuickItem::ItemVisibleHasChanged: + updateVisibleItems(); + break; + default: + break; + } + QQuickItem::itemChange(change, value); +} + +void ContentItem::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + updateVisibleItems(); + QQuickItem::geometryChanged(newGeometry, oldGeometry); +} + +void ContentItem::syncItemsOrder() +{ + if (m_items == childItems()) { + return; + } + + m_items = childItems(); + polish(); +} + +void ContentItem::updateRepeaterModel() +{ + if (!sender()) { + return; + } + + QObject *modelObj = sender()->property("model").value(); + + if (!modelObj) { + m_models.remove(sender()); + return; + } + + if (m_models[sender()]) { + disconnect(m_models[sender()], nullptr, this, nullptr); + } + + m_models[sender()] = modelObj; + + QAbstractItemModel *qaim = qobject_cast(modelObj); + + if (qaim) { + connect(qaim, &QAbstractItemModel::rowsMoved, this, &ContentItem::syncItemsOrder); + + } else { + connect(modelObj, SIGNAL(childrenChanged()), this, SLOT(syncItemsOrder())); + } +} + + + + + + + +ColumnView::ColumnView(QQuickItem *parent) + : QQuickItem(parent), + m_contentItem(nullptr) +{ + //NOTE: this is to *not* trigger itemChange + m_contentItem = new ContentItem(this); + setAcceptedMouseButtons(Qt::LeftButton); + setFiltersChildMouseEvents(true); + + connect(m_contentItem->m_slideAnim, &QPropertyAnimation::finished, this, [this] () { + m_moving = false; + emit movingChanged(); + }); + connect(m_contentItem, &ContentItem::widthChanged, this, &ColumnView::contentWidthChanged); + connect(m_contentItem, &ContentItem::xChanged, this, &ColumnView::contentXChanged); +} + +ColumnView::~ColumnView() +{ +} + +ColumnView::ColumnResizeMode ColumnView::columnResizeMode() const +{ + return m_contentItem->m_columnResizeMode; +} + +void ColumnView::setColumnResizeMode(ColumnResizeMode mode) +{ + if (m_contentItem->m_columnResizeMode == mode) { + return; + } + + m_contentItem->m_columnResizeMode = mode; + if (mode == SingleColumn && m_currentItem) { + m_contentItem->m_viewAnchorItem = m_currentItem; + } + m_contentItem->m_shouldAnimate = false; + polish(); + emit columnResizeModeChanged(); +} + +qreal ColumnView::columnWidth() const +{ + return m_contentItem->m_columnWidth; +} + +void ColumnView::setColumnWidth(qreal width) +{ + // Always forget the internal binding when the user sets anything, even the same value + disconnect(&privateQmlComponentsPoolSelf->self, &QmlComponentsPool::gridUnitChanged, this, nullptr); + + if (m_contentItem->m_columnWidth == width) { + return; + } + + m_contentItem->m_columnWidth = width; + m_contentItem->m_shouldAnimate = false; + polish(); + emit columnWidthChanged(); +} + +int ColumnView::currentIndex() const +{ + return m_currentIndex; +} + +void ColumnView::setCurrentIndex(int index) +{ + if (m_currentIndex == index || index < -1 || index >= m_contentItem->m_items.count()) { + return; + } + + m_currentIndex = index; + + if (index == -1) { + m_currentItem.clear(); + + } else { + m_currentItem = m_contentItem->m_items[index]; + Q_ASSERT(m_currentItem); + m_currentItem->forceActiveFocus(); + + // If the current item is not on view, scroll + QRectF mappedCurrent = m_currentItem->mapRectToItem(this, + QRectF(QPointF(0, 0), + m_currentItem->size())); + + if (m_contentItem->m_slideAnim->state() == QAbstractAnimation::Running) { + mappedCurrent.moveLeft(mappedCurrent.left() + m_contentItem->x() + m_contentItem->m_slideAnim->endValue().toInt()); + } + + //m_contentItem->m_slideAnim->stop(); + + QRectF contentsRect(QPointF(0, 0), size()); + + m_contentItem->m_shouldAnimate = true; + + if (!contentsRect.contains(mappedCurrent)) { + m_contentItem->m_viewAnchorItem = m_currentItem; + m_contentItem->animateX(-m_currentItem->x()); + } else { + m_contentItem->snapToItem(); + } + } + + emit currentIndexChanged(); + emit currentItemChanged(); +} + +QQuickItem *ColumnView::currentItem() +{ + return m_currentItem; +} + +QListColumnView::visibleItems() const +{ + return m_contentItem->m_visibleItems; +} + +QQuickItem *ColumnView::firstVisibleItem() const +{ + if (m_contentItem->m_visibleItems.isEmpty()) { + return nullptr; + } + + return qobject_cast(m_contentItem->m_visibleItems.first()); +} + +QQuickItem *ColumnView::lastVisibleItem() const +{ + if (m_contentItem->m_visibleItems.isEmpty()) { + return nullptr; + } + + return qobject_cast(m_contentItem->m_visibleItems.last()); +} + +int ColumnView::count() const +{ + return m_contentItem->m_items.count(); +} + +QQuickItem *ColumnView::contentItem() const +{ + return m_contentItem; +} + +int ColumnView::scrollDuration() const +{ + return m_contentItem->m_slideAnim->duration(); +} + +void ColumnView::setScrollDuration(int duration) +{ + disconnect(&privateQmlComponentsPoolSelf->self, &QmlComponentsPool::longDurationChanged, this, nullptr); + + if (m_contentItem->m_slideAnim->duration() == duration) { + return; + } + + m_contentItem->m_slideAnim->setDuration(duration); + emit scrollDurationChanged(); +} + +bool ColumnView::separatorVisible() const +{ + return m_separatorVisible; +} + +void ColumnView::setSeparatorVisible(bool visible) +{ + if (visible == m_separatorVisible) { + return; + } + + m_separatorVisible = visible; + + if (visible) { + for (QQuickItem *item : m_contentItem->m_items) { + QQuickItem *sep = m_contentItem->ensureSeparator(item); + if (sep) { + sep->setVisible(true); + } + } + } else { + for (QQuickItem *sep : m_contentItem->m_separators.values()) { + sep->setVisible(false); + } + } + + emit separatorVisibleChanged(); +} + +bool ColumnView::dragging() const +{ + return m_dragging; +} + +bool ColumnView::moving() const +{ + return m_moving; +} + +qreal ColumnView::contentWidth() const +{ + return m_contentItem->width(); +} + +qreal ColumnView::contentX() const +{ + return -m_contentItem->x(); +} + +void ColumnView::setContentX(qreal x) const +{ + m_contentItem->setX(qRound(-x)); +} + +bool ColumnView::interactive() const +{ + return m_interactive; +} + +void ColumnView::setInteractive(bool interactive) +{ + if (m_interactive != interactive) { + return; + } + + m_interactive = interactive; + + if (!m_interactive) { + if (m_dragging) { + m_dragging = false; + emit draggingChanged(); + } + + m_contentItem->snapToItem(); + setKeepMouseGrab(false); + } + + emit interactiveChanged(); +} + +void ColumnView::addItem(QQuickItem *item) +{ + insertItem(m_contentItem->m_items.length(), item); +} + +void ColumnView::insertItem(int pos, QQuickItem *item) +{ + if (!item || m_contentItem->m_items.contains(item)) { + return; + } + + m_contentItem->m_items.insert(qBound(0, pos, m_contentItem->m_items.length()), item); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setOriginalParent(item->parentItem()); + attached->setShouldDeleteOnRemove(item->parentItem() == nullptr && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership); + item->setParentItem(m_contentItem); + + item->forceActiveFocus(); + // We layout immediately to be sure all geometries are final after the return of this call + m_contentItem->m_shouldAnimate = false; + m_contentItem->layoutItems(); + emit contentChildrenChanged(); + + // In order to keep the same current item we need to increase the current index if displaced + // NOTE: just updating m_currentIndex does *not* update currentItem (which is what we need atm) while setCurrentIndex will update also currentItem + if (m_currentIndex >= pos) { + ++m_currentIndex; + emit currentIndexChanged(); + } + + emit itemInserted(pos, item); +} + +void ColumnView::moveItem(int from, int to) +{ + if (m_contentItem->m_items.isEmpty() + || from < 0 || from >= m_contentItem->m_items.length() + || to < 0 || to >= m_contentItem->m_items.length()) { + return; + } + + m_contentItem->m_items.move(from, to); + m_contentItem->m_shouldAnimate = true; + + if (from == m_currentIndex) { + m_currentIndex = to; + emit currentIndexChanged(); + } else if (from < m_currentIndex && to > m_currentIndex) { + --m_currentIndex; + emit currentIndexChanged(); + } else if (from > m_currentIndex && to <= m_currentIndex) { + ++m_currentIndex; + emit currentIndexChanged(); + } + + polish(); +} + +QQuickItem *ColumnView::removeItem(const QVariant &item) +{ + if (item.canConvert()) { + return removeItem(item.value()); + } else if (item.canConvert()) { + return removeItem(item.toInt()); + } else { + return nullptr; + } +} + +QQuickItem *ColumnView::removeItem(QQuickItem *item) +{ + if (m_contentItem->m_items.isEmpty() || !m_contentItem->m_items.contains(item)) { + return nullptr; + } + + const int index = m_contentItem->m_items.indexOf(item); + + // In order to keep the same current item we need to increase the current index if displaced + if (m_currentIndex >= index) { + setCurrentIndex(m_currentIndex - 1); + } + + m_contentItem->forgetItem(item); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); + + if (attached && attached->shouldDeleteOnRemove()) { + item->deleteLater(); + } else { + item->setParentItem(attached ? attached->originalParent() : nullptr); + } + + emit itemRemoved(item); + + return item; +} + +QQuickItem *ColumnView::removeItem(int pos) +{ + if (m_contentItem->m_items.isEmpty() + || pos < 0 || pos >= m_contentItem->m_items.length()) { + return nullptr; + } + + return removeItem(m_contentItem->m_items[pos]); +} + +QQuickItem *ColumnView::pop(QQuickItem *item) +{ + QQuickItem *removed = nullptr; + + while (!m_contentItem->m_items.isEmpty() && m_contentItem->m_items.last() != item) { + removed = removeItem(m_contentItem->m_items.last()); + // if no item has been passed, just pop one + if (!item) { + break; + } + } + return removed; +} + +void ColumnView::clear() +{ + ColumnViewAttached *attached = nullptr; + + for (QQuickItem *item : m_contentItem->m_items) { + attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); + if (attached && attached->shouldDeleteOnRemove()) { + item->deleteLater(); + } else { + item->setParentItem(attached ? attached->originalParent() : nullptr); + } + } + m_contentItem->m_items.clear(); + emit contentChildrenChanged(); +} + +bool ColumnView::containsItem(QQuickItem *item) +{ + return m_contentItem->m_items.contains(item); +} + +ColumnViewAttached *ColumnView::qmlAttachedProperties(QObject *object) +{ + return new ColumnViewAttached(object); +} + +void ColumnView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + m_contentItem->setHeight(newGeometry.height()); + m_contentItem->m_shouldAnimate = false; + polish(); + + m_contentItem->updateVisibleItems(); + QQuickItem::geometryChanged(newGeometry, oldGeometry); +} + +bool ColumnView::childMouseEventFilter(QQuickItem *item, QEvent *event) +{ + if (!m_interactive || item == m_contentItem) { + return QQuickItem::childMouseEventFilter(item, event); + } + + switch (event->type()) { + case QEvent::MouseButtonPress: { + m_contentItem->m_slideAnim->stop(); + if (item->property("preventStealing").toBool()) { + m_contentItem->snapToItem(); + return false; + } + QMouseEvent *me = static_cast(event); + m_oldMouseX = m_startMouseX = mapFromItem(item, me->localPos()).x(); + + me->setAccepted(false); + setKeepMouseGrab(false); + + // On press, we set the current index of the view to the root item + QQuickItem *candidateItem = item; + while (candidateItem->parentItem() && candidateItem->parentItem() != m_contentItem) { + candidateItem = candidateItem->parentItem(); + } + if (candidateItem->parentItem() == m_contentItem) { + setCurrentIndex(m_contentItem->m_items.indexOf(candidateItem)); + } + + break; + } + case QEvent::MouseMove: { + if ((!keepMouseGrab() && item->keepMouseGrab()) || item->property("preventStealing").toBool()) { + m_contentItem->snapToItem(); + return false; + } + + QQuickItem *candidateItem = item; + while (candidateItem->parentItem() && candidateItem->parentItem() != m_contentItem) { + candidateItem = candidateItem->parentItem(); + } + if (candidateItem->parentItem() == m_contentItem) { + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + if (attached->preventStealing()) { + return false; + } + } + + QMouseEvent *me = static_cast(event); + const QPointF pos = mapFromItem(item, me->localPos()); + + const bool wasDragging = m_dragging; + // If a drag happened, start to steal all events, use startDragDistance * 2 to give time to widgets to take the mouse grab by themselves + m_dragging = keepMouseGrab() || qAbs(mapFromItem(item, me->localPos()).x() - m_startMouseX) > qApp->styleHints()->startDragDistance() * 3; + + if (m_dragging != wasDragging) { + m_moving = true; + emit movingChanged(); + emit draggingChanged(); + } + + if (m_dragging) { + m_contentItem->setBoundedX(m_contentItem->x() + pos.x() - m_oldMouseX); + } + + m_oldMouseX = pos.x(); + + setKeepMouseGrab(m_dragging); + me->setAccepted(m_dragging); + return m_dragging; + break; + } + case QEvent::MouseButtonRelease: { + m_contentItem->snapToItem(); + if (m_dragging) { + m_dragging = false; + emit draggingChanged(); + } + + if (item->property("preventStealing").toBool()) { + return false; + } + QMouseEvent *me = static_cast(event); + event->accept(); + + //if a drag happened, don't pass the event + const bool block = keepMouseGrab(); + setKeepMouseGrab(false); + + me->setAccepted(block); + return block; + break; + } + default: + break; + } + + return QQuickItem::childMouseEventFilter(item, event); +} + +void ColumnView::mousePressEvent(QMouseEvent *event) +{ + if (!m_interactive) { + return; + } + + m_contentItem->snapToItem(); + m_oldMouseX = event->localPos().x(); + m_startMouseX = event->localPos().x(); + setKeepMouseGrab(false); + event->accept(); +} + +void ColumnView::mouseMoveEvent(QMouseEvent *event) +{ + if (!m_interactive) { + return; + } + + const bool wasDragging = m_dragging; + // Same startDragDistance * 2 as the event filter + m_dragging = keepMouseGrab() || qAbs(event->localPos().x() - m_startMouseX) > qApp->styleHints()->startDragDistance() * 2; + if (m_dragging != wasDragging) { + m_moving = true; + emit movingChanged(); + emit draggingChanged(); + } + + setKeepMouseGrab(m_dragging); + + if (m_dragging) { + m_contentItem->setBoundedX(m_contentItem->x() + event->pos().x() - m_oldMouseX); + } + + m_oldMouseX = event->pos().x(); + event->accept(); +} + +void ColumnView::mouseReleaseEvent(QMouseEvent *event) +{ + if (!m_interactive) { + return; + } + + if (m_dragging) { + m_dragging = false; + emit draggingChanged(); + } + + m_contentItem->snapToItem(); + setKeepMouseGrab(false); + event->accept(); +} + +void ColumnView::mouseUngrabEvent() +{ + if (m_dragging) { + m_dragging = false; + emit draggingChanged(); + } + + m_contentItem->snapToItem(); + setKeepMouseGrab(false); +} + +void ColumnView::classBegin() +{ + privateQmlComponentsPoolSelf->self.initialize(qmlEngine(this)); + + auto syncColumnWidth = [this]() { + m_contentItem->m_columnWidth = privateQmlComponentsPoolSelf->self.m_units->property("gridUnit").toInt() * 20; + emit columnWidthChanged(); + }; + + connect(&privateQmlComponentsPoolSelf->self, &QmlComponentsPool::gridUnitChanged, this, syncColumnWidth); + syncColumnWidth(); + + auto syncDuration = [this]() { + m_contentItem->m_slideAnim->setDuration(privateQmlComponentsPoolSelf->self.m_units->property("longDuration").toInt()); + emit scrollDurationChanged(); + }; + + connect(&privateQmlComponentsPoolSelf->self, &QmlComponentsPool::longDurationChanged, this, syncDuration); + syncDuration(); + + QQuickItem::classBegin(); +} + +void ColumnView::componentComplete() +{ + m_complete = true; + QQuickItem::componentComplete(); +} + +void ColumnView::updatePolish() +{ + m_contentItem->layoutItems(); +} + +void ColumnView::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + switch (change) { + case QQuickItem::ItemChildAddedChange: + if (m_contentItem && value.item != m_contentItem && !value.item->inherits("QQuickRepeater")) { + addItem(value.item); + } + break; + default: + break; + } + QQuickItem::itemChange(change, value); +} + +void ColumnView::contentChildren_append(QQmlListProperty *prop, QQuickItem *item) +{ + // This can only be called from QML + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + view->m_contentItem->m_items.append(item); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setOriginalParent(item->parentItem()); + attached->setShouldDeleteOnRemove(item->parentItem() == nullptr && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership); + + item->setParentItem(view->m_contentItem); +} + +int ColumnView::contentChildren_count(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return 0; + } + + return view->m_contentItem->m_items.count(); +} + +QQuickItem *ColumnView::contentChildren_at(QQmlListProperty *prop, int index) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return nullptr; + } + + if (index < 0 || index >= view->m_contentItem->m_items.count()) { + return nullptr; + } + return view->m_contentItem->m_items.value(index); +} + +void ColumnView::contentChildren_clear(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + return view->m_contentItem->m_items.clear(); +} + +QQmlListProperty ColumnView::contentChildren() +{ + return QQmlListProperty(this, nullptr, + contentChildren_append, + contentChildren_count, + contentChildren_at, + contentChildren_clear); +} + +void ColumnView::contentData_append(QQmlListProperty *prop, QObject *object) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + view->m_contentData.append(object); + QQuickItem *item = qobject_cast(object); + //exclude repeaters from layout + if (item && item->inherits("QQuickRepeater")) { + item->setParentItem(view); + + connect(item, SIGNAL(modelChanged()), view->m_contentItem, SLOT(updateRepeaterModel())); + + } else if (item) { + view->m_contentItem->m_items.append(item); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setOriginalParent(item->parentItem()); + attached->setShouldDeleteOnRemove(view->m_complete && !item->parentItem() && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership); + + item->setParentItem(view->m_contentItem); + + } else { + object->setParent(view); + } +} + +int ColumnView::contentData_count(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return 0; + } + + return view->m_contentData.count(); +} + +QObject *ColumnView::contentData_at(QQmlListProperty *prop, int index) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return nullptr; + } + + if (index < 0 || index >= view->m_contentData.count()) { + return nullptr; + } + return view->m_contentData.value(index); +} + +void ColumnView::contentData_clear(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + return view->m_contentData.clear(); +} + +QQmlListProperty ColumnView::contentData() +{ + return QQmlListProperty(this, nullptr, + contentData_append, + contentData_count, + contentData_at, + contentData_clear); +} + +#include "moc_columnview.cpp" diff --git a/src/columnview.h b/src/columnview.h new file mode 100644 index 00000000..75f9bced --- /dev/null +++ b/src/columnview.h @@ -0,0 +1,408 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program 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 Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +class ContentItem; +class ColumnView; + +/** + * This is an attached property to every item that is inserted in the ColumnView, + * used to access the view and page information such as the position and informations for layouting, such as fillWidth + * @since 2.7 + */ +class ColumnViewAttached : public QObject +{ + Q_OBJECT + + /** + * The index position of the column in the view, starting from 0 + */ + Q_PROPERTY(int index READ index WRITE setIndex NOTIFY indexChanged) + + /** + * If true, the column will expand to take the whole viewport space minus reservedSpace + */ + Q_PROPERTY(bool fillWidth READ fillWidth WRITE setFillWidth NOTIFY fillWidthChanged) + + /** + * When a column is fillWidth, it will keep reservedSpace amount of pixels from going to fill the full viewport width + */ + Q_PROPERTY(qreal reservedSpace READ reservedSpace WRITE setReservedSpace NOTIFY reservedSpaceChanged) + + /** + * Like the same property of MouseArea, when this is true, the column view won't + * try to manage events by itself when filtering from a child, not + * disturbing user interaction + */ + Q_PROPERTY(bool preventStealing READ preventStealing WRITE setPreventStealing NOTIFY preventStealingChanged) + + /** + * The view this column belongs to + */ + Q_PROPERTY(ColumnView *view READ view NOTIFY viewChanged) + +public: + ColumnViewAttached(QObject *parent = nullptr); + ~ColumnViewAttached(); + + void setIndex(int index); + int index() const; + + void setFillWidth(bool fill); + bool fillWidth() const; + + qreal reservedSpace() const; + void setReservedSpace(qreal space); + + ColumnView *view(); + void setView(ColumnView *view); + + //Private API, not for QML use + QQuickItem *originalParent() const; + void setOriginalParent(QQuickItem *parent); + + bool shouldDeleteOnRemove() const; + void setShouldDeleteOnRemove(bool del); + + bool preventStealing() const; + void setPreventStealing(bool prevent); + +Q_SIGNALS: + void indexChanged(); + void fillWidthChanged(); + void reservedSpaceChanged(); + void viewChanged(); + void preventStealingChanged(); + +private: + int m_index = -1; + bool m_fillWidth = false; + qreal m_reservedSpace = 0; + QPointer m_view; + QPointer m_originalParent; + bool m_customFillWidth = false; + bool m_customReservedSpace = false; + bool m_shouldDeleteOnRemove = true; + bool m_preventStealing = false; +}; + + +/** + * ColumnView is a container that lays out items horizontally in a row, + * when not all items fit in the ColumnView, it will behave ike a Flickable and will be a scrollable view which shows only a determined number of columns. + * The columns can either all have the same fixed size (recommended), + * size themselves with implicitWidth, or automatically expand to take all the available width: by default the last column will always be the expanding one. + * Items inside the Columnview can access info of the view and set layouting hints via the Columnview attached property. + * + * This is the base for the implementation of PageRow + * @since 2.7 + */ +class ColumnView : public QQuickItem +{ + Q_OBJECT + + /** + * The strategy to follow while automatically resizing the columns, + * the enum can have the following values: + * * FixedColumns: every column is fixed at the same width of the columnWidth property + * * DynamicColumns: columns take their width from their implicitWidth + * * SingleColumn: only one column at a time is shown, as wide as the viewport, eventual reservedSpace on the column's atttached property is ignored + */ + Q_PROPERTY(ColumnResizeMode columnResizeMode READ columnResizeMode WRITE setColumnResizeMode NOTIFY columnResizeModeChanged) + + /** + * The width of all columns when columnResizeMode is FixedColumns + */ + Q_PROPERTY(qreal columnWidth READ columnWidth WRITE setColumnWidth NOTIFY columnWidthChanged) + + /** + * How many columns this view containsItem*/ + Q_PROPERTY(int count READ count NOTIFY countChanged) + + /** + * The position of the currently active column. The current column will also have keyboard focus + */ + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + + /** + * The currently active column. The current column will also have keyboard focus + */ + Q_PROPERTY(QQuickItem *currentItem READ currentItem NOTIFY currentItemChanged) + + /** + * The main content item of this view: it's the parent of the column items + */ + Q_PROPERTY(QQuickItem *contentItem READ contentItem CONSTANT) + + /** + * The value of the horizontal scroll of the view, in pixels + */ + Q_PROPERTY(qreal contentX READ contentX WRITE setContentX NOTIFY contentXChanged) + + /** + * The compound width of all columns in the view + */ + Q_PROPERTY(qreal contentWidth READ contentWidth NOTIFY contentWidthChanged) + + /** + * The duration for scrolling animations + */ + Q_PROPERTY(int scrollDuration READ scrollDuration WRITE setScrollDuration NOTIFY scrollDurationChanged) + + /** + * True if columns should be visually separed by a separator line + */ + Q_PROPERTY(bool separatorVisible READ separatorVisible WRITE setSeparatorVisible NOTIFY separatorVisibleChanged) + + /** + * The list of all visible column items that are at least partially in the viewport at any given moment + */ + Q_PROPERTY(QList visibleItems READ visibleItems NOTIFY visibleItemsChanged) + + /** + * The first of visibleItems provided from convenience + */ + Q_PROPERTY(QQuickItem *firstVisibleItem READ firstVisibleItem NOTIFY firstVisibleItemChanged) + + /** + * The last of visibleItems provided from convenience + */ + Q_PROPERTY(QQuickItem *lastVisibleItem READ lastVisibleItem NOTIFY lastVisibleItemChanged) + + // Properties to make it similar to Flickable + /** + * True when the user is dragging around with touch gestures the view contents + */ + Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged) + + /** + * True both when the user is dragging around with touch gestures the view contents or the view is animating + */ + Q_PROPERTY(bool moving READ moving NOTIFY movingChanged) + + /** + * True if it supports moving the contents by dragging + */ + Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged) + + // Default properties + /** + * Every column item the view contains + */ + Q_PROPERTY(QQmlListProperty contentChildren READ contentChildren NOTIFY contentChildrenChanged FINAL) + /** + * every item declared inside the view, both visual and non-visual items + */ + Q_PROPERTY(QQmlListProperty contentData READ contentData FINAL) + Q_CLASSINFO("DefaultProperty", "contentData") + + Q_ENUMS(ColumnResizeMode) + +public: + enum ColumnResizeMode { + FixedColumns = 0, + DynamicColumns, + SingleColumn + }; + ColumnView(QQuickItem *parent = nullptr); + ~ColumnView(); + + // QML property accessors + ColumnResizeMode columnResizeMode() const; + void setColumnResizeMode(ColumnResizeMode mode); + + qreal columnWidth() const; + void setColumnWidth(qreal width); + + int currentIndex() const; + void setCurrentIndex(int index); + + int scrollDuration() const; + void setScrollDuration(int duration); + + bool separatorVisible() const; + void setSeparatorVisible(bool visible); + + int count() const; + + QQuickItem *currentItem(); + + //NOTE: It's a QList as QML can't corectly build an Array out of QList + QList visibleItems() const; + QQuickItem *firstVisibleItem() const; + QQuickItem *lastVisibleItem() const; + + + QQuickItem *contentItem() const; + + QQmlListProperty contentChildren(); + QQmlListProperty contentData(); + + bool dragging() const; + bool moving() const; + qreal contentWidth() const; + + qreal contentX() const; + void setContentX(qreal x) const; + + bool interactive() const; + void setInteractive(bool interactive); + + // Api not intended for QML use + //can't do overloads in QML + QQuickItem *removeItem(QQuickItem *item); + QQuickItem *removeItem(int item); + + // QML attached property + static ColumnViewAttached *qmlAttachedProperties(QObject *object); + +public Q_SLOTS: + /** + * Pushes a new item at the end of the view + * @param item the new item which will be reparented and managed + */ + void addItem(QQuickItem *item); + + /** + * Inserts a new item in the view at a given position. + * The current Item will not be changed, currentIndex will be adjusted + * accordingly if needed to keep the same current item. + * @param pos the position we want the new item to be inserted in + * @param item the new item which will be reparented and managed + */ + void insertItem(int pos, QQuickItem *item); + + /** + * Move an item inside the view. + * The currentIndex property may be changed in order to keep currentItem the same. + * @param from the old position + * @param to the new position + */ + void moveItem(int from, int to); + + /** + * Removes an item from the view. + * Items will be reparented to their old parent. + * If they have JavaScript ownership and they didn't have an old parent, they will be destroyed. + * CurrentIndex may be changed in order to keep the same currentItem + * @param item it can either be a pointer of an item or an integer specifying the position to remove + * @returns the item that has just been removed + */ + QQuickItem *removeItem(const QVariant &item); + + /** + * Removes all the items after item. Starting from the last column, every column will be removed until item is found, which will be left in place. + * Items will be reparented to their old parent. + * If they have JavaScript ownership and they didn't have an old parent, they will be destroyed + * @param item the item which will be the new last one of the row. + * @returns the last item that has been removed + */ + QQuickItem *pop(QQuickItem *item); + + /** + * Removes every item in the view. + * Items will be reparented to their old parent. + * If they have JavaScript ownership and they didn't have an old parent, they will be destroyed + */ + void clear(); + + /** + * @returns true if the view contains the given item + */ + bool containsItem(QQuickItem *item); + +protected: + void classBegin() override; + void componentComplete() override; + void updatePolish() override; + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) override; + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + bool childMouseEventFilter(QQuickItem *item, QEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseUngrabEvent() override; + +Q_SIGNALS: + /** + * A new item has been inserted + * @param position where the page has been inserted + * @param item a pointer to the new item + */ + void itemInserted(int position, QQuickItem *item); + + /** + * An item has just been removed from the view + * @param item a pointer to the item that has just been removed + */ + void itemRemoved(QQuickItem *item); + + // Property notifiers + void contentChildrenChanged(); + void columnResizeModeChanged(); + void columnWidthChanged(); + void currentIndexChanged(); + void currentItemChanged(); + void visibleItemsChanged(); + void countChanged(); + void draggingChanged(); + void movingChanged(); + void contentXChanged(); + void contentWidthChanged(); + void interactiveChanged(); + void scrollDurationChanged(); + void separatorVisibleChanged(); + void firstVisibleItemChanged(); + void lastVisibleItemChanged(); + +private: + static void contentChildren_append(QQmlListProperty *prop, QQuickItem *object); + static int contentChildren_count(QQmlListProperty *prop); + static QQuickItem *contentChildren_at(QQmlListProperty *prop, int index); + static void contentChildren_clear(QQmlListProperty *prop); + + static void contentData_append(QQmlListProperty *prop, QObject *object); + static int contentData_count(QQmlListProperty *prop); + static QObject *contentData_at(QQmlListProperty *prop, int index); + static void contentData_clear(QQmlListProperty *prop); + + + QList m_contentData; + + ContentItem *m_contentItem; + QPointer m_currentItem; + + static QHash m_attachedObjects; + qreal m_oldMouseX = -1.0; + qreal m_startMouseX = -1.0; + int m_currentIndex = -1; + + bool m_interactive = true; + bool m_dragging = false; + bool m_moving = false; + bool m_separatorVisible = true; + bool m_complete = false; +}; + +QML_DECLARE_TYPEINFO(ColumnView, QML_HAS_ATTACHED_PROPERTIES) diff --git a/src/columnview_p.h b/src/columnview_p.h new file mode 100644 index 00000000..e9a6cd99 --- /dev/null +++ b/src/columnview_p.h @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program 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 Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include "columnview.h" + +#include +#include + +class QPropertyAnimation; +class QQmlComponent; + +class QmlComponentsPool: public QObject { + Q_OBJECT + +public: + QmlComponentsPool(QObject *parent = nullptr); + ~QmlComponentsPool(); + + void initialize(QQmlEngine *engine); + + QQmlComponent *m_separatorComponent = nullptr; + QObject *m_units = nullptr; + +Q_SIGNALS: + void gridUnitChanged(); + void longDurationChanged(); + +private: + QObject *m_instance = nullptr; +}; + +class ContentItem : public QQuickItem { + Q_OBJECT + +public: + ContentItem(ColumnView *parent = nullptr); + ~ContentItem(); + + void layoutItems(); + qreal childWidth(QQuickItem *child); + void updateVisibleItems(); + void forgetItem(QQuickItem *item); + QQuickItem *ensureSeparator(QQuickItem *item); + + void setBoundedX(qreal x); + void animateX(qreal x); + void snapToItem(); + +protected: + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) override; + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + +private Q_SLOTS: + void syncItemsOrder(); + void updateRepeaterModel(); + +private: + ColumnView *m_view; + QPropertyAnimation *m_slideAnim; + QList m_items; + QList m_visibleItems; + QPointer m_viewAnchorItem; + QHash m_separators; + QHash m_models; + + qreal m_columnWidth = 0; + ColumnView::ColumnResizeMode m_columnResizeMode = ColumnView::FixedColumns; + bool m_shouldAnimate = false; + friend class ColumnView; +}; + diff --git a/src/controls/Page.qml b/src/controls/Page.qml index df31e8bd..25a9d43f 100644 --- a/src/controls/Page.qml +++ b/src/controls/Page.qml @@ -1,404 +1,396 @@ /* * Copyright 2015 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.5 import QtQuick.Layouts 1.2 -import org.kde.kirigami 2.4 as Kirigami +import org.kde.kirigami 2.7 as Kirigami import "private" import QtQuick.Templates 2.0 as T2 /** * Page is a container for all the app pages: everything pushed to the * ApplicationWindow stackView should be a Page instabnce (or a subclass, * such as ScrollablePage) * @see ScrollablePage * @inherit QtQuick.Templates.Page */ T2.Page { id: root /** * leftPadding: int * default contents padding at left */ leftPadding: Kirigami.Units.gridUnit /** * topPadding: int * default contents padding at top */ topPadding: Kirigami.Units.gridUnit /** * rightPadding: int * default contents padding at right */ rightPadding: Kirigami.Units.gridUnit /** * bottomPadding: int * default contents padding at bottom */ bottomPadding: actionButtons.item ? actionButtons.height : Kirigami.Units.gridUnit /** * flickable: Flickable * if the central element of the page is a Flickable * (ListView and Gridview as well) you can set it there. * normally, you wouldn't need to do that, but just use the * ScrollablePage element instead * @see ScrollablePage * Use this if your flickable has some non standard properties, such as not covering the whole Page */ property Flickable flickable /** * actions.contextualActions: list * Defines the contextual actions for the page: * an easy way to assign actions in the right sliding panel * * Example usage: * @code * import org.kde.kirigami 2.4 as Kirigami * * Kirigami.ApplicationWindow { * [...] * contextDrawer: Kirigami.ContextDrawer { * id: contextDrawer * } * [...] * } * @endcode * * @code * import org.kde.kirigami 2.4 as Kirigami * * Kirigami.Page { * [...] * actions.contextualActions: [ * Kirigami.Action { * iconName: "edit" * text: "Action text" * onTriggered: { * // do stuff * } * }, * Kirigami.Action { * iconName: "edit" * text: "Action text" * onTriggered: { * // do stuff * } * } * ] * [...] * } * @endcode */ //TODO: remove property alias contextualActions: actionsGroup.contextualActions /** * actions.main: Action * An optional single action for the action button. * it can be a Kirigami.Action or a QAction * * Example usage: * * @code * import org.kde.kirigami 2.4 as Kirigami * Kirigami.Page { * actions.main: Kirigami.Action { * iconName: "edit" * onTriggered: { * // do stuff * } * } * } * @endcode */ //TODO: remove property alias mainAction: actionsGroup.main /** * actions.left: Action * An optional extra action at the left of the main action button. * it can be a Kirigami.Action or a QAction * * Example usage: * * @code * import org.kde.kirigami 2.4 as Kirigami * Kirigami.Page { * actions.left: Kirigami.Action { * iconName: "edit" * onTriggered: { * // do stuff * } * } * } * @endcode */ //TODO: remove property alias leftAction: actionsGroup.left /** * actions.right: Action * An optional extra action at the right of the main action button. * it can be a Kirigami.Action or a QAction * * Example usage: * * @code * import org.kde.kirigami 2.4 as Kirigami * Kirigami.Page { * actions.right: Kirigami.Action { * iconName: "edit" * onTriggered: { * // do stuff * } * } * } * @endcode */ //TODO: remove property alias rightAction: actionsGroup.right /** * Actions properties are grouped. * * @code * import org.kde.kirigami 2.4 as Kirigami * Kirigami.Page { * actions { * main: Kirigami.Action {...} * left: Kirigami.Action {...} * right: Kirigami.Action {...} * contextualActions: [ * Kirigami.Action {...}, * Kirigami.Action {...} * ] * } * } * @endcode */ readonly property alias actions: actionsGroup /** * isCurrentPage: bool * * Specifies if it's the currently selected page in the window's pages row. * * @since 2.1 */ readonly property bool isCurrentPage: typeof applicationWindow === "undefined" || !globalToolBar.row ? true : (globalToolBar.row.layers.depth > 1 ? globalToolBar.row.layers.currentItem === root : globalToolBar.row.currentItem === root) /** * overlay: Item * an item which stays on top of every other item in the page, * if you want to make sure some elements are completely in a * layer on top of the whole content, parent items to this one. * It's a "local" version of ApplicationWindow's overlay * @since 2.5 */ readonly property alias overlay: overlayItem /** * emitted When the application requests a Back action * For instance a global "back" shortcut or the Android * Back button has been pressed. * The page can manage the back event by itself, * and if it set event.accepted = true, it will stop the main * application to manage the back event. */ signal backRequested(var event); property Component titleDelegate: Kirigami.Heading { id: title level: 1 Layout.fillWidth: true Layout.preferredWidth: titleTextMetrics.width Layout.minimumWidth: titleTextMetrics.width opacity: root.isCurrentPage ? 1 : 0.4 maximumLineCount: 1 elide: Text.ElideRight text: root.title TextMetrics { id: titleTextMetrics text: root.title font: title.font } } // Look for sheets and cose them //FIXME: port Sheets to Popup? onBackRequested: { for(var i in root.resources) { var item = root.resources[i]; if (item.hasOwnProperty("close") && item.hasOwnProperty("sheetOpen") && item.sheetOpen) { item.close() event.accepted = true; return; } } } /** * globalToolBarItem: Item * The item used as global toolbar for the page * present only if we are in a PageRow as a page or as a layer, * and the style is either Titles or ToolBar * @since 2.5 */ readonly property Item globalToolBarItem: globalToolBar.item /** * The style for the automatically generated global toolbar: by default the Page toolbar is the one set globally in the PageRow in its globalToolBar.style property. * A single page can override the application toolbar style for itself. * It is discouraged to use this, except very specific exceptions, like a chat * application which can't have controls on the bottom except the text field. */ property int globalToolBarStyle: globalToolBar.row.globalToolBar.actualStyle //NOTE: contentItem will be created if not existing (and contentChildren of Page would become its children) This with anchors enforces the geometry we want, where globalToolBar is a super-header, on top of header contentItem: Item { anchors { top: root.header ? root.header.bottom : (globalToolBar.visible ? globalToolBar.bottom : parent.top) topMargin: root.topPadding + root.spacing bottom: root.footer ? root.footer.top : parent.bottom bottomMargin: root.bottomPadding + root.spacing } } background: Rectangle { color: Kirigami.Theme.backgroundColor } //FIXME: on material the shadow would bleed over clip: root.header != null; onHeaderChanged: { if (header) { header.anchors.top = Qt.binding(function() {return globalToolBar.visible ? globalToolBar.bottom : root.top}); } } Component.onCompleted: { headerChanged(); parentChanged(root.parent); } onParentChanged: { if (!parent) { return; } globalToolBar.stack = null; globalToolBar.row = null; - if (root.parent.hasOwnProperty("__pageRow")) { - globalToolBar.row = root.parent.__pageRow; + if (root.Kirigami.ColumnView.view) { + globalToolBar.row = root.Kirigami.ColumnView.view.__pageRow; } if (root.T2.StackView.view) { globalToolBar.stack = root.T2.StackView.view; globalToolBar.row = root.T2.StackView.view.parent; } if (globalToolBar.row) { root.globalToolBarStyleChanged.connect(globalToolBar.syncSource); globalToolBar.syncSource(); } } //in data in order for them to not be considered for contentItem, contentChildren, contentData data: [ PageActionPropertyGroup { id: actionsGroup }, Item { id: overlayItem parent: root z: 9997 anchors.fill: parent }, //global top toolbar if we are in a PageRow (in the row or as a layer) Loader { id: globalToolBar z: 9999 height: item ? item.implicitHeight : 0 anchors { left: parent.left right: parent.right top: parent.top } property Kirigami.PageRow row property T2.StackView stack visible: active active: row && (stack != null || root.globalToolBarStyle === Kirigami.ApplicationHeaderStyle.ToolBar || root.globalToolBarStyle == Kirigami.ApplicationHeaderStyle.Titles) function syncSource() { if (row && active) { setSource(Qt.resolvedUrl(root.globalToolBarStyle === Kirigami.ApplicationHeaderStyle.ToolBar ? "private/globaltoolbar/ToolBarPageHeader.qml" : "private/globaltoolbar/TitlesPageHeader.qml"), //TODO: find container reliably, remove assumption {"pageRow": Qt.binding(function() {return row}), "page": root, - "current": Qt.binding(function() {return stack || !root.parent ? true : row.currentIndex === root.parent.level})}); + "current": Qt.binding(function() {return stack || row.currentIndex === root.Kirigami.ColumnView.level})}); } } - - Separator { - z: 999 - anchors.verticalCenter: globalToolBar.verticalCenter - height: globalToolBar.height * 0.6 - visible: !root.T2.StackView.view && globalToolBar.row && root.parent && globalToolBar.row.contentItem.contentX < root.parent.x - globalToolBar.row.globalToolBar.leftReservedSpace - Kirigami.Theme.textColor: globalToolBar.item ? globalToolBar.item.Kirigami.Theme.textColor : undefined - } }, //bottom action buttons Loader { id: actionButtons z: 9999 parent: root anchors { left: parent.left right: parent.right bottom: parent.bottom } //It should be T2.Page, Qt 5.7 doesn't like it property Item page: root height: item ? item.height : 0 active: typeof applicationWindow !== "undefined" && (!globalToolBar.row || root.globalToolBarStyle !== Kirigami.ApplicationHeaderStyle.ToolBar) && //Legacy (typeof applicationWindow === "undefined" || (!applicationWindow().header || applicationWindow().header.toString().indexOf("ToolBarApplicationHeader") === -1) && (!applicationWindow().footer || applicationWindow().footer.toString().indexOf("ToolBarApplicationHeader") === -1)) source: Qt.resolvedUrl("./private/ActionButton.qml") } ] Layout.fillWidth: true } diff --git a/src/controls/PageRow.qml b/src/controls/PageRow.qml index 57f90aaa..4d3a92de 100644 --- a/src/controls/PageRow.qml +++ b/src/controls/PageRow.qml @@ -1,857 +1,613 @@ /* * Copyright 2016 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.5 import QtQuick.Layouts 1.2 import QtQml.Models 2.2 import QtQuick.Templates 2.0 as T import QtQuick.Controls 2.0 as QQC2 -import org.kde.kirigami 2.4 +import org.kde.kirigami 2.7 import "private/globaltoolbar" as GlobalToolBar import "templates" as KT /** * PageRow implements a row-based navigation model, which can be used * with a set of interlinked information pages. Items are pushed in the * back of the row and the view scrolls until that row is visualized. * A PageRowcan show a single page or a multiple set of columns, depending * on the window width: on a phone a single column should be fullscreen, * while on a tablet or a desktop more than one column should be visible. * @inherit QtQuick.Templates.Control */ T.Control { id: root //BEGIN PROPERTIES /** * This property holds the number of items currently pushed onto the view */ - readonly property int depth: popScrollAnim.running && popScrollAnim.pendingDepth > -1 ? popScrollAnim.pendingDepth : pagesLogic.count + property alias depth: columnView.count /** * The last Page in the Row */ - readonly property Item lastItem: pagesLogic.count ? pagesLogic.get(pagesLogic.count - 1).page : null + readonly property Item lastItem: columnView.contentChildren.length > 0 ? columnView.contentChildren[columnView.contentChildren.length - 1] : null /** * The currently visible Item */ - readonly property Item currentItem: mainView.currentItem ? mainView.currentItem.page : null + property alias currentItem: columnView.currentItem /** * the index of the currently visible Item */ - property alias currentIndex: mainView.currentIndex + property alias currentIndex: columnView.currentIndex /** * The initial item when this PageRow is created */ property variant initialPage /** - * The main flickable of this Row + * The main ColumnView of this Row */ - contentItem: mainView + contentItem: columnView /** * items: list * All the items that are present in the PageRow * @since 2.6 */ - readonly property var items: pagesLogic.pages; + property alias items: columnView.contentChildren; /** * visibleItems: list * All pages which are visible in the PageRow, excluding those which are scrolled away * @since 2.6 */ - property var visibleItems: [] + property alias visibleItems: columnView.visibleItems /** * firstVisibleItem: Item * The first at least partially visible page in the PageRow, pages before that one will be out of the viewport * @since 2.6 */ - readonly property Item firstVisibleItem: visibleItems.length > 0 ? visibleItems[0] : null + property alias firstVisibleItem: columnView.firstVisibleItem /** * lastVisibleItem: Item * The last at least partially visible page in the PageRow, pages after that one will be out of the viewport * @since 2.6 */ - readonly property Item lastVisibleItem: visibleItems.length > 0 ? visibleItems[visibleItems.length - 1] : null + property alias lastVisibleItem: columnView.lastVisibleItem /** * The default width for a column * default is wide enough for 30 grid units. * Pages can override it with their Layout.fillWidth, * implicitWidth Layout.minimumWidth etc. */ property int defaultColumnWidth: Units.gridUnit * 20 /** * interactive: bool * If true it will be possible to go back/forward by dragging the * content themselves with a gesture. * Otherwise the only way to go back will be programmatically * default: true */ - property alias interactive: mainView.interactive + property alias interactive: columnView.interactive /** * wideMode: bool * If true, the PageRow is wide enough that willshow more than one column at once * @since 5.37 */ - readonly property bool wideMode: root.width >= root.defaultColumnWidth*2 && pagesLogic.count >= 2 + readonly property bool wideMode: root.width >= root.defaultColumnWidth*2 && depth >= 2 /** * separatorVisible: bool * True if the separator between pages should be visible * default: true * @since 5.38 */ - property bool separatorVisible: true + property alias separatorVisible: columnView.separatorVisible /** * globalToolBar: grouped property * Controls the appearance of an optional global toolbar for the whole PageRow. * It's a grouped property comprised of the following properties: * * style: (Kirigami.ApplicationHeaderStyle) can have the following values: * ** Auto: depending on application formfactor, it can behave automatically like other values, such as a Breadcrumb on mobile and ToolBar on desktop * ** Breadcrumb: it will show a breadcrumb of all the page titles in the stack, for easy navigation * ** Titles: each page will only have its own tile on top * ** TabBar: the global toolbar will look like a TabBar to select the pages * ** ToolBar: each page will have the title on top together buttons and menus to represent all of the page actions: not available on Mobile systems. * ** None: no global toolbar will be shown * * * actualStyle: this will represent the actual style of the toolbar: it can be different from style in the case style is Auto * * showNavigationButtons: if true, forward and backward navigation buttons will be shown on the left of the toolbar * * minimumHeight: (int) minimum height of the header, which will be resized when scrolling, only in Mobile mode (default: preferredHeight, sliding but no scaling) property int preferredHeight: (int) the height the toolbar will usually have property int maximumHeight: (int) The height the toolbar will have in mobile mode when the app is in reachable mode (default: preferredHeight * 1.5) * * leftReservedSpace: (int, readonly) how many pixels are reserved at the left of the page toolBar (for navigation buttons or drawer handle) property int rightReservedSpace: (int, readonly) how many pixels are reserved at the right of the page toolbar (drawer handle) * @since 5.48 */ readonly property alias globalToolBar: globalToolBar //END PROPERTIES //BEGIN FUNCTIONS /** * Pushes a page on the stack. * The page can be defined as a component, item or string. * If an item is used then the page will get re-parented. * If a string is used then it is interpreted as a url that is used to load a page * component. + * The last pushed page will become the current item. * * @param page The page can also be given as an array of pages. * In this case all those pages will * be pushed onto the stack. The items in the stack can be components, items or * strings just like for single pages. * Additionally an object can be used, which specifies a page and an optional * properties property. * This can be used to push multiple pages while still giving each of * them properties. * When an array is used the transition animation will only be to the last page. * * @param properties The properties argument is optional and allows defining a - * map of properties to set on the page. - * @return The new created page + * map of properties to set on the page. If page is actually an array of pages, properties should also be an array of key/value maps + * @return The new created page (or the last one if it was an array) */ function push(page, properties) { + var item = insertPage(depth, page, properties); + currentIndex = depth - 1; + return item; + } + + /** + * Inserts a new page or a list of new at an arbitrary position + * The page can be defined as a component, item or string. + * If an item is used then the page will get re-parented. + * If a string is used then it is interpreted as a url that is used to load a page + * component. + * The current Page will not be changed, currentIndex will be adjusted + * accordingly if needed to keep the same current page. + * + * @param page The page can also be given as an array of pages. + * In this case all those pages will + * be pushed onto the stack. The items in the stack can be components, items or + * strings just like for single pages. + * Additionally an object can be used, which specifies a page and an optional + * properties property. + * This can be used to push multiple pages while still giving each of + * them properties. + * When an array is used the transition animation will only be to the last page. + * + * @param properties The properties argument is optional and allows defining a + * map of properties to set on the page. If page is actually an array of pages, properties should also be an array of key/value maps + * @return The new created page (or the last one if it was an array) + * @since 2.7 + */ + function insertPage(position, page, properties) { //don't push again things already there - if (page.createObject === undefined && typeof page != "string" && pagesLogic.containsPage(page)) { + if (page.createObject === undefined && typeof page != "string" && columnView.containsItem(page)) { print("The item " + page + " is already in the PageRow"); - return; + return null; } - if (popScrollAnim.running) { - popScrollAnim.running = false; - popScrollAnim.popPageCleanup(popScrollAnim.pendingPage); - } + position = Math.max(0, Math.min(depth, position)); - popScrollAnim.popPageCleanup(currentItem); + columnView.pop(columnView.currentItem); // figure out if more than one page is being pushed var pages; + var propsArray = []; if (page instanceof Array) { pages = page; page = pages.pop(); + //compatibility with pre-qqc1 api, can probably be removed if (page.createObject === undefined && page.parent === undefined && typeof page != "string") { properties = properties || page.properties; page = page.page; } } + if (properties instanceof Array) { + propsArray = properties; + properties = propsArray.pop(); + } else { + propsArray = [properties]; + } // push any extra defined pages onto the stack if (pages) { var i; for (i = 0; i < pages.length; i++) { var tPage = pages[i]; - var tProps; + var tProps = propsArray[i]; + //compatibility with pre-qqc1 api, can probably be removed if (tPage.createObject === undefined && tPage.parent === undefined && typeof tPage != "string") { - if (pagesLogic.containsPage(tPage)) { + if (columnView.containsItem(tPage)) { print("The item " + page + " is already in the PageRow"); continue; } tProps = tPage.properties; tPage = tPage.page; } - var container = pagesLogic.initPage(tPage, tProps); - pagesLogic.append(container); - pagesLogic.pages.push(tPage); - root.itemsChanged(); + var pageItem = pagesLogic.initAndInsertPage(position, tPage, tProps); + ++position; } } // initialize the page - var container = pagesLogic.initPage(page, properties); - pagesLogic.append(container); - pagesLogic.pages.push(page); - container.visible = container.page.visible = true; - - mainView.currentIndex = container.level; - pagePushed(container.page); - root.itemsChanged(); - return container.page + var pageItem = pagesLogic.initAndInsertPage(position, page, properties); + + pagePushed(pageItem); + + return pageItem; + } + + /** + * Move the page at position fromPos to the new position toPos + * If needed, currentIndex will be adjusted + * in order to keep the same current page. + * @since 2.7 + */ + function movePage(fromPos, toPos) { + columnView.moveItem(fromPos, toPos); + } + + /** + * Remove the given page + * @param page The page can be given both as integer position or by reference + * @return The page that has just been removed + * @since 2.7 + */ + function removePage(page) { + if (depth == 0) { + return null; + } + + return columnView.removeItem(page); } /** * Pops a page off the stack. * @param page If page is specified then the stack is unwound to that page, * to unwind to the first page specify * page as null. * @return The page instance that was popped off the stack. */ function pop(page) { if (depth == 0) { - return; + return null; } - //if a pop was animating, stop it - if (popScrollAnim.running) { - popScrollAnim.running = false; - popScrollAnim.popPageCleanup(popScrollAnim.pendingPage); - //if a push was animating, stop it - } else { - mainView.positionViewAtIndex(mainView.currentIndex, ListView.Beginning); - } - - popScrollAnim.from = mainView.contentX - - if ((!page || !page.parent) && pagesLogic.count > 1) { - page = pagesLogic.get(pagesLogic.count - 2).page; - } - popScrollAnim.to = page && page.parent ? page.parent.x : 0; - popScrollAnim.pendingPage = page; - popScrollAnim.pendingDepth = page && page.parent ? page.parent.level + 1 : 0; - - popScrollAnim.running = true; + return columnView.pop(page); } /** - * Emitted when a page has been pushed + * Emitted when a page has been inserted anywhere + * @param position where the page has been inserted + * @param page the new page + * @since 2.7 + */ + signal pageInserted(int position, Item page) + + /** + * Emitted when a page has been pushed to the bottom * @param page the new page * @since 2.5 */ signal pagePushed(Item page) /** * Emitted when a page has been removed from the row. * @param page the page that has been removed: at this point it's still valid, * but may be auto deleted soon. * @since 2.5 */ signal pageRemoved(Item page) - SequentialAnimation { - id: popScrollAnim - property real from - property real to - property var pendingPage - property int pendingDepth: -1 - function popPageCleanup(page) { - if (pagesLogic.count == 0) { - return; - } - if (popScrollAnim.running) { - popScrollAnim.running = false; - } - - var oldPage = pagesLogic.get(pagesLogic.count-1).page; - if (page !== undefined) { - // an unwind target has been specified - pop until we find it - while (page !== oldPage && pagesLogic.count > 1) { - pagesLogic.removePage(oldPage.parent.level); - - oldPage = pagesLogic.get(pagesLogic.count-1).page; - } - } else { - pagesLogic.removePage(pagesLogic.count-1); - } - } - NumberAnimation { - target: mainView - properties: "contentX" - duration: Units.shortDuration - from: popScrollAnim.from - to: popScrollAnim.to - } - ScriptAction { - script: { - //snap - mainView.flick(100, 0) - popScrollAnim.popPageCleanup(popScrollAnim.pendingPage); - } - } - } /** * Replaces a page on the stack. * @param page The page can also be given as an array of pages. * In this case all those pages will * be pushed onto the stack. The items in the stack can be components, items or * strings just like for single pages. + * the current page and all pagest after it in the stack will be removed. * Additionally an object can be used, which specifies a page and an optional * properties property. * This can be used to push multiple pages while still giving each of * them properties. * When an array is used the transition animation will only be to the last page. * @param properties The properties argument is optional and allows defining a * map of properties to set on the page. * @see push() for details. */ function replace(page, properties) { - if (currentIndex>=1) - popScrollAnim.popPageCleanup(pagesLogic.get(currentIndex-1).page); - else if (currentIndex==0) - popScrollAnim.popPageCleanup(); - else + if (currentIndex >= 1) { + pop(columnView.contentChildren[currentIndex-1]); + } else if (currentIndex == 0) { + pop(); + } else { console.warn("There's no page to replace"); + } return push(page, properties); } /** * Clears the page stack. * Destroy (or reparent) all the pages contained. */ function clear() { - return pagesLogic.clearPages(); + return columnView.clear(); } /** * @return the page at idx * @param idx the depth of the page we want */ function get(idx) { - return pagesLogic.get(idx).page; + return columnView.contentChildren[idx]; } /** * go back to the previous index and scroll to the left to show one more column */ function flickBack() { if (depth > 1) { currentIndex = Math.max(0, currentIndex - 1); } - - if (LayoutMirroring.enabled) { - if (!mainView.atEnd) { - mainViewScrollAnim.from = mainView.contentX - mainViewScrollAnim.to = Math.min(mainView.contentWidth - mainView.width, mainView.contentX + defaultColumnWidth) - mainViewScrollAnim.running = true; - } - } else { - if (mainView.contentX - mainView.originX > 0) { - mainViewScrollAnim.from = mainView.contentX - mainViewScrollAnim.to = Math.max(mainView.originX, mainView.contentX - defaultColumnWidth) - mainViewScrollAnim.running = true; - } - } } /** * layers: QtQuick.Controls.PageStack * Access to the modal layers. * Sometimes an application needs a modal page that always covers all the rows. * For instance the full screen image of an image viewer or a settings page. * @since 5.38 */ property alias layers: layersStack //END FUNCTIONS onInitialPageChanged: { if (initialPage) { clear(); push(initialPage, null) } } Keys.forwardTo: [currentItem] - SequentialAnimation { - id: mainViewScrollAnim - property real from - property real to - NumberAnimation { - target: mainView - properties: "contentX" - duration: Units.longDuration - from: mainViewScrollAnim.from - to: mainViewScrollAnim.to - } - ScriptAction { - script: { - mainView.flick(100, 0); - } - } - } - GlobalToolBar.PageRowGlobalToolBarStyleGroup { id: globalToolBar readonly property int leftReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.leftReservedSpace : 0 readonly property int rightReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.rightReservedSpace : 0 readonly property int height: globalToolBarUI.height readonly property Item leftHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.leftHandleAnchor : null readonly property Item rightHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.rightHandleAnchor : null } QQC2.StackView { id: layersStack z: 99 visible: depth > 1 || busy anchors { fill: parent } //placeholder as initial item initialItem: Item {} function clear () { //don't let it kill the main page row var d = root.depth; for (var i = 1; i < d; ++i) { pop(); } } popEnter: Transition { OpacityAnimator { from: 0 to: 1 duration: Units.longDuration easing.type: Easing.InOutCubic } } popExit: Transition { ParallelAnimation { OpacityAnimator { from: 1 to: 0 duration: Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: 0 to: height/2 duration: Units.longDuration easing.type: Easing.InCubic } } } pushEnter: Transition { ParallelAnimation { //NOTE: It's a PropertyAnimation instead of an Animator because with an animator the item will be visible for an instant before starting to fade PropertyAnimation { property: "opacity" from: 0 to: 1 duration: Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: height/2 to: 0 duration: Units.longDuration easing.type: Easing.OutCubic } } } pushExit: Transition { OpacityAnimator { from: 1 to: 0 duration: Units.longDuration easing.type: Easing.InOutCubic } } replaceEnter: Transition { ParallelAnimation { OpacityAnimator { from: 0 to: 1 duration: Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: height/2 to: 0 duration: Units.longDuration easing.type: Easing.OutCubic } } } replaceExit: Transition { ParallelAnimation { OpacityAnimator { from: 1 to: 0 duration: Units.longDuration easing.type: Easing.InCubic } YAnimator { from: 0 to: -height/2 duration: Units.longDuration easing.type: Easing.InOutCubic } } } } Loader { id: globalToolBarUI anchors { left: parent.left top: parent.top right: parent.right } z: 100 active: globalToolBar.actualStyle != ApplicationHeaderStyle.None visible: active height: active ? implicitHeight : 0 source: Qt.resolvedUrl("private/globaltoolbar/PageRowGlobalToolBarUI.qml"); } - ListView { - id: mainView - boundsBehavior: Flickable.StopAtBounds - orientation: Qt.Horizontal - snapMode: ListView.SnapToItem - currentIndex: 0 - property int marginForLast: count > 1 ? pagesLogic.get(count-1).page.width - pagesLogic.get(count-1).width : 0 - leftMargin: LayoutMirroring.enabled ? marginForLast : 0 - rightMargin: LayoutMirroring.enabled ? 0 : marginForLast - preferredHighlightBegin: 0 - preferredHighlightEnd: 0 - highlightMoveDuration: Units.longDuration - highlightFollowsCurrentItem: true - onWidthChanged: updatevisibleItems() - - onContentXChanged: updatevisibleItems() - - function updatevisibleItems() { - var visibleItems = []; - var cont; - var signalChange = false; - for (var i = 0; i < pagesLogic.count; ++i) { - cont = pagesLogic.get(i); - if (cont.x - contentX < width && cont.x + cont.width - contentX > 0) { - visibleItems.push(cont.page); - if (root.visibleItems.indexOf(cont.page) === -1) { - signalChange = true; + QtObject { + id: pagesLogic + readonly property var componentCache: new Array() + + function initAndInsertPage(position, page, properties) { + var pageComp; + + if (page.createObject) { + // page defined as component + pageComp = page; + } else if (typeof page == "string") { + // page defined as string (a url) + pageComp = pagesLogic.componentCache[page]; + if (!pageComp) { + pageComp = pagesLogic.componentCache[page] = Qt.createComponent(page); + } + } + if (pageComp) { + // instantiate page from component + // FIXME: parent directly to columnView or root? + page = pageComp.createObject(null, properties || {}); + columnView.insertItem(position, page); + + if (pageComp.status === Component.Error) { + throw new Error("Error while loading page: " + pageComp.errorString()); + } + } else { + // copy properties to the page + for (var prop in properties) { + if (properties.hasOwnProperty(prop)) { + page[prop] = properties[prop]; } } + columnView.insertItem(position, page); } - signalChange = signalChange || (visibleItems.length != root.visibleItems.length) - - if (signalChange) { - root.visibleItems = visibleItems; - root.visibleItemsChanged(); - } + return page; } - onMovementEnded: currentIndex = Math.max(0, indexAt(contentX, 0)) + } - onFlickEnded: onMovementEnded(); - onCurrentIndexChanged: { - if (currentItem) { - currentItem.page.forceActiveFocus(); - } - } + ColumnView { + id: columnView + anchors.fill: parent + readonly property Item __pageRow: root + columnResizeMode: root.wideMode ? ColumnView.FixedColumns : ColumnView.SingleColumn + columnWidth: root.defaultColumnWidth opacity: layersStack.depth < 2 + + onItemInserted: root.pageInserted(position, item); + onItemRemoved: root.pageRemoved(item); + Behavior on opacity { OpacityAnimator { duration: Units.longDuration easing.type: Easing.InOutQuad } } - - - model: ObjectModel { - id: pagesLogic - readonly property var componentCache: new Array() - readonly property int roundedDefaultColumnWidth: root.width < root.defaultColumnWidth*2 ? root.width : root.defaultColumnWidth - property var pages: [] - - function removePage(id) { - if (id < 0 || id >= count) { - print("Tried to remove an invalid page index:" + id); - return; - } - - var item = pagesLogic.get(id); - if (item.owner) { - item.page.visible = false; - item.page.parent = item.owner; - } - //FIXME: why reparent ing is necessary? - //is destroy just an async deleteLater() that isn't executed immediately or it actually leaks? - pagesLogic.remove(id); - item.parent = root; - root.pageRemoved(item.page); - if (item.page.parent===item) { - item.page.destroy(1) - } - item.destroy(); - pages.splice(id, 1); - root.itemsChanged(); - } - function clearPages () { - popScrollAnim.running = false; - popScrollAnim.pendingDepth = -1; - while (count > 0) { - removePage(count-1); - } - pages = []; - root.itemsChanged(); - } - function initPage(page, properties) { - var container = containerComponent.createObject(mainView, { - "level": pagesLogic.count, - "page": page - }); - - var pageComp; - if (page.createObject) { - // page defined as component - pageComp = page; - } else if (typeof page == "string") { - // page defined as string (a url) - pageComp = pagesLogic.componentCache[page]; - if (!pageComp) { - pageComp = pagesLogic.componentCache[page] = Qt.createComponent(page); - } - } - if (pageComp) { - // instantiate page from component - page = pageComp.createObject(container.pageParent, properties || {}); - - if (pageComp.status === Component.Error) { - throw new Error("Error while loading page: " + pageComp.errorString()); - } - } else { - // copy properties to the page - for (var prop in properties) { - if (properties.hasOwnProperty(prop)) { - page[prop] = properties[prop]; - } - } - } - - container.page = page; - if (page.parent === null || page.parent === container.pageParent) { - container.owner = null; - } - - // the page has to be reparented - if (page.parent !== container) { - page.parent = container; - } + } - return container; - } - function containsPage(page) { - for (var i = 0; i < pagesLogic.count; ++i) { - var candidate = pagesLogic.get(i); - if (candidate.page === page) { - print("The item " + page + " is already in the PageRow"); - return; - } - } - } + Rectangle { + anchors.bottom: columnView.bottom + height: Units.smallSpacing + x: (columnView.width - width) * (columnView.contentX / (columnView.contentWidth - columnView.width)) + width: columnView.width * (columnView.width/columnView.contentWidth) + color: Theme.textColor + opacity: 0 + onXChanged: { + opacity = 0.3 + scrollIndicatorTimer.restart(); } - T.ScrollIndicator.horizontal: T.ScrollIndicator { - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - height: Units.smallSpacing - contentItem: Rectangle { - height: Units.smallSpacing - width: Units.smallSpacing - color: Theme.textColor - opacity: 0 - onXChanged: { - opacity = 0.3 - scrollIndicatorTimer.restart(); - } - Behavior on opacity { - OpacityAnimator { - duration: Units.longDuration - easing.type: Easing.InOutQuad - } - } - Timer { - id: scrollIndicatorTimer - interval: Units.longDuration * 4 - onTriggered: parent.opacity = 0; - } + Behavior on opacity { + OpacityAnimator { + duration: Units.longDuration + easing.type: Easing.InOutQuad } } - - onContentWidthChanged: mainView.positionViewAtIndex(root.currentIndex, ListView.Contain) - } - - Component { - id: containerComponent - - MouseArea { - id: container - height: mainView.height - width: root.width - state: page - ? (page.visible ? (!root.wideMode ? "vertical" : (container.level >= pagesLogic.count - 1 ? "last" : "middle")) : "hidden") - : ""; - acceptedButtons: Qt.LeftButton | Qt.BackButton | Qt.ForwardButton - - property int level - - readonly property int hint: page && page.implicitWidth ? page.implicitWidth : root.defaultColumnWidth - readonly property int roundedHint: Math.floor(root.width/hint) > 0 ? root.width/Math.floor(root.width/hint) : root.width - property T.Control __pageRow: root - - property Item footer - - property Item page - onPageChanged: { - if (page) { - owner = page.parent; - page.parent = container; - page.anchors.left = container.left; - page.anchors.top = container.top; - page.anchors.right = container.right; - page.anchors.bottom = container.bottom; - page.anchors.topMargin = Qt.binding(function() { - if (!wideMode && (page.globalToolBarStyle == ApplicationHeaderStyle.ToolBar || page.globalToolBarStyle == ApplicationHeaderStyle.Titles)) { - return 0; - } - return globalToolBar.actualStyle == ApplicationHeaderStyle.TabBar || globalToolBar.actualStyle == ApplicationHeaderStyle.Breadcrumb ? globalToolBarUI.height : 0; - }); - } else { - pagesLogic.remove(level); - } - } - property Item owner - drag.filterChildren: true - onPressed: { - switch (mouse.button) { - case Qt.BackButton: - root.flickBack(); - break; - case Qt.ForwardButton: - root.currentIndex = Math.min(root.depth, root.currentIndex + 1); - break; - default: - root.currentIndex = level; - break; - } - mouse.accepted = false; - } - onFocusChanged: { - if (focus) { - root.currentIndex = level; - } - } - - //TODO: move in Page itself? - Separator { - z: 999 - anchors { - top: page ? page.top : parent.top - bottom: parent.bottom - left: parent.left - //ensure a sharp angle - topMargin: -width + (globalToolBar.actualStyle == ApplicationHeaderStyle.ToolBar || globalToolBar.actualStyle == ApplicationHeaderStyle.Titles ? globalToolBarUI.height : 0) - } - visible: root.separatorVisible && mainView.contentX < container.x - } - states: [ - State { - name: "vertical" - PropertyChanges { - target: container - width: root.width - } - PropertyChanges { - target: container.page ? container.page.anchors : null - rightMargin: 0 - } - }, - State { - name: "last" - PropertyChanges { - target: container - width: pagesLogic.roundedDefaultColumnWidth - } - PropertyChanges { - target: container.page.anchors - rightMargin: { - return -(root.width - pagesLogic.roundedDefaultColumnWidth*2); - } - } - }, - State { - name: "middle" - PropertyChanges { - target: container - width: pagesLogic.roundedDefaultColumnWidth - } - PropertyChanges { - target: container.page.anchors - rightMargin: 0 - } - }, - State { - name: "hidden" - PropertyChanges { - target: container - width: 0 - } - } - ] + Timer { + id: scrollIndicatorTimer + interval: Units.longDuration * 4 + onTriggered: parent.opacity = 0; } } } diff --git a/src/controls/private/ActionButton.qml b/src/controls/private/ActionButton.qml index d3f335db..6cf0ddea 100644 --- a/src/controls/private/ActionButton.qml +++ b/src/controls/private/ActionButton.qml @@ -1,502 +1,502 @@ /* * Copyright 2015 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.1 import QtQuick.Layouts 1.2 import QtQuick.Controls 2.0 as Controls import QtGraphicalEffects 1.0 -import org.kde.kirigami 2.4 +import org.kde.kirigami 2.7 import "../templates/private" Item { id: root Theme.colorSet: Theme.Button Theme.inherit: false anchors { left: parent.left right: parent.right bottom: parent.bottom bottomMargin: root.page.footer ? root.page.footer.height : 0 } //smallSpacing for the shadow height: button.height + Units.smallSpacing clip: true readonly property Page page: root.parent.page //either Action or QAction should work here function isActionAvailable(action) { return action && (action.visible === undefined || action.visible); } readonly property QtObject action: root.page && isActionAvailable(root.page.mainAction) ? root.page.mainAction : null readonly property QtObject leftAction: root.page && isActionAvailable(root.page.leftAction) ? root.page.leftAction : null readonly property QtObject rightAction: root.page && isActionAvailable(root.page.rightAction) ? root.page.rightAction : null readonly property bool hasApplicationWindow: typeof applicationWindow !== "undefined" && applicationWindow readonly property bool hasGlobalDrawer: typeof globalDrawer !== "undefined" && globalDrawer readonly property bool hasContextDrawer: typeof contextDrawer !== "undefined" && contextDrawer transform: Translate { id: translateTransform y: mouseArea.internalVisibility ? 0 : button.height Behavior on y { NumberAnimation { duration: Units.longDuration easing.type: mouseArea.internalVisibility == true ? Easing.InQuad : Easing.OutQuad } } } onWidthChanged: button.x = root.width/2 - button.width/2 Item { id: button x: root.width/2 - button.width/2 anchors.bottom: edgeMouseArea.bottom implicitWidth: implicitHeight + Units.iconSizes.smallMedium*2 + Units.gridUnit implicitHeight: Units.iconSizes.medium + Units.largeSpacing * 2 onXChanged: { if (mouseArea.pressed || edgeMouseArea.pressed || fakeContextMenuButton.pressed) { if (root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal) { globalDrawer.peeking = true; globalDrawer.visible = true; globalDrawer.position = Math.min(1, Math.max(0, (x - root.width/2 + button.width/2)/globalDrawer.contentItem.width + mouseArea.drawerShowAdjust)); } if (root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal) { contextDrawer.peeking = true; contextDrawer.visible = true; contextDrawer.position = Math.min(1, Math.max(0, (root.width/2 - button.width/2 - x)/contextDrawer.contentItem.width + mouseArea.drawerShowAdjust)); } } } MouseArea { id: mouseArea anchors.fill: parent visible: action != null || leftAction != null || rightAction != null property bool internalVisibility: (!root.hasApplicationWindow || (applicationWindow().controlsVisible && applicationWindow().height > root.height*2)) && (root.action === null || root.action.visible === undefined || root.action.visible) preventStealing: true drag { target: button //filterChildren: true axis: Drag.XAxis minimumX: root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal ? 0 : root.width/2 - button.width/2 maximumX: root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal ? root.width : root.width/2 - button.width/2 } property var downTimestamp; property int startX property int startMouseY property real drawerShowAdjust readonly property int currentThird: (3*mouseX)/width readonly property QtObject actionUnderMouse: { switch(currentThird) { case 0: return leftAction; case 1: return action; case 2: return rightAction; default: return null } } hoverEnabled: true Controls.ToolTip.visible: containsMouse && !Settings.tabletMode && actionUnderMouse Controls.ToolTip.text: actionUnderMouse ? actionUnderMouse.text : "" Controls.ToolTip.delay: Units.toolTipDelay onPressed: { //search if we have a page to set to current - if (root.hasApplicationWindow && applicationWindow().pageStack.currentIndex !== undefined && root.page.parent.level !== undefined) { + if (root.hasApplicationWindow && applicationWindow().pageStack.currentIndex !== undefined && root.page.ColumnView.level !== undefined) { //search the button parent's parent, that is the page parent //this will make the context drawer open for the proper page - applicationWindow().pageStack.currentIndex = root.page.parent.level; + applicationWindow().pageStack.currentIndex = root.page.ColumnView.level; } downTimestamp = (new Date()).getTime(); startX = button.x + button.width/2; startMouseY = mouse.y; drawerShowAdjust = 0; } onReleased: { if (root.hasGlobalDrawer) globalDrawer.peeking = false; if (root.hasContextDrawer) contextDrawer.peeking = false; //pixel/second var x = button.x + button.width/2; var speed = ((x - startX) / ((new Date()).getTime() - downTimestamp) * 1000); drawerShowAdjust = 0; //project where it would be a full second in the future if (root.hasContextDrawer && root.hasGlobalDrawer && globalDrawer.modal && x + speed > Math.min(root.width/4*3, root.width/2 + globalDrawer.contentItem.width/2)) { globalDrawer.open(); contextDrawer.close(); } else if (root.hasContextDrawer && x + speed < Math.max(root.width/4, root.width/2 - contextDrawer.contentItem.width/2)) { if (root.hasContextDrawer && contextDrawer.modal) { contextDrawer.open(); } if (root.hasGlobalDrawer && globalDrawer.modal) { globalDrawer.close(); } } else { if (root.hasGlobalDrawer && globalDrawer.modal) { globalDrawer.close(); } if (root.hasContextDrawer && contextDrawer.modal) { contextDrawer.close(); } } //Don't rely on native onClicked, but fake it here: //Qt.startDragDistance is not adapted to devices dpi in case //of Android, so consider the button "clicked" when: //*the button has been dragged less than a gridunit //*the finger is still on the button if (Math.abs((button.x + button.width/2) - startX) < Units.gridUnit && mouse.y > 0) { if (!actionUnderMouse) { return; } //if an action has been assigned, trigger it if (actionUnderMouse && actionUnderMouse.trigger) { actionUnderMouse.trigger(); } if (actionUnderMouse.children.length > 0) { var subMenuUnderMouse; switch (actionUnderMouse) { case leftAction: subMenuUnderMouse = leftActionSubMenu; break; case mainAction: subMenuUnderMouse = mainActionSubMenu; break case rightAction: subMenuUnderMouse = rightActionSubMenu; break; } if (subMenuUnderMouse && !subMenuUnderMouse.visible) { subMenuUnderMouse.visible = true; } } } } onPositionChanged: { drawerShowAdjust = Math.min(0.3, Math.max(0, (startMouseY - mouse.y)/(Units.gridUnit*15))); button.xChanged(); } onPressAndHold: { if (!actionUnderMouse) { return; } //if an action has been assigned, show a message like a tooltip if (actionUnderMouse && actionUnderMouse.text && Settings.tabletMode) { Controls.ToolTip.show(actionUnderMouse.text, 3000) } } Connections { target: root.hasGlobalDrawer ? globalDrawer : null onPositionChanged: { if ( globalDrawer && globalDrawer.modal && !mouseArea.pressed && !edgeMouseArea.pressed && !fakeContextMenuButton.pressed) { button.x = globalDrawer.contentItem.width * globalDrawer.position + root.width/2 - button.width/2; } } } Connections { target: root.hasContextDrawer ? contextDrawer : null onPositionChanged: { if (contextDrawer && contextDrawer.modal && !mouseArea.pressed && !edgeMouseArea.pressed && !fakeContextMenuButton.pressed) { button.x = root.width/2 - button.width/2 - contextDrawer.contentItem.width * contextDrawer.position; } } } Item { id: background anchors { fill: parent } Rectangle { id: buttonGraphics radius: width/2 anchors.centerIn: parent height: parent.height - Units.smallSpacing*2 width: height enabled: root.action && root.action.enabled visible: root.action readonly property bool pressed: root.action && root.action.enabled && ((root.action == mouseArea.actionUnderMouse && mouseArea.pressed) || root.action.checked) property color baseColor: root.action && root.action.icon && root.action.icon.color && root.action.icon.color != undefined && root.action.icon.color.a > 0 ? root.action.icon.color : Theme.highlightColor color: pressed ? Qt.darker(baseColor, 1.3) : baseColor ActionsMenu { id: mainActionSubMenu y: -height x: -width/2 + parent.width/2 actions: root.action ? root.action.children : "" submenuComponent: Component { ActionsMenu {} } } Icon { id: icon anchors.centerIn: parent width: Units.iconSizes.smallMedium height: width source: root.action && root.action.iconName ? root.action.iconName : "" selected: true color: root.action && root.action.color && root.action.color.a > 0 ? root.action.color : (selected ? Theme.highlightedTextColor : Theme.textColor) } Behavior on color { ColorAnimation { duration: Units.longDuration easing.type: Easing.InOutQuad } } Behavior on x { NumberAnimation { duration: Units.longDuration easing.type: Easing.InOutQuad } } } //left button Rectangle { id: leftButtonGraphics z: -1 anchors { left: parent.left bottom: parent.bottom bottomMargin: Units.smallSpacing } enabled: root.leftAction && root.leftAction.enabled radius: Units.devicePixelRatio*2 height: Units.iconSizes.smallMedium + Units.smallSpacing * 2 width: height + (root.action ? Units.gridUnit*2 : 0) visible: root.leftAction readonly property bool pressed: root.leftAction && root.leftAction.enabled && ((mouseArea.actionUnderMouse == root.leftAction && mouseArea.pressed) || root.leftAction.checked) property color baseColor: root.leftAction && root.leftAction.icon && root.leftAction.icon.color && root.leftAction.icon.color != undefined && root.leftAction.icon.color.a > 0 ? root.leftAction.icon.color : Theme.highlightColor color: pressed ? baseColor : Theme.backgroundColor Behavior on color { ColorAnimation { duration: Units.longDuration easing.type: Easing.InOutQuad } } ActionsMenu { id: leftActionSubMenu y: -height x: -width/2 + parent.width/2 actions: root.leftAction ? root.leftAction.children : "" submenuComponent: Component { ActionsMenu {} } } Icon { source: root.leftAction && root.leftAction.iconName ? root.leftAction.iconName : "" width: Units.iconSizes.smallMedium height: width selected: leftButtonGraphics.pressed color: root.action && root.action.color && root.action.color.a > 0 ? root.action.color : (selected ? Theme.highlightedTextColor : Theme.textColor) anchors { left: parent.left verticalCenter: parent.verticalCenter margins: Units.smallSpacing * 2 } } } //right button Rectangle { id: rightButtonGraphics z: -1 anchors { right: parent.right //verticalCenter: parent.verticalCenter bottom: parent.bottom bottomMargin: Units.smallSpacing } enabled: root.rightAction && root.rightAction.enabled radius: Units.devicePixelRatio*2 height: Units.iconSizes.smallMedium + Units.smallSpacing * 2 width: height + (root.action ? Units.gridUnit*2 : 0) visible: root.rightAction readonly property bool pressed: root.rightAction && root.rightAction.enabled && ((mouseArea.actionUnderMouse == root.rightAction && mouseArea.pressed) || root.rightAction.checked) property color baseColor: root.rightAction && root.rightAction.icon && root.rightAction.icon.color && root.rightAction.icon.color != undefined && root.rightAction.icon.color.a > 0 ? root.rightAction.icon.color : Theme.highlightColor color: pressed ? baseColor : Theme.backgroundColor Behavior on color { ColorAnimation { duration: Units.longDuration easing.type: Easing.InOutQuad } } ActionsMenu { id: rightActionSubMenu y: -height x: -width/2 + parent.width/2 actions: root.rightAction ? root.rightAction.children : "" submenuComponent: Component { ActionsMenu {} } } Icon { source: root.rightAction && root.rightAction.iconName ? root.rightAction.iconName : "" width: Units.iconSizes.smallMedium height: width selected: rightButtonGraphics.pressed color: root.action && root.action.color && root.action.color.a > 0 ? root.action.color : (selected ? Theme.highlightedTextColor : Theme.textColor) anchors { right: parent.right verticalCenter: parent.verticalCenter margins: Units.smallSpacing * 2 } } } } DropShadow { anchors.fill: background horizontalOffset: 0 verticalOffset: Units.devicePixelRatio radius: Units.gridUnit /2 samples: 16 color: Qt.rgba(0, 0, 0, mouseArea.pressed ? 0.6 : 0.4) source: background } } } MouseArea { id: fakeContextMenuButton anchors { right: edgeMouseArea.right bottom: parent.bottom margins: Units.smallSpacing } drag { target: button axis: Drag.XAxis minimumX: root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal ? 0 : root.width/2 - button.width/2 maximumX: root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal ? root.width : root.width/2 - button.width/2 } visible: root.page.actions && root.page.actions.contextualActions.length > 0 && (applicationWindow === undefined || applicationWindow().wideScreen) //using internal pagerow api - && (root.page && root.page.parent ? root.page.parent.level < applicationWindow().pageStack.depth-1 : false) + && (root.page && root.page.parent ? root.page.ColumnView.level < applicationWindow().pageStack.depth-1 : false) width: Units.iconSizes.smallMedium + Units.smallSpacing*2 height: width DropShadow { anchors.fill: handleGraphics horizontalOffset: 0 verticalOffset: Units.devicePixelRatio radius: Units.gridUnit /2 samples: 16 color: Qt.rgba(0, 0, 0, fakeContextMenuButton.pressed ? 0.6 : 0.4) source: handleGraphics } Rectangle { id: handleGraphics anchors.fill: parent color: fakeContextMenuButton.pressed ? Theme.highlightColor : Theme.backgroundColor radius: Units.devicePixelRatio Icon { anchors.centerIn: parent width: Units.iconSizes.smallMedium selected: fakeContextMenuButton.pressed height: width source: "overflow-menu" } Behavior on color { ColorAnimation { duration: Units.longDuration easing.type: Easing.InOutQuad } } } onPressed: { mouseArea.onPressed(mouse) } onReleased: { if (globalDrawer) { globalDrawer.peeking = false; } if (contextDrawer) { contextDrawer.peeking = false; } var pos = root.mapFromItem(fakeContextMenuButton, mouse.x, mouse.y); if (contextDrawer) { if (pos.x < root.width/2) { contextDrawer.open(); } else if (contextDrawer.drawerOpen && mouse.x > 0 && mouse.x < width) { contextDrawer.close(); } } if (globalDrawer) { if (globalDrawer.position > 0.5) { globalDrawer.open(); } else { globalDrawer.close(); } } if (containsMouse && (!globalDrawer || !globalDrawer.drawerOpen || !globalDrawer.modal) && (!contextDrawer || !contextDrawer.drawerOpen || !contextDrawer.modal)) { contextMenu.visible = !contextMenu.visible; } } ActionsMenu { id: contextMenu x: parent.width - width y: -height actions: root.page.actions.contextualActions submenuComponent: Component { ActionsMenu {} } } } MouseArea { id: edgeMouseArea z:99 anchors { left: parent.left right: parent.right bottom: parent.bottom } drag { target: button //filterChildren: true axis: Drag.XAxis minimumX: root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal ? 0 : root.width/2 - button.width/2 maximumX: root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal ? root.width : root.width/2 - button.width/2 } height: Units.smallSpacing * 3 onPressed: mouseArea.onPressed(mouse) onPositionChanged: mouseArea.positionChanged(mouse) onReleased: mouseArea.released(mouse) } } diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp index 4448b863..5233f37a 100644 --- a/src/kirigamiplugin.cpp +++ b/src/kirigamiplugin.cpp @@ -1,187 +1,189 @@ /* * Copyright 2009 by Alan Alpert * Copyright 2010 by Ménard Alexis * Copyright 2010 by Marco Martin * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kirigamiplugin.h" +#include "columnview.h" #include "enums.h" #include "desktopicon.h" #include "settings.h" #include "formlayoutattached.h" #include "mnemonicattached.h" #include "delegaterecycler.h" #include "scenepositionattached.h" #include #include #include #include #include "libkirigami/platformtheme.h" static QString s_selectedStyle; //Q_INIT_RESOURCE(kirigami); #ifdef KIRIGAMI_BUILD_TYPE_STATIC #include #endif QUrl KirigamiPlugin::componentUrl(const QString &fileName) const { foreach (const QString &style, m_stylesFallbackChain) { const QString candidate = QStringLiteral("styles/") + style + QLatin1Char('/') + fileName; if (QFile::exists(resolveFilePath(candidate))) { #ifdef KIRIGAMI_BUILD_TYPE_STATIC return QUrl(QStringLiteral("qrc:/org/kde/kirigami/styles/") + style + QLatin1Char('/') + fileName); #else return QUrl(resolveFileUrl(candidate)); #endif } } #ifdef KIRIGAMI_BUILD_TYPE_STATIC return QUrl(QStringLiteral("qrc:/org/kde/kirigami/") + fileName); #else return QUrl(resolveFileUrl(fileName)); #endif } void KirigamiPlugin::registerTypes(const char *uri) { Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.kirigami")); const QString style = QQuickStyle::name(); if (!qEnvironmentVariableIsSet("XDG_CURRENT_DESKTOP")) { QIcon::setThemeSearchPaths({resolveFilePath(QStringLiteral(".")), QStringLiteral(":/icons")}); QIcon::setThemeName(QStringLiteral("breeze-internal")); } #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) //org.kde.desktop.plasma is a couple of files that fall back to desktop by purpose if ((style.isEmpty() || style == QStringLiteral("org.kde.desktop.plasma")) && QFile::exists(resolveFilePath(QStringLiteral("/styles/org.kde.desktop")))) { m_stylesFallbackChain.prepend(QStringLiteral("org.kde.desktop")); } #elif defined(Q_OS_ANDROID) if (!m_stylesFallbackChain.contains(QStringLiteral("Material"))) { m_stylesFallbackChain.prepend(QStringLiteral("Material")); } #else // do we have an iOS specific style? if (!m_stylesFallbackChain.contains(QStringLiteral("Material"))) { m_stylesFallbackChain.prepend(QStringLiteral("Material")); } #endif if (!style.isEmpty() && QFile::exists(resolveFilePath(QStringLiteral("/styles/") + style)) && !m_stylesFallbackChain.contains(style)) { m_stylesFallbackChain.prepend(style); //if we have plasma deps installed, use them for extra integration if (style == QStringLiteral("org.kde.desktop") && QFile::exists(resolveFilePath(QStringLiteral("/styles/org.kde.desktop.plasma")))) { m_stylesFallbackChain.prepend(QStringLiteral("org.kde.desktop.plasma")); } } else { #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) m_stylesFallbackChain.prepend(QStringLiteral("org.kde.desktop")); #endif } //At this point the fallback chain will be selected->org.kde.desktop->Fallback s_selectedStyle = m_stylesFallbackChain.first(); qmlRegisterSingletonType(uri, 2, 0, "Settings", [](QQmlEngine*, QJSEngine*) -> QObject* { Settings *settings = new Settings; settings->setStyle(s_selectedStyle); return settings; } ); qmlRegisterUncreatableType(uri, 2, 0, "ApplicationHeaderStyle", QStringLiteral("Cannot create objects of type ApplicationHeaderStyle")); //old legacy retrocompatible Theme qmlRegisterSingletonType(componentUrl(QStringLiteral("Theme.qml")), uri, 2, 0, "Theme"); qmlRegisterSingletonType(componentUrl(QStringLiteral("Units.qml")), uri, 2, 0, "Units"); qmlRegisterType(componentUrl(QStringLiteral("Action.qml")), uri, 2, 0, "Action"); qmlRegisterType(componentUrl(QStringLiteral("AbstractApplicationHeader.qml")), uri, 2, 0, "AbstractApplicationHeader"); qmlRegisterType(componentUrl(QStringLiteral("AbstractApplicationWindow.qml")), uri, 2, 0, "AbstractApplicationWindow"); qmlRegisterType(componentUrl(QStringLiteral("AbstractListItem.qml")), uri, 2, 0, "AbstractListItem"); qmlRegisterType(componentUrl(QStringLiteral("ApplicationHeader.qml")), uri, 2, 0, "ApplicationHeader"); qmlRegisterType(componentUrl(QStringLiteral("ToolBarApplicationHeader.qml")), uri, 2, 0, "ToolBarApplicationHeader"); qmlRegisterType(componentUrl(QStringLiteral("ApplicationWindow.qml")), uri, 2, 0, "ApplicationWindow"); qmlRegisterType(componentUrl(QStringLiteral("BasicListItem.qml")), uri, 2, 0, "BasicListItem"); qmlRegisterType(componentUrl(QStringLiteral("OverlayDrawer.qml")), uri, 2, 0, "OverlayDrawer"); qmlRegisterType(componentUrl(QStringLiteral("ContextDrawer.qml")), uri, 2, 0, "ContextDrawer"); qmlRegisterType(componentUrl(QStringLiteral("GlobalDrawer.qml")), uri, 2, 0, "GlobalDrawer"); qmlRegisterType(componentUrl(QStringLiteral("Heading.qml")), uri, 2, 0, "Heading"); qmlRegisterType(componentUrl(QStringLiteral("Separator.qml")), uri, 2, 0, "Separator"); qmlRegisterType(componentUrl(QStringLiteral("PageRow.qml")), uri, 2, 0, "PageRow"); qmlRegisterType(uri, 2, 0, "Icon"); qmlRegisterType(componentUrl(QStringLiteral("Label.qml")), uri, 2, 0, "Label"); //TODO: uncomment for 2.3 release //qmlRegisterTypeNotAvailable(uri, 2, 3, "Label", "Label type not supported anymore, use QtQuick.Controls.Label 2.0 instead"); qmlRegisterType(componentUrl(QStringLiteral("OverlaySheet.qml")), uri, 2, 0, "OverlaySheet"); qmlRegisterType(componentUrl(QStringLiteral("Page.qml")), uri, 2, 0, "Page"); qmlRegisterType(componentUrl(QStringLiteral("ScrollablePage.qml")), uri, 2, 0, "ScrollablePage"); qmlRegisterType(componentUrl(QStringLiteral("SplitDrawer.qml")), uri, 2, 0, "SplitDrawer"); qmlRegisterType(componentUrl(QStringLiteral("SwipeListItem.qml")), uri, 2, 0, "SwipeListItem"); //2.1 qmlRegisterType(componentUrl(QStringLiteral("AbstractItemViewHeader.qml")), uri, 2, 1, "AbstractItemViewHeader"); qmlRegisterType(componentUrl(QStringLiteral("ItemViewHeader.qml")), uri, 2, 1, "ItemViewHeader"); qmlRegisterType(componentUrl(QStringLiteral("AbstractApplicationItem.qml")), uri, 2, 1, "AbstractApplicationItem"); qmlRegisterType(componentUrl(QStringLiteral("ApplicationItem.qml")), uri, 2, 1, "ApplicationItem"); //2.2 //Theme changed from a singleton to an attached property qmlRegisterUncreatableType(uri, 2, 2, "Theme", QStringLiteral("Cannot create objects of type Theme, use it as an attached property")); //2.3 qmlRegisterType(componentUrl(QStringLiteral("FormLayout.qml")), uri, 2, 3, "FormLayout"); qmlRegisterUncreatableType(uri, 2, 3, "FormData", QStringLiteral("Cannot create objects of type FormData, use it as an attached property")); qmlRegisterUncreatableType(uri, 2, 3, "MnemonicData", QStringLiteral("Cannot create objects of type MnemonicData, use it as an attached property")); //2.4 qmlRegisterType(componentUrl(QStringLiteral("AbstractCard.qml")), uri, 2, 4, "AbstractCard"); qmlRegisterType(componentUrl(QStringLiteral("Card.qml")), uri, 2, 4, "Card"); qmlRegisterType(componentUrl(QStringLiteral("CardsListView.qml")), uri, 2, 4, "CardsListView"); qmlRegisterType(componentUrl(QStringLiteral("CardsGridView.qml")), uri, 2, 4, "CardsGridView"); qmlRegisterType(componentUrl(QStringLiteral("CardsLayout.qml")), uri, 2, 4, "CardsLayout"); qmlRegisterType(componentUrl(QStringLiteral("InlineMessage.qml")), uri, 2, 4, "InlineMessage"); qmlRegisterUncreatableType(uri, 2, 4, "MessageType", QStringLiteral("Cannot create objects of type MessageType")); qmlRegisterType(uri, 2, 4, "DelegateRecycler"); //2.5 qmlRegisterType(componentUrl(QStringLiteral("ListItemDragHandle.qml")), uri, 2, 5, "ListItemDragHandle"); qmlRegisterType(componentUrl(QStringLiteral("ActionToolBar.qml")), uri, 2, 5, "ActionToolBar"); qmlRegisterUncreatableType(uri, 2, 5, "ScenePosition", QStringLiteral("Cannot create objects of type ScenePosition, use it as an attached property")); //2.6 qmlRegisterType(componentUrl(QStringLiteral("AboutPage.qml")), uri, 2, 6, "AboutPage"); qmlRegisterType(componentUrl(QStringLiteral("LinkButton.qml")), uri, 2, 6, "LinkButton"); qmlRegisterType(componentUrl(QStringLiteral("UrlButton.qml")), uri, 2, 6, "UrlButton"); //2.7 + qmlRegisterType(uri, 2, 7, "ColumnView"); qmlRegisterType(componentUrl(QStringLiteral("ActionTextField.qml")), uri, 2, 7, "ActionTextField"); qmlProtectModule(uri, 2); } #include "moc_kirigamiplugin.cpp"