diff options
Diffstat (limited to 'vcl/qt5')
34 files changed, 12656 insertions, 0 deletions
diff --git a/vcl/qt5/QtAccessibleEventListener.cxx b/vcl/qt5/QtAccessibleEventListener.cxx new file mode 100644 index 0000000000..0bf4dcddbf --- /dev/null +++ b/vcl/qt5/QtAccessibleEventListener.cxx @@ -0,0 +1,439 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtAccessibleEventListener.hxx> +#include <QtAccessibleRegistry.hxx> +#include <QtTools.hxx> + +#include <sal/log.hxx> + +#include <com/sun/star/accessibility/AccessibleEventId.hpp> +#include <com/sun/star/accessibility/AccessibleStateType.hpp> +#include <com/sun/star/accessibility/AccessibleTableModelChange.hpp> +#include <com/sun/star/accessibility/AccessibleTableModelChangeType.hpp> +#include <com/sun/star/accessibility/TextSegment.hpp> + +#include <QtGui/QAccessible> +#include <QtGui/QAccessibleTextSelectionEvent> + +using namespace css; +using namespace css::accessibility; +using namespace css::lang; +using namespace css::uno; + +QtAccessibleEventListener::QtAccessibleEventListener(QtAccessibleWidget* pAccessibleWidget) + : m_pAccessibleWidget(pAccessibleWidget) +{ +} + +void QtAccessibleEventListener::HandleStateChangedEvent( + QAccessibleInterface* pQAccessibleInterface, + const css::accessibility::AccessibleEventObject& rEvent) +{ + QAccessible::State aState; + + sal_Int64 nState = 0; + rEvent.NewValue >>= nState; + // States in 'QAccessibleStateChangeEvent' indicate what states have changed, so if e.g. + // an object loses focus (not just if it gains it), 'focus' state needs to be set to 'true', + // so retrieve the old/previous value from the event if necessary. + if (nState == AccessibleStateType::INVALID) + rEvent.OldValue >>= nState; + + switch (nState) + { + case AccessibleStateType::ACTIVE: + aState.active = true; + break; + case AccessibleStateType::BUSY: + aState.busy = true; + break; + case AccessibleStateType::CHECKABLE: + aState.checkable = true; + break; + case AccessibleStateType::CHECKED: + aState.checked = true; + break; + case AccessibleStateType::COLLAPSE: + aState.collapsed = true; + break; + case AccessibleStateType::DEFAULT: + aState.defaultButton = true; + break; + case AccessibleStateType::ENABLED: + aState.disabled = true; + break; + case AccessibleStateType::EDITABLE: + aState.editable = true; + break; + case AccessibleStateType::EXPANDABLE: + aState.expandable = true; + break; + case AccessibleStateType::EXPANDED: + aState.expanded = true; + break; + case AccessibleStateType::FOCUSABLE: + aState.focusable = true; + break; + case AccessibleStateType::FOCUSED: + aState.focused = true; + break; + case AccessibleStateType::INVALID: + aState.invalid = true; + break; + case AccessibleStateType::VISIBLE: + aState.invisible = true; + break; + case AccessibleStateType::MODAL: + aState.modal = true; + break; + case AccessibleStateType::MOVEABLE: + aState.movable = true; + break; + case AccessibleStateType::MULTI_LINE: + // comment in Qt's qaccessible.h has this: + // "// quint64 singleLine : 1; // we have multi line, this is redundant." + case AccessibleStateType::SINGLE_LINE: + aState.multiLine = true; + break; + case AccessibleStateType::MULTI_SELECTABLE: + aState.multiSelectable = true; + break; + case AccessibleStateType::OFFSCREEN: + aState.offscreen = true; + break; + case AccessibleStateType::PRESSED: + aState.pressed = true; + break; + case AccessibleStateType::RESIZABLE: + aState.sizeable = true; + break; + case AccessibleStateType::SELECTABLE: + aState.selectable = true; + break; + case AccessibleStateType::SELECTED: + aState.selected = true; + break; + case AccessibleStateType::SHOWING: + { + // Qt does not have an equivalent for the SHOWING state, + // but has separate event types + QAccessible::Event eEventType; + sal_Int64 nNewState = 0; + if ((rEvent.NewValue >>= nNewState) && nNewState == AccessibleStateType::SHOWING) + eEventType = QAccessible::ObjectShow; + else + eEventType = QAccessible::ObjectHide; + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, eEventType)); + break; + } + // These don't seem to have a matching Qt equivalent + case AccessibleStateType::ARMED: + case AccessibleStateType::DEFUNC: + case AccessibleStateType::HORIZONTAL: + case AccessibleStateType::ICONIFIED: + case AccessibleStateType::INDETERMINATE: + case AccessibleStateType::MANAGES_DESCENDANTS: + case AccessibleStateType::OPAQUE: + case AccessibleStateType::SENSITIVE: + case AccessibleStateType::STALE: + case AccessibleStateType::TRANSIENT: + case AccessibleStateType::VERTICAL: + default: + return; + } + + QAccessible::updateAccessibility( + new QAccessibleStateChangeEvent(pQAccessibleInterface, aState)); +} + +void QtAccessibleEventListener::notifyEvent(const css::accessibility::AccessibleEventObject& aEvent) +{ + QAccessibleInterface* pQAccessibleInterface = m_pAccessibleWidget; + + switch (aEvent.EventId) + { + case AccessibleEventId::NAME_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::NameChanged)); + return; + case AccessibleEventId::DESCRIPTION_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::DescriptionChanged)); + return; + case AccessibleEventId::ACTION_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::ActionChanged)); + return; + case AccessibleEventId::ACTIVE_DESCENDANT_CHANGED: + { + // Qt has a QAccessible::ActiveDescendantChanged event type, but events of + // that type are currently just ignored on Qt side and not forwarded to AT-SPI. + // Send a state change event for the focused state of the newly + // active descendant instead + uno::Reference<accessibility::XAccessible> xActiveAccessible; + aEvent.NewValue >>= xActiveAccessible; + if (!xActiveAccessible.is()) + return; + + QObject* pQtAcc = QtAccessibleRegistry::getQObject(xActiveAccessible); + QAccessibleInterface* pInterface = QAccessible::queryAccessibleInterface(pQtAcc); + QAccessible::State aState; + aState.focused = true; + QAccessible::updateAccessibility(new QAccessibleStateChangeEvent(pInterface, aState)); + return; + } + case AccessibleEventId::CARET_CHANGED: + { + sal_Int32 nNewCursorPos = 0; + aEvent.NewValue >>= nNewCursorPos; + QAccessible::updateAccessibility( + new QAccessibleTextCursorEvent(pQAccessibleInterface, nNewCursorPos)); + return; + } + case AccessibleEventId::CHILD: + { + Reference<XAccessible> xChild; + if (aEvent.NewValue >>= xChild) + { + assert(xChild.is() + && "AccessibleEventId::CHILD event NewValue without valid child set"); + // tdf#159213 for now, workaround invalid events being sent and don't crash in release builds + if (!xChild.is()) + return; + QAccessible::updateAccessibility(new QAccessibleEvent( + QtAccessibleRegistry::getQObject(xChild), QAccessible::ObjectCreated)); + return; + } + if (aEvent.OldValue >>= xChild) + { + assert(xChild.is() + && "AccessibleEventId::CHILD event OldValue without valid child set"); + // tdf#159213 for now, workaround invalid events being sent and don't crash in release builds + if (!xChild.is()) + return; + QAccessible::updateAccessibility(new QAccessibleEvent( + QtAccessibleRegistry::getQObject(xChild), QAccessible::ObjectDestroyed)); + return; + } + SAL_WARN("vcl.qt", + "Ignoring invalid AccessibleEventId::CHILD event without any child set."); + return; + } + case AccessibleEventId::HYPERTEXT_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::HypertextChanged)); + return; + case AccessibleEventId::SELECTION_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::Selection)); + return; + case AccessibleEventId::VISIBLE_DATA_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::VisibleDataChanged)); + return; + case AccessibleEventId::TEXT_SELECTION_CHANGED: + { + QAccessibleTextInterface* pTextInterface = pQAccessibleInterface->textInterface(); + if (!pTextInterface) + { + SAL_WARN("vcl.qt", "TEXT_SELECTION_CHANGED event received for object not " + "implementing text interface"); + return; + } + int nStartOffset = 0; + int nEndOffset = 0; + pTextInterface->selection(0, &nStartOffset, &nEndOffset); + QAccessible::updateAccessibility( + new QAccessibleTextSelectionEvent(pQAccessibleInterface, nStartOffset, nEndOffset)); + return; + } + case AccessibleEventId::TEXT_ATTRIBUTE_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::AttributeChanged)); + return; + case AccessibleEventId::TEXT_CHANGED: + { + TextSegment aDeletedText; + TextSegment aInsertedText; + if (aEvent.OldValue >>= aDeletedText) + { + QAccessible::updateAccessibility( + new QAccessibleTextRemoveEvent(pQAccessibleInterface, aDeletedText.SegmentStart, + toQString(aDeletedText.SegmentText))); + } + if (aEvent.NewValue >>= aInsertedText) + { + QAccessible::updateAccessibility(new QAccessibleTextInsertEvent( + pQAccessibleInterface, aInsertedText.SegmentStart, + toQString(aInsertedText.SegmentText))); + } + return; + } + case AccessibleEventId::TABLE_CAPTION_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::TableCaptionChanged)); + return; + case AccessibleEventId::TABLE_COLUMN_DESCRIPTION_CHANGED: + QAccessible::updateAccessibility(new QAccessibleEvent( + pQAccessibleInterface, QAccessible::TableColumnDescriptionChanged)); + return; + case AccessibleEventId::TABLE_COLUMN_HEADER_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::TableColumnHeaderChanged)); + return; + case AccessibleEventId::TABLE_MODEL_CHANGED: + { + AccessibleTableModelChange aChange; + aEvent.NewValue >>= aChange; + + QAccessibleTableModelChangeEvent::ModelChangeType nType; + switch (aChange.Type) + { + case AccessibleTableModelChangeType::COLUMNS_INSERTED: + nType = QAccessibleTableModelChangeEvent::ColumnsInserted; + break; + case AccessibleTableModelChangeType::COLUMNS_REMOVED: + nType = QAccessibleTableModelChangeEvent::ColumnsRemoved; + break; + case AccessibleTableModelChangeType::ROWS_INSERTED: + nType = QAccessibleTableModelChangeEvent::RowsInserted; + break; + case AccessibleTableModelChangeType::ROWS_REMOVED: + nType = QAccessibleTableModelChangeEvent::RowsRemoved; + break; + case AccessibleTableModelChangeType::UPDATE: + nType = QAccessibleTableModelChangeEvent::DataChanged; + break; + default: + assert(false && "Unhandled AccessibleTableModelChangeType"); + return; + } + QAccessibleTableModelChangeEvent* pTableEvent + = new QAccessibleTableModelChangeEvent(pQAccessibleInterface, nType); + pTableEvent->setFirstRow(aChange.FirstRow); + pTableEvent->setLastRow(aChange.LastRow); + pTableEvent->setFirstColumn(aChange.FirstColumn); + pTableEvent->setLastColumn(aChange.LastColumn); + QAccessible::updateAccessibility(pTableEvent); + return; + } + case AccessibleEventId::TABLE_ROW_DESCRIPTION_CHANGED: + QAccessible::updateAccessibility(new QAccessibleEvent( + pQAccessibleInterface, QAccessible::TableRowDescriptionChanged)); + return; + case AccessibleEventId::TABLE_ROW_HEADER_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::TableRowHeaderChanged)); + return; + case AccessibleEventId::TABLE_SUMMARY_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::TableSummaryChanged)); + return; + case AccessibleEventId::SELECTION_CHANGED_ADD: + case AccessibleEventId::SELECTION_CHANGED_REMOVE: + { + QAccessible::Event eEventType; + if (aEvent.EventId == AccessibleEventId::SELECTION_CHANGED_ADD) + eEventType = QAccessible::SelectionAdd; + else + eEventType = QAccessible::SelectionRemove; + + uno::Reference<accessibility::XAccessible> xChildAcc; + aEvent.NewValue >>= xChildAcc; + if (!xChildAcc.is()) + { + SAL_WARN("vcl.qt", + "Selection add/remove event without the (un)selected accessible set"); + return; + } + Reference<XAccessibleContext> xContext = xChildAcc->getAccessibleContext(); + if (!xContext.is()) + { + SAL_WARN("vcl.qt", "No valid XAccessibleContext for (un)selected accessible."); + return; + } + + // Qt expects the event to be sent for the (un)selected child + QObject* pChildObject = QtAccessibleRegistry::getQObject(xChildAcc); + assert(pChildObject); + QAccessible::updateAccessibility(new QAccessibleEvent(pChildObject, eEventType)); + return; + } + case AccessibleEventId::SELECTION_CHANGED_WITHIN: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::SelectionWithin)); + return; + case AccessibleEventId::PAGE_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::PageChanged)); + return; + case AccessibleEventId::SECTION_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::SectionChanged)); + return; + case AccessibleEventId::COLUMN_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::TextColumnChanged)); + return; + case AccessibleEventId::BOUNDRECT_CHANGED: + QAccessible::updateAccessibility( + new QAccessibleEvent(pQAccessibleInterface, QAccessible::LocationChanged)); + return; + case AccessibleEventId::STATE_CHANGED: + HandleStateChangedEvent(pQAccessibleInterface, aEvent); + return; + case AccessibleEventId::VALUE_CHANGED: + { + QAccessibleValueInterface* pValueInterface = pQAccessibleInterface->valueInterface(); + if (pValueInterface) + { + const QVariant aValue = pValueInterface->currentValue(); + QAccessible::updateAccessibility( + new QAccessibleValueChangeEvent(pQAccessibleInterface, aValue)); + } + return; + } + case AccessibleEventId::ROLE_CHANGED: + case AccessibleEventId::INVALIDATE_ALL_CHILDREN: + case AccessibleEventId::CONTENT_FLOWS_FROM_RELATION_CHANGED: + case AccessibleEventId::CONTENT_FLOWS_TO_RELATION_CHANGED: + case AccessibleEventId::CONTROLLED_BY_RELATION_CHANGED: + case AccessibleEventId::CONTROLLER_FOR_RELATION_CHANGED: + case AccessibleEventId::LABEL_FOR_RELATION_CHANGED: + case AccessibleEventId::LABELED_BY_RELATION_CHANGED: + case AccessibleEventId::MEMBER_OF_RELATION_CHANGED: + case AccessibleEventId::SUB_WINDOW_OF_RELATION_CHANGED: + case AccessibleEventId::LISTBOX_ENTRY_EXPANDED: + case AccessibleEventId::LISTBOX_ENTRY_COLLAPSED: + case AccessibleEventId::ACTIVE_DESCENDANT_CHANGED_NOFOCUS: + default: + SAL_WARN("vcl.qt", "Unmapped AccessibleEventId: " << aEvent.EventId); + return; + } +} + +void QtAccessibleEventListener::disposing(const EventObject& /* Source */) +{ + assert(m_pAccessibleWidget); + m_pAccessibleWidget->invalidate(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtAccessibleRegistry.cxx b/vcl/qt5/QtAccessibleRegistry.cxx new file mode 100644 index 0000000000..88f9abcfd1 --- /dev/null +++ b/vcl/qt5/QtAccessibleRegistry.cxx @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <QtAccessibleRegistry.hxx> +#include <QtXAccessible.hxx> + +#include <cassert> + +std::map<XAccessible*, QObject*> QtAccessibleRegistry::m_aMapping = {}; + +QObject* QtAccessibleRegistry::getQObject(css::uno::Reference<XAccessible> xAcc) +{ + if (!xAcc.is()) + return nullptr; + + // look for existing entry in the map + auto entry = m_aMapping.find(xAcc.get()); + if (entry != m_aMapping.end()) + return entry->second; + + // create a new object and remember it in the map + QtXAccessible* pQtAcc = new QtXAccessible(xAcc); + m_aMapping.emplace(xAcc.get(), pQtAcc); + return pQtAcc; +} + +void QtAccessibleRegistry::insert(css::uno::Reference<XAccessible> xAcc, QObject* pQObject) +{ + assert(pQObject); + m_aMapping.emplace(xAcc.get(), pQObject); +} + +void QtAccessibleRegistry::remove(css::uno::Reference<XAccessible> xAcc) +{ + assert(xAcc.is()); + m_aMapping.erase(xAcc.get()); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtAccessibleWidget.cxx b/vcl/qt5/QtAccessibleWidget.cxx new file mode 100644 index 0000000000..7eadc33138 --- /dev/null +++ b/vcl/qt5/QtAccessibleWidget.cxx @@ -0,0 +1,1848 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtAccessibleWidget.hxx> + +#include <QtGui/QAccessibleInterface> + +#include <QtAccessibleEventListener.hxx> +#include <QtAccessibleRegistry.hxx> +#include <QtFrame.hxx> +#include <QtTools.hxx> +#include <QtWidget.hxx> +#include <QtXAccessible.hxx> + +#include <com/sun/star/accessibility/AccessibleRelationType.hpp> +#include <com/sun/star/accessibility/AccessibleRole.hpp> +#include <com/sun/star/accessibility/AccessibleScrollType.hpp> +#include <com/sun/star/accessibility/AccessibleStateType.hpp> +#include <com/sun/star/accessibility/AccessibleTextType.hpp> +#include <com/sun/star/accessibility/XAccessible.hpp> +#include <com/sun/star/accessibility/XAccessibleAction.hpp> +#include <com/sun/star/accessibility/XAccessibleComponent.hpp> +#include <com/sun/star/accessibility/XAccessibleEditableText.hpp> +#include <com/sun/star/accessibility/XAccessibleEventBroadcaster.hpp> +#include <com/sun/star/accessibility/XAccessibleEventListener.hpp> +#include <com/sun/star/accessibility/XAccessibleKeyBinding.hpp> +#include <com/sun/star/accessibility/XAccessibleRelationSet.hpp> +#include <com/sun/star/accessibility/XAccessibleSelection.hpp> +#include <com/sun/star/accessibility/XAccessibleTable.hpp> +#include <com/sun/star/accessibility/XAccessibleTableSelection.hpp> +#include <com/sun/star/accessibility/XAccessibleText.hpp> +#include <com/sun/star/accessibility/XAccessibleValue.hpp> +#include <com/sun/star/beans/PropertyValue.hpp> +#include <com/sun/star/lang/DisposedException.hpp> +#include <com/sun/star/uno/Sequence.hxx> + +#include <comphelper/AccessibleImplementationHelper.hxx> +#include <o3tl/any.hxx> +#include <sal/log.hxx> +#include <vcl/accessibility/AccessibleTextAttributeHelper.hxx> + +using namespace css; +using namespace css::accessibility; +using namespace css::beans; +using namespace css::uno; + +QtAccessibleWidget::QtAccessibleWidget(const Reference<XAccessible> xAccessible, QObject* pObject) + : m_xAccessible(xAccessible) + , m_pObject(pObject) +{ + Reference<XAccessibleContext> xContext = xAccessible->getAccessibleContext(); + Reference<XAccessibleEventBroadcaster> xBroadcaster(xContext, UNO_QUERY); + if (xBroadcaster.is()) + { + Reference<XAccessibleEventListener> xListener(new QtAccessibleEventListener(this)); + xBroadcaster->addAccessibleEventListener(xListener); + } +} + +void QtAccessibleWidget::invalidate() +{ + QtAccessibleRegistry::remove(m_xAccessible); + m_xAccessible.clear(); +} + +Reference<XAccessibleContext> QtAccessibleWidget::getAccessibleContextImpl() const +{ + Reference<XAccessibleContext> xAc; + + if (m_xAccessible.is()) + { + try + { + xAc = m_xAccessible->getAccessibleContext(); + } + catch (css::lang::DisposedException /*ex*/) + { + SAL_WARN("vcl.qt", "Accessible context disposed already"); + } + // sometimes getAccessibleContext throws also RuntimeException if context is no longer alive + catch (css::uno::RuntimeException /*ex*/) + { + // so let's catch it here, cuz otherwise soffice falls flat on its face + // with FatalError and nothing else + SAL_WARN("vcl.qt", "Accessible context no longer alive"); + } + } + + return xAc; +} + +css::uno::Reference<css::accessibility::XAccessibleTable> +QtAccessibleWidget::getAccessibleTableForParent() const +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return nullptr; + + Reference<XAccessible> xParent = xAcc->getAccessibleParent(); + if (!xParent.is()) + return nullptr; + + Reference<XAccessibleContext> xParentContext = xParent->getAccessibleContext(); + if (!xParentContext.is()) + return nullptr; + + return Reference<XAccessibleTable>(xParentContext, UNO_QUERY); +} + +QWindow* QtAccessibleWidget::window() const +{ + assert(m_pObject); + if (m_pObject->isWidgetType()) + { + QWidget* pWidget = static_cast<QWidget*>(m_pObject); + QWidget* pWindow = pWidget->window(); + if (pWindow) + return pWindow->windowHandle(); + } + + QAccessibleInterface* pParent = parent(); + if (pParent) + return pParent->window(); + + return nullptr; +} + +int QtAccessibleWidget::childCount() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return 0; + + sal_Int64 nChildCount = xAc->getAccessibleChildCount(); + if (nChildCount > std::numeric_limits<int>::max()) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::childCount: Child count exceeds maximum int value, " + "returning max int."); + nChildCount = std::numeric_limits<int>::max(); + } + + return nChildCount; +} + +int QtAccessibleWidget::indexOfChild(const QAccessibleInterface* pChild) const +{ + const QtAccessibleWidget* pAccessibleWidget = dynamic_cast<const QtAccessibleWidget*>(pChild); + if (!pAccessibleWidget) + { + SAL_WARN( + "vcl.qt", + "QtAccessibleWidget::indexOfChild called with child that is no QtAccessibleWidget"); + return -1; + } + + Reference<XAccessibleContext> xContext = pAccessibleWidget->getAccessibleContextImpl(); + if (!xContext.is()) + return -1; + + sal_Int64 nChildIndex = xContext->getAccessibleIndexInParent(); + if (nChildIndex > std::numeric_limits<int>::max()) + { + // use -2 when the child index is too large to fit into 32 bit to neither use the + // valid index of another child nor -1, which would e.g. make the Orca screen reader + // interpret the object as being a zombie + SAL_WARN("vcl.qt", + "QtAccessibleWidget::indexOfChild: Child index exceeds maximum int value, " + "returning -2."); + nChildIndex = -2; + } + return nChildIndex; +} + +namespace +{ +sal_Int16 lcl_matchQtTextBoundaryType(QAccessible::TextBoundaryType boundaryType) +{ + switch (boundaryType) + { + case QAccessible::CharBoundary: + return com::sun::star::accessibility::AccessibleTextType::CHARACTER; + case QAccessible::WordBoundary: + return com::sun::star::accessibility::AccessibleTextType::WORD; + case QAccessible::SentenceBoundary: + return com::sun::star::accessibility::AccessibleTextType::SENTENCE; + case QAccessible::ParagraphBoundary: + return com::sun::star::accessibility::AccessibleTextType::PARAGRAPH; + case QAccessible::LineBoundary: + return com::sun::star::accessibility::AccessibleTextType::LINE; + case QAccessible::NoBoundary: + // assert here, better handle it directly at call site + assert(false + && "No match for QAccessible::NoBoundary, handle it separately at call site."); + break; + default: + break; + } + + SAL_WARN("vcl.qt", "Unmatched text boundary type: " << boundaryType); + return -1; +} + +QAccessible::Relation lcl_matchUnoRelation(short relationType) +{ + // Qt semantics is the other way around + switch (relationType) + { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + case AccessibleRelationType::CONTENT_FLOWS_FROM: + return QAccessible::FlowsTo; + case AccessibleRelationType::CONTENT_FLOWS_TO: + return QAccessible::FlowsFrom; +#endif + case AccessibleRelationType::CONTROLLED_BY: + return QAccessible::Controller; + case AccessibleRelationType::CONTROLLER_FOR: + return QAccessible::Controlled; +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + case AccessibleRelationType::DESCRIBED_BY: + return QAccessible::DescriptionFor; +#endif + case AccessibleRelationType::LABELED_BY: + return QAccessible::Label; + case AccessibleRelationType::LABEL_FOR: + return QAccessible::Labelled; + case AccessibleRelationType::INVALID: + case AccessibleRelationType::MEMBER_OF: + case AccessibleRelationType::SUB_WINDOW_OF: + case AccessibleRelationType::NODE_CHILD_OF: + default: + SAL_WARN("vcl.qt", "Unmatched relation: " << relationType); + return {}; + } +} + +void lcl_appendRelation(QVector<QPair<QAccessibleInterface*, QAccessible::Relation>>* relations, + AccessibleRelation aRelation, QAccessible::Relation match) +{ + QAccessible::Relation aQRelation = lcl_matchUnoRelation(aRelation.RelationType); + // skip in case there's no Qt relation matching the filter + if (!(aQRelation & match)) + return; + + sal_uInt32 nTargetCount = aRelation.TargetSet.getLength(); + + for (sal_uInt32 i = 0; i < nTargetCount; i++) + { + Reference<XAccessible> xAccessible(aRelation.TargetSet[i], uno::UNO_QUERY); + relations->append( + { QAccessible::queryAccessibleInterface(QtAccessibleRegistry::getQObject(xAccessible)), + aQRelation }); + } +} +} + +QVector<QPair<QAccessibleInterface*, QAccessible::Relation>> +QtAccessibleWidget::relations(QAccessible::Relation match) const +{ + QVector<QPair<QAccessibleInterface*, QAccessible::Relation>> relations; + + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return relations; + + Reference<XAccessibleRelationSet> xRelationSet = xAc->getAccessibleRelationSet(); + if (xRelationSet.is()) + { + int count = xRelationSet->getRelationCount(); + for (int i = 0; i < count; i++) + { + AccessibleRelation aRelation = xRelationSet->getRelation(i); + lcl_appendRelation(&relations, aRelation, match); + } + } + + return relations; +} + +QAccessibleInterface* QtAccessibleWidget::focusChild() const +{ + /* if (m_pWindow->HasChildPathFocus()) + return QAccessible::queryAccessibleInterface( + new QtXAccessible(m_xAccessible->getAccessibleContext()->getAccessibleChild(index))); */ + return QAccessible::queryAccessibleInterface(object()); +} + +QRect QtAccessibleWidget::rect() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QRect(); + + Reference<XAccessibleComponent> xAccessibleComponent(xAc, UNO_QUERY); + awt::Point aPoint = xAccessibleComponent->getLocationOnScreen(); + awt::Size aSize = xAccessibleComponent->getSize(); + + return QRect(aPoint.X, aPoint.Y, aSize.Width, aSize.Height); +} + +QAccessibleInterface* QtAccessibleWidget::parent() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return nullptr; + + if (xAc->getAccessibleParent().is()) + return QAccessible::queryAccessibleInterface( + QtAccessibleRegistry::getQObject(xAc->getAccessibleParent())); + + // go via the QObject hierarchy; some a11y objects like the application + // (at the root of the a11y hierarchy) are handled solely by Qt and have + // no LO-internal a11y objects associated with them + QObject* pObj = object(); + if (pObj && pObj->parent()) + return QAccessible::queryAccessibleInterface(pObj->parent()); + + // return app as parent for top-level objects + return QAccessible::queryAccessibleInterface(qApp); +} + +QAccessibleInterface* QtAccessibleWidget::child(int index) const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return nullptr; + + if (index < 0 || index >= xAc->getAccessibleChildCount()) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::child called with invalid index: " << index); + return nullptr; + } + + return QAccessible::queryAccessibleInterface( + QtAccessibleRegistry::getQObject(xAc->getAccessibleChild(index))); +} + +QString QtAccessibleWidget::text(QAccessible::Text text) const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QString(); + + switch (text) + { + case QAccessible::Name: + return toQString(xAc->getAccessibleName()); + case QAccessible::Description: + case QAccessible::DebugDescription: + return toQString(xAc->getAccessibleDescription()); + case QAccessible::Value: + case QAccessible::Help: + case QAccessible::Accelerator: + case QAccessible::UserText: + default: + return QString("Unknown"); + } +} +QAccessible::Role QtAccessibleWidget::role() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QAccessible::NoRole; + + switch (xAc->getAccessibleRole()) + { + case AccessibleRole::UNKNOWN: + return QAccessible::NoRole; + case AccessibleRole::ALERT: + return QAccessible::AlertMessage; + case AccessibleRole::COLUMN_HEADER: + return QAccessible::ColumnHeader; + case AccessibleRole::CANVAS: + return QAccessible::Canvas; + case AccessibleRole::CHECK_BOX: + return QAccessible::CheckBox; + case AccessibleRole::CHECK_MENU_ITEM: + return QAccessible::MenuItem; + case AccessibleRole::COLOR_CHOOSER: + return QAccessible::ColorChooser; + case AccessibleRole::COMBO_BOX: + return QAccessible::ComboBox; + case AccessibleRole::DATE_EDITOR: + return QAccessible::EditableText; + case AccessibleRole::DESKTOP_ICON: + return QAccessible::Graphic; + case AccessibleRole::DESKTOP_PANE: + case AccessibleRole::DIRECTORY_PANE: + return QAccessible::Pane; + case AccessibleRole::DIALOG: + return QAccessible::Dialog; + case AccessibleRole::DOCUMENT: + return QAccessible::Document; + case AccessibleRole::EMBEDDED_OBJECT: + return QAccessible::UserRole; + case AccessibleRole::END_NOTE: + return QAccessible::Note; + case AccessibleRole::FILE_CHOOSER: + return QAccessible::Dialog; + case AccessibleRole::FILLER: + return QAccessible::Whitespace; + case AccessibleRole::FONT_CHOOSER: + return QAccessible::UserRole; + case AccessibleRole::FOOTER: + return QAccessible::Footer; + case AccessibleRole::FOOTNOTE: + return QAccessible::Note; + case AccessibleRole::FRAME: // top-level window with title bar + return QAccessible::Window; + case AccessibleRole::GLASS_PANE: + return QAccessible::UserRole; + case AccessibleRole::GRAPHIC: + return QAccessible::Graphic; + case AccessibleRole::GROUP_BOX: + return QAccessible::Grouping; + case AccessibleRole::HEADER: + return QAccessible::UserRole; + case AccessibleRole::HEADING: + return QAccessible::Heading; + case AccessibleRole::HYPER_LINK: + return QAccessible::Link; + case AccessibleRole::ICON: + return QAccessible::Graphic; + case AccessibleRole::INTERNAL_FRAME: + return QAccessible::UserRole; + case AccessibleRole::LABEL: + return QAccessible::StaticText; + case AccessibleRole::LAYERED_PANE: + return QAccessible::Pane; + case AccessibleRole::LIST: + return QAccessible::List; + case AccessibleRole::LIST_ITEM: + return QAccessible::ListItem; + case AccessibleRole::MENU: + case AccessibleRole::MENU_BAR: + return QAccessible::MenuBar; + case AccessibleRole::MENU_ITEM: + return QAccessible::MenuItem; + case AccessibleRole::NOTIFICATION: + return QAccessible::Notification; + case AccessibleRole::OPTION_PANE: + return QAccessible::Pane; + case AccessibleRole::PAGE_TAB: + return QAccessible::PageTab; + case AccessibleRole::PAGE_TAB_LIST: + return QAccessible::PageTabList; + case AccessibleRole::PANEL: + return QAccessible::Pane; + case AccessibleRole::PARAGRAPH: + case AccessibleRole::BLOCK_QUOTE: + return QAccessible::Paragraph; + case AccessibleRole::PASSWORD_TEXT: + // Qt API doesn't have a separate role to distinguish password edits, + // but a 'passwordEdit' state + return QAccessible::EditableText; + case AccessibleRole::POPUP_MENU: + return QAccessible::PopupMenu; + case AccessibleRole::PUSH_BUTTON: + return QAccessible::Button; + case AccessibleRole::PROGRESS_BAR: + return QAccessible::ProgressBar; + case AccessibleRole::RADIO_BUTTON: + return QAccessible::RadioButton; + case AccessibleRole::RADIO_MENU_ITEM: + return QAccessible::MenuItem; + case AccessibleRole::ROW_HEADER: + return QAccessible::RowHeader; + case AccessibleRole::ROOT_PANE: + return QAccessible::Pane; + case AccessibleRole::SCROLL_BAR: + return QAccessible::ScrollBar; + case AccessibleRole::SCROLL_PANE: + return QAccessible::Pane; + case AccessibleRole::SHAPE: + return QAccessible::Graphic; + case AccessibleRole::SEPARATOR: + return QAccessible::Separator; + case AccessibleRole::SLIDER: + return QAccessible::Slider; + case AccessibleRole::SPIN_BOX: + return QAccessible::SpinBox; + case AccessibleRole::SPLIT_PANE: + return QAccessible::Pane; + case AccessibleRole::STATUS_BAR: + return QAccessible::StatusBar; + case AccessibleRole::TABLE: + return QAccessible::Table; + case AccessibleRole::TABLE_CELL: + return QAccessible::Cell; + case AccessibleRole::TEXT: + return QAccessible::EditableText; + case AccessibleRole::TEXT_FRAME: + return QAccessible::UserRole; + case AccessibleRole::TOGGLE_BUTTON: + return QAccessible::Button; + case AccessibleRole::TOOL_BAR: + return QAccessible::ToolBar; + case AccessibleRole::TOOL_TIP: + return QAccessible::ToolTip; + case AccessibleRole::TREE: + return QAccessible::Tree; + case AccessibleRole::VIEW_PORT: + return QAccessible::UserRole; + case AccessibleRole::BUTTON_DROPDOWN: + return QAccessible::ButtonDropDown; + case AccessibleRole::BUTTON_MENU: + return QAccessible::ButtonMenu; + case AccessibleRole::CAPTION: + return QAccessible::StaticText; + case AccessibleRole::CHART: + return QAccessible::Chart; + case AccessibleRole::EDIT_BAR: + return QAccessible::Equation; + case AccessibleRole::FORM: + return QAccessible::Form; + case AccessibleRole::IMAGE_MAP: + return QAccessible::Graphic; + case AccessibleRole::NOTE: + return QAccessible::Note; + case AccessibleRole::RULER: + return QAccessible::UserRole; + case AccessibleRole::SECTION: + return QAccessible::Section; + case AccessibleRole::TREE_ITEM: + return QAccessible::TreeItem; + case AccessibleRole::TREE_TABLE: + return QAccessible::Tree; + case AccessibleRole::COMMENT: + return QAccessible::Note; + case AccessibleRole::COMMENT_END: + return QAccessible::UserRole; + case AccessibleRole::DOCUMENT_PRESENTATION: + return QAccessible::Document; + case AccessibleRole::DOCUMENT_SPREADSHEET: + return QAccessible::Document; + case AccessibleRole::DOCUMENT_TEXT: + return QAccessible::Document; + case AccessibleRole::STATIC: + return QAccessible::StaticText; + case AccessibleRole::WINDOW: // top-level window without title bar + return QAccessible::Window; + } + + SAL_WARN("vcl.qt", "Unmapped role: " << getAccessibleContextImpl()->getAccessibleRole()); + return QAccessible::NoRole; +} + +namespace +{ +void lcl_addState(QAccessible::State* state, sal_Int64 nState) +{ + switch (nState) + { + case AccessibleStateType::INVALID: + state->invalid = true; + break; + case AccessibleStateType::ACTIVE: + state->active = true; + break; + case AccessibleStateType::ARMED: + // No match + break; + case AccessibleStateType::BUSY: + state->busy = true; + break; + case AccessibleStateType::CHECKABLE: + state->checkable = true; + break; + case AccessibleStateType::CHECKED: + state->checked = true; + break; + case AccessibleStateType::EDITABLE: + state->editable = true; + break; + case AccessibleStateType::ENABLED: + state->disabled = false; + break; + case AccessibleStateType::EXPANDABLE: + state->expandable = true; + break; + case AccessibleStateType::EXPANDED: + state->expanded = true; + break; + case AccessibleStateType::FOCUSABLE: + state->focusable = true; + break; + case AccessibleStateType::FOCUSED: + state->focused = true; + break; + case AccessibleStateType::HORIZONTAL: + // No match + break; + case AccessibleStateType::ICONIFIED: + // No match + break; + case AccessibleStateType::INDETERMINATE: + state->checkStateMixed = true; + break; + case AccessibleStateType::MANAGES_DESCENDANTS: + // No match + break; + case AccessibleStateType::MODAL: + state->modal = true; + break; + case AccessibleStateType::MOVEABLE: + state->movable = true; + break; + case AccessibleStateType::MULTI_LINE: + state->multiLine = true; + break; + case AccessibleStateType::OPAQUE: + // No match + break; + case AccessibleStateType::PRESSED: + state->pressed = true; + break; + case AccessibleStateType::RESIZABLE: + state->sizeable = true; + break; + case AccessibleStateType::SELECTABLE: + state->selectable = true; + break; + case AccessibleStateType::SELECTED: + state->selected = true; + break; + case AccessibleStateType::SENSITIVE: + // No match + break; + case AccessibleStateType::SHOWING: + // No match + break; + case AccessibleStateType::SINGLE_LINE: + // No match + break; + case AccessibleStateType::STALE: + // No match + break; + case AccessibleStateType::TRANSIENT: + // No match + break; + case AccessibleStateType::VERTICAL: + // No match + break; + case AccessibleStateType::VISIBLE: + state->invisible = false; + break; + case AccessibleStateType::DEFAULT: + // No match + break; + case AccessibleStateType::DEFUNC: + state->invalid = true; + break; + case AccessibleStateType::MULTI_SELECTABLE: + state->multiSelectable = true; + break; + default: + SAL_WARN("vcl.qt", "Unmapped state: " << nState); + break; + } +} +} + +QAccessible::State QtAccessibleWidget::state() const +{ + QAccessible::State state; + + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return state; + + sal_Int64 nStateSet(xAc->getAccessibleStateSet()); + + for (int i = 0; i < 63; ++i) + { + sal_Int64 nState = sal_Int64(1) << i; + if (nStateSet & nState) + lcl_addState(&state, nState); + } + + if (xAc->getAccessibleRole() == AccessibleRole::PASSWORD_TEXT) + state.passwordEdit = true; + + return state; +} + +QColor QtAccessibleWidget::foregroundColor() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QColor(); + + Reference<XAccessibleComponent> xAccessibleComponent(xAc, UNO_QUERY); + return toQColor(Color(ColorTransparency, xAccessibleComponent->getForeground())); +} + +QColor QtAccessibleWidget::backgroundColor() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QColor(); + + Reference<XAccessibleComponent> xAccessibleComponent(xAc, UNO_QUERY); + return toQColor(Color(ColorTransparency, xAccessibleComponent->getBackground())); +} + +void* QtAccessibleWidget::interface_cast(QAccessible::InterfaceType t) +{ + if (t == QAccessible::ActionInterface && accessibleProvidesInterface<XAccessibleAction>()) + return static_cast<QAccessibleActionInterface*>(this); + if (t == QAccessible::TextInterface && accessibleProvidesInterface<XAccessibleText>()) + return static_cast<QAccessibleTextInterface*>(this); + if (t == QAccessible::EditableTextInterface + && accessibleProvidesInterface<XAccessibleEditableText>()) + return static_cast<QAccessibleEditableTextInterface*>(this); + if (t == QAccessible::ValueInterface && accessibleProvidesInterface<XAccessibleValue>()) + return static_cast<QAccessibleValueInterface*>(this); + if (t == QAccessible::TableCellInterface) + { + // parent must be a table + Reference<XAccessibleTable> xTable = getAccessibleTableForParent(); + if (xTable.is()) + return static_cast<QAccessibleTableCellInterface*>(this); + } + if (t == QAccessible::TableInterface && accessibleProvidesInterface<XAccessibleTable>()) + return static_cast<QAccessibleTableInterface*>(this); +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + if (t == QAccessible::SelectionInterface && accessibleProvidesInterface<XAccessibleSelection>()) + return static_cast<QAccessibleSelectionInterface*>(this); +#endif + return nullptr; +} + +bool QtAccessibleWidget::isValid() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + return xAc.is(); +} + +QObject* QtAccessibleWidget::object() const { return m_pObject; } + +void QtAccessibleWidget::setText(QAccessible::Text /* t */, const QString& /* text */) {} + +QAccessibleInterface* QtAccessibleWidget::childAt(int x, int y) const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return nullptr; + + Reference<XAccessibleComponent> xAccessibleComponent(xAc, UNO_QUERY); + // convert from screen to local coordinates + QPoint aLocalCoords = QPoint(x, y) - rect().topLeft(); + return QAccessible::queryAccessibleInterface( + QtAccessibleRegistry::getQObject(xAccessibleComponent->getAccessibleAtPoint( + awt::Point(aLocalCoords.x(), aLocalCoords.y())))); +} + +QAccessibleInterface* QtAccessibleWidget::customFactory(const QString& classname, QObject* object) +{ + if (classname == QLatin1String("QtWidget") && object && object->isWidgetType()) + { + QtWidget* pWidget = static_cast<QtWidget*>(object); + vcl::Window* pWindow = pWidget->frame().GetWindow(); + + if (pWindow) + { + css::uno::Reference<XAccessible> xAcc = pWindow->GetAccessible(); + // insert into registry so the association between the XAccessible and the QtWidget + // is remembered rather than creating a different QtXAccessible when a QObject is needed later + QtAccessibleRegistry::insert(xAcc, object); + return new QtAccessibleWidget(xAcc, object); + } + } + if (classname == QLatin1String("QtXAccessible") && object) + { + QtXAccessible* pXAccessible = static_cast<QtXAccessible*>(object); + if (pXAccessible->m_xAccessible.is()) + { + QtAccessibleWidget* pRet = new QtAccessibleWidget(pXAccessible->m_xAccessible, object); + // clear the reference in the QtXAccessible, no longer needed now that the QtAccessibleWidget holds one + pXAccessible->m_xAccessible.clear(); + return pRet; + } + } + + return nullptr; +} + +// QAccessibleActionInterface +QStringList QtAccessibleWidget::actionNames() const +{ + QStringList actionNames; + Reference<XAccessibleAction> xAccessibleAction(getAccessibleContextImpl(), UNO_QUERY); + if (!xAccessibleAction.is()) + return actionNames; + + int count = xAccessibleAction->getAccessibleActionCount(); + for (int i = 0; i < count; i++) + { + OUString desc = xAccessibleAction->getAccessibleActionDescription(i); + actionNames.append(toQString(desc)); + } + return actionNames; +} + +void QtAccessibleWidget::doAction(const QString& actionName) +{ + Reference<XAccessibleAction> xAccessibleAction(getAccessibleContextImpl(), UNO_QUERY); + if (!xAccessibleAction.is()) + return; + + int index = actionNames().indexOf(actionName); + if (index == -1) + return; + xAccessibleAction->doAccessibleAction(index); +} + +QStringList QtAccessibleWidget::keyBindingsForAction(const QString& actionName) const +{ + QStringList keyBindings; + Reference<XAccessibleAction> xAccessibleAction(getAccessibleContextImpl(), UNO_QUERY); + if (!xAccessibleAction.is()) + return keyBindings; + + int index = actionNames().indexOf(actionName); + if (index == -1) + return keyBindings; + + Reference<XAccessibleKeyBinding> xKeyBinding + = xAccessibleAction->getAccessibleActionKeyBinding(index); + + if (!xKeyBinding.is()) + return keyBindings; + + int count = xKeyBinding->getAccessibleKeyBindingCount(); + for (int i = 0; i < count; i++) + { + Sequence<awt::KeyStroke> keyStroke = xKeyBinding->getAccessibleKeyBinding(i); + keyBindings.append(toQString(comphelper::GetkeyBindingStrByXkeyBinding(keyStroke))); + } + return keyBindings; +} + +// QAccessibleTextInterface +void QtAccessibleWidget::addSelection(int /* startOffset */, int /* endOffset */) +{ + SAL_INFO("vcl.qt", "Unsupported QAccessibleTextInterface::addSelection"); +} + +// Text attributes are returned in format specified in IAccessible2 spec, since that +// is what Qt handles: +// https://wiki.linuxfoundation.org/accessibility/iaccessible2/textattributes +QString QtAccessibleWidget::attributes(int offset, int* startOffset, int* endOffset) const +{ + if (startOffset == nullptr || endOffset == nullptr) + return QString(); + + *startOffset = -1; + *endOffset = -1; + + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (!xText.is()) + return QString(); + + // handle special values for offset the same way base class's QAccessibleTextWidget::attributes does + // (as defined in IAccessible 2: -1 -> length, -2 -> cursor position) + if (offset == -2) + offset = cursorPosition(); + + const int nTextLength = characterCount(); + if (offset == -1 || offset == nTextLength) + offset = nTextLength - 1; + + if (offset < 0 || offset > nTextLength) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::attributes called with invalid offset: " << offset); + return QString(); + } + + // Qt doesn't have the strict separation into text and object attributes, but also + // supports text-specific attributes that are object attributes according to the + // IAccessible2 spec. + sal_Int32 nStart = 0; + sal_Int32 nEnd = 0; + const OUString aRet = AccessibleTextAttributeHelper::GetIAccessible2TextAttributes( + xText, IA2AttributeType::TextAttributes | IA2AttributeType::ObjectAttributes, + static_cast<sal_Int32>(offset), nStart, nEnd); + *startOffset = static_cast<int>(nStart); + *endOffset = static_cast<int>(nEnd); + return toQString(aRet); +} + +int QtAccessibleWidget::characterCount() const +{ + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (xText.is()) + return xText->getCharacterCount(); + return 0; +} + +QRect QtAccessibleWidget::characterRect(int nOffset) const +{ + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (!xText.is()) + return QRect(); + + if (nOffset < 0 || nOffset > xText->getCharacterCount()) + { + SAL_WARN("vcl.qt", + "QtAccessibleWidget::characterRect called with invalid offset: " << nOffset); + return QRect(); + } + + const awt::Rectangle aBounds = xText->getCharacterBounds(nOffset); + const QRect aRect(aBounds.X, aBounds.Y, aBounds.Width, aBounds.Height); + // convert to screen coordinates + const QRect aScreenPos = rect(); + return aRect.translated(aScreenPos.x(), aScreenPos.y()); +} + +int QtAccessibleWidget::cursorPosition() const +{ + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (xText.is()) + return xText->getCaretPosition(); + return 0; +} + +int QtAccessibleWidget::offsetAtPoint(const QPoint& rPoint) const +{ + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (!xText.is()) + return -1; + + // convert from screen to local coordinates + QPoint aLocalCoords = rPoint - rect().topLeft(); + awt::Point aPoint(aLocalCoords.x(), aLocalCoords.y()); + return xText->getIndexAtPoint(aPoint); +} + +void QtAccessibleWidget::removeSelection(int /* selectionIndex */) +{ + SAL_INFO("vcl.qt", "Unsupported QAccessibleTextInterface::removeSelection"); +} + +void QtAccessibleWidget::scrollToSubstring(int startIndex, int endIndex) +{ + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (!xText.is()) + return; + + sal_Int32 nTextLength = xText->getCharacterCount(); + if (startIndex < 0 || startIndex > nTextLength || endIndex < 0 || endIndex > nTextLength) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::scrollToSubstring called with invalid offset."); + return; + } + + xText->scrollSubstringTo(startIndex, endIndex, AccessibleScrollType_SCROLL_ANYWHERE); +} + +void QtAccessibleWidget::selection(int selectionIndex, int* startOffset, int* endOffset) const +{ + if (!startOffset && !endOffset) + return; + + Reference<XAccessibleText> xText; + if (selectionIndex == 0) + xText = Reference<XAccessibleText>(getAccessibleContextImpl(), UNO_QUERY); + + if (startOffset) + *startOffset = xText.is() ? xText->getSelectionStart() : 0; + if (endOffset) + *endOffset = xText.is() ? xText->getSelectionEnd() : 0; +} + +int QtAccessibleWidget::selectionCount() const +{ + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (xText.is() && !xText->getSelectedText().isEmpty()) + return 1; // Only 1 selection supported atm + return 0; +} + +void QtAccessibleWidget::setCursorPosition(int position) +{ + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (!xText.is()) + return; + + if (position < 0 || position > xText->getCharacterCount()) + { + SAL_WARN("vcl.qt", + "QtAccessibleWidget::setCursorPosition called with invalid offset: " << position); + return; + } + + xText->setCaretPosition(position); +} + +void QtAccessibleWidget::setSelection(int /* selectionIndex */, int startOffset, int endOffset) +{ + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (!xText.is()) + return; + + sal_Int32 nTextLength = xText->getCharacterCount(); + if (startOffset < 0 || startOffset > nTextLength || endOffset < 0 || endOffset > nTextLength) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::setSelection called with invalid offset."); + return; + } + + xText->setSelection(startOffset, endOffset); +} + +QString QtAccessibleWidget::text(int startOffset, int endOffset) const +{ + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (!xText.is()) + return QString(); + + sal_Int32 nTextLength = xText->getCharacterCount(); + if (startOffset < 0 || startOffset > nTextLength || endOffset < 0 || endOffset > nTextLength) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::text called with invalid offset."); + return QString(); + } + + return toQString(xText->getTextRange(startOffset, endOffset)); +} + +QString QtAccessibleWidget::textAfterOffset(int nOffset, + QAccessible::TextBoundaryType eBoundaryType, + int* pStartOffset, int* pEndOffset) const +{ + if (pStartOffset == nullptr || pEndOffset == nullptr) + return QString(); + + *pStartOffset = -1; + *pEndOffset = -1; + + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (!xText.is()) + return QString(); + + const int nCharCount = characterCount(); + // -1 is special value for text length + if (nOffset == -1) + nOffset = nCharCount; + else if (nOffset < -1 || nOffset > nCharCount) + { + SAL_WARN("vcl.qt", + "QtAccessibleWidget::textAfterOffset called with invalid offset: " << nOffset); + return QString(); + } + + if (eBoundaryType == QAccessible::NoBoundary) + { + if (nOffset == nCharCount) + return QString(); + *pStartOffset = nOffset + 1; + *pEndOffset = nCharCount; + return text(nOffset + 1, nCharCount); + } + + sal_Int16 nUnoBoundaryType = lcl_matchQtTextBoundaryType(eBoundaryType); + assert(nUnoBoundaryType > 0); + const TextSegment aSegment = xText->getTextBehindIndex(nOffset, nUnoBoundaryType); + *pStartOffset = aSegment.SegmentStart; + *pEndOffset = aSegment.SegmentEnd; + return toQString(aSegment.SegmentText); +} + +QString QtAccessibleWidget::textAtOffset(int offset, QAccessible::TextBoundaryType boundaryType, + int* startOffset, int* endOffset) const +{ + if (startOffset == nullptr || endOffset == nullptr) + return QString(); + + const int nCharCount = characterCount(); + if (boundaryType == QAccessible::NoBoundary) + { + *startOffset = 0; + *endOffset = nCharCount; + return text(0, nCharCount); + } + + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (!xText.is()) + return QString(); + + sal_Int16 nUnoBoundaryType = lcl_matchQtTextBoundaryType(boundaryType); + assert(nUnoBoundaryType > 0); + + // special value of -1 for offset means text length + if (offset == -1) + offset = nCharCount; + + if (offset < 0 || offset > nCharCount) + { + SAL_WARN("vcl.qt", + "QtAccessibleWidget::textAtOffset called with invalid offset: " << offset); + return QString(); + } + + const TextSegment segment = xText->getTextAtIndex(offset, nUnoBoundaryType); + *startOffset = segment.SegmentStart; + *endOffset = segment.SegmentEnd; + return toQString(segment.SegmentText); +} + +QString QtAccessibleWidget::textBeforeOffset(int nOffset, + QAccessible::TextBoundaryType eBoundaryType, + int* pStartOffset, int* pEndOffset) const +{ + if (pStartOffset == nullptr || pEndOffset == nullptr) + return QString(); + + *pStartOffset = -1; + *pEndOffset = -1; + + Reference<XAccessibleText> xText(getAccessibleContextImpl(), UNO_QUERY); + if (!xText.is()) + return QString(); + + const int nCharCount = characterCount(); + // -1 is special value for text length + if (nOffset == -1) + nOffset = nCharCount; + else if (nOffset < -1 || nOffset > nCharCount) + { + SAL_WARN("vcl.qt", + "QtAccessibleWidget::textBeforeOffset called with invalid offset: " << nOffset); + return QString(); + } + + if (eBoundaryType == QAccessible::NoBoundary) + { + *pStartOffset = 0; + *pEndOffset = nOffset; + return text(0, nOffset); + } + + sal_Int16 nUnoBoundaryType = lcl_matchQtTextBoundaryType(eBoundaryType); + assert(nUnoBoundaryType > 0); + const TextSegment aSegment = xText->getTextBeforeIndex(nOffset, nUnoBoundaryType); + *pStartOffset = aSegment.SegmentStart; + *pEndOffset = aSegment.SegmentEnd; + return toQString(aSegment.SegmentText); +} + +// QAccessibleEditableTextInterface + +void QtAccessibleWidget::deleteText(int startOffset, int endOffset) +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return; + + Reference<XAccessibleEditableText> xEditableText(xAc, UNO_QUERY); + if (!xEditableText.is()) + return; + + sal_Int32 nTextLength = xEditableText->getCharacterCount(); + if (startOffset < 0 || startOffset > nTextLength || endOffset < 0 || endOffset > nTextLength) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::deleteText called with invalid offset."); + return; + } + + xEditableText->deleteText(startOffset, endOffset); +} + +void QtAccessibleWidget::insertText(int offset, const QString& text) +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return; + + Reference<XAccessibleEditableText> xEditableText(xAc, UNO_QUERY); + if (!xEditableText.is()) + return; + + if (offset < 0 || offset > xEditableText->getCharacterCount()) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::insertText called with invalid offset: " << offset); + return; + } + + xEditableText->insertText(toOUString(text), offset); +} + +void QtAccessibleWidget::replaceText(int startOffset, int endOffset, const QString& text) +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return; + + Reference<XAccessibleEditableText> xEditableText(xAc, UNO_QUERY); + if (!xEditableText.is()) + return; + + sal_Int32 nTextLength = xEditableText->getCharacterCount(); + if (startOffset < 0 || startOffset > nTextLength || endOffset < 0 || endOffset > nTextLength) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::replaceText called with invalid offset."); + return; + } + + xEditableText->replaceText(startOffset, endOffset, toOUString(text)); +} + +// QAccessibleValueInterface +QVariant QtAccessibleWidget::currentValue() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QVariant(); + + Reference<XAccessibleValue> xValue(xAc, UNO_QUERY); + if (!xValue.is()) + return QVariant(); + double aDouble = 0; + xValue->getCurrentValue() >>= aDouble; + return QVariant(aDouble); +} + +QVariant QtAccessibleWidget::maximumValue() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QVariant(); + + Reference<XAccessibleValue> xValue(xAc, UNO_QUERY); + if (!xValue.is()) + return QVariant(); + double aDouble = 0; + xValue->getMaximumValue() >>= aDouble; + return QVariant(aDouble); +} + +QVariant QtAccessibleWidget::minimumStepSize() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QVariant(); + + Reference<XAccessibleValue> xValue(xAc, UNO_QUERY); + if (!xValue.is()) + return QVariant(); + double dMinStep = 0; + xValue->getMinimumIncrement() >>= dMinStep; + return QVariant(dMinStep); +} + +QVariant QtAccessibleWidget::minimumValue() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QVariant(); + + Reference<XAccessibleValue> xValue(xAc, UNO_QUERY); + if (!xValue.is()) + return QVariant(); + double aDouble = 0; + xValue->getMinimumValue() >>= aDouble; + return QVariant(aDouble); +} + +void QtAccessibleWidget::setCurrentValue(const QVariant& value) +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return; + + Reference<XAccessibleValue> xValue(xAc, UNO_QUERY); + if (!xValue.is()) + return; + + // Different types of numerical values for XAccessibleValue are possible. + // If current value has an integer type, also use that for the new value, to make + // sure underlying implementations expecting that can handle the value properly. + const Any aCurrentValue = xValue->getCurrentValue(); + if (aCurrentValue.getValueTypeClass() == css::uno::TypeClass::TypeClass_LONG) + xValue->setCurrentValue(Any(static_cast<sal_Int32>(value.toInt()))); + else if (aCurrentValue.getValueTypeClass() == css::uno::TypeClass::TypeClass_HYPER) + xValue->setCurrentValue(Any(static_cast<sal_Int64>(value.toLongLong()))); + else + xValue->setCurrentValue(Any(value.toDouble())); +} + +// QAccessibleTableInterface +QAccessibleInterface* QtAccessibleWidget::caption() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return nullptr; + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return nullptr; + return QAccessible::queryAccessibleInterface( + QtAccessibleRegistry::getQObject(xTable->getAccessibleCaption())); +} + +QAccessibleInterface* QtAccessibleWidget::cellAt(int row, int column) const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return nullptr; + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return nullptr; + + if (row < 0 || row >= xTable->getAccessibleRowCount() || column < 0 + || column >= xTable->getAccessibleColumnCount()) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::cellAt called with invalid row/column index (" + << row << ", " << column << ")"); + return nullptr; + } + + return QAccessible::queryAccessibleInterface( + QtAccessibleRegistry::getQObject(xTable->getAccessibleCellAt(row, column))); +} + +int QtAccessibleWidget::columnCount() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return 0; + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return 0; + return xTable->getAccessibleColumnCount(); +} + +QString QtAccessibleWidget::columnDescription(int column) const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QString(); + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return QString(); + return toQString(xTable->getAccessibleColumnDescription(column)); +} + +bool QtAccessibleWidget::isColumnSelected(int nColumn) const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return false; + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return false; + + if (nColumn < 0 || nColumn >= xTable->getAccessibleColumnCount()) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::isColumnSelected called with invalid column index " + << nColumn); + return false; + } + + return xTable->isAccessibleColumnSelected(nColumn); +} + +bool QtAccessibleWidget::isRowSelected(int nRow) const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return false; + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return false; + + if (nRow < 0 || nRow >= xTable->getAccessibleRowCount()) + { + SAL_WARN("vcl.qt", + "QtAccessibleWidget::isRowSelected called with invalid row index " << nRow); + return false; + } + + return xTable->isAccessibleRowSelected(nRow); +} + +void QtAccessibleWidget::modelChange(QAccessibleTableModelChangeEvent*) {} + +int QtAccessibleWidget::rowCount() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return 0; + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return 0; + return xTable->getAccessibleRowCount(); +} + +QString QtAccessibleWidget::rowDescription(int row) const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QString(); + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return QString(); + return toQString(xTable->getAccessibleRowDescription(row)); +} + +bool QtAccessibleWidget::selectColumn(int column) +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return false; + + if (column < 0 || column >= columnCount()) + { + SAL_WARN("vcl.qt", + "QtAccessibleWidget::selectColumn called with invalid column index " << column); + return false; + } + + Reference<XAccessibleTableSelection> xTableSelection(xAc, UNO_QUERY); + if (!xTableSelection.is()) + return false; + return xTableSelection->selectColumn(column); +} + +bool QtAccessibleWidget::selectRow(int row) +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return false; + + if (row < 0 || row >= rowCount()) + { + SAL_WARN("vcl.qt", "QtAccessibleWidget::selectRow called with invalid row index " << row); + return false; + } + + Reference<XAccessibleTableSelection> xTableSelection(xAc, UNO_QUERY); + if (!xTableSelection.is()) + return false; + return xTableSelection->selectRow(row); +} + +int QtAccessibleWidget::selectedCellCount() const { return selectedItemCount(); } + +QList<QAccessibleInterface*> QtAccessibleWidget::selectedCells() const { return selectedItems(); } + +int QtAccessibleWidget::selectedColumnCount() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return 0; + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return 0; + return xTable->getSelectedAccessibleColumns().getLength(); +} + +QList<int> QtAccessibleWidget::selectedColumns() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QList<int>(); + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return QList<int>(); + return toQList(xTable->getSelectedAccessibleColumns()); +} + +int QtAccessibleWidget::selectedRowCount() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return 0; + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return 0; + return xTable->getSelectedAccessibleRows().getLength(); +} + +QList<int> QtAccessibleWidget::selectedRows() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return QList<int>(); + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return QList<int>(); + return toQList(xTable->getSelectedAccessibleRows()); +} + +QAccessibleInterface* QtAccessibleWidget::summary() const +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return nullptr; + + Reference<XAccessibleTable> xTable(xAc, UNO_QUERY); + if (!xTable.is()) + return nullptr; + return QAccessible::queryAccessibleInterface( + QtAccessibleRegistry::getQObject(xTable->getAccessibleSummary())); +} + +bool QtAccessibleWidget::unselectColumn(int column) +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return false; + + Reference<XAccessibleTableSelection> xTableSelection(xAc, UNO_QUERY); + if (!xTableSelection.is()) + return false; + return xTableSelection->unselectColumn(column); +} + +bool QtAccessibleWidget::unselectRow(int row) +{ + Reference<XAccessibleContext> xAc = getAccessibleContextImpl(); + if (!xAc.is()) + return false; + + Reference<XAccessibleTableSelection> xTableSelection(xAc, UNO_QUERY); + if (!xTableSelection.is()) + return false; + return xTableSelection->unselectRow(row); +} + +// QAccessibleTableCellInterface +QList<QAccessibleInterface*> QtAccessibleWidget::columnHeaderCells() const +{ + Reference<XAccessibleTable> xTable = getAccessibleTableForParent(); + if (!xTable.is()) + return QList<QAccessibleInterface*>(); + + Reference<XAccessibleTable> xHeaders = xTable->getAccessibleColumnHeaders(); + if (!xHeaders.is()) + return QList<QAccessibleInterface*>(); + + const sal_Int32 nCol = columnIndex(); + QList<QAccessibleInterface*> aHeaderCells; + for (sal_Int32 nRow = 0; nRow < xHeaders->getAccessibleRowCount(); nRow++) + { + Reference<XAccessible> xCell = xHeaders->getAccessibleCellAt(nRow, nCol); + QAccessibleInterface* pInterface + = QAccessible::queryAccessibleInterface(QtAccessibleRegistry::getQObject(xCell)); + aHeaderCells.push_back(pInterface); + } + return aHeaderCells; +} + +int QtAccessibleWidget::columnIndex() const +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return -1; + + Reference<XAccessibleTable> xTable = getAccessibleTableForParent(); + if (!xTable.is()) + return -1; + + const sal_Int64 nIndexInParent = xAcc->getAccessibleIndexInParent(); + return xTable->getAccessibleColumn(nIndexInParent); +} + +bool QtAccessibleWidget::isSelected() const +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return false; + + Reference<XAccessibleTable> xTable = getAccessibleTableForParent(); + if (!xTable.is()) + return false; + + const sal_Int32 nColumn = columnIndex(); + const sal_Int32 nRow = rowIndex(); + return xTable->isAccessibleSelected(nRow, nColumn); +} + +int QtAccessibleWidget::columnExtent() const +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return -1; + + Reference<XAccessibleTable> xTable = getAccessibleTableForParent(); + if (!xTable.is()) + return -1; + + const sal_Int32 nColumn = columnIndex(); + const sal_Int32 nRow = rowIndex(); + return xTable->getAccessibleColumnExtentAt(nRow, nColumn); +} + +QList<QAccessibleInterface*> QtAccessibleWidget::rowHeaderCells() const +{ + Reference<XAccessibleTable> xTable = getAccessibleTableForParent(); + if (!xTable.is()) + return QList<QAccessibleInterface*>(); + + Reference<XAccessibleTable> xHeaders = xTable->getAccessibleRowHeaders(); + if (!xHeaders.is()) + return QList<QAccessibleInterface*>(); + + const sal_Int32 nRow = rowIndex(); + QList<QAccessibleInterface*> aHeaderCells; + for (sal_Int32 nCol = 0; nCol < xHeaders->getAccessibleColumnCount(); nCol++) + { + Reference<XAccessible> xCell = xHeaders->getAccessibleCellAt(nRow, nCol); + QAccessibleInterface* pInterface + = QAccessible::queryAccessibleInterface(QtAccessibleRegistry::getQObject(xCell)); + aHeaderCells.push_back(pInterface); + } + return aHeaderCells; +} + +int QtAccessibleWidget::rowExtent() const +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return -1; + + Reference<XAccessibleTable> xTable = getAccessibleTableForParent(); + if (!xTable.is()) + return -1; + + const sal_Int32 nColumn = columnIndex(); + const sal_Int32 nRow = rowIndex(); + return xTable->getAccessibleRowExtentAt(nRow, nColumn); +} + +int QtAccessibleWidget::rowIndex() const +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return -1; + + Reference<XAccessibleTable> xTable = getAccessibleTableForParent(); + if (!xTable.is()) + return -1; + + const sal_Int64 nIndexInParent = xAcc->getAccessibleIndexInParent(); + return xTable->getAccessibleRow(nIndexInParent); +} + +QAccessibleInterface* QtAccessibleWidget::table() const +{ + Reference<XAccessibleTable> xTable = getAccessibleTableForParent(); + if (!xTable.is()) + return nullptr; + + Reference<XAccessible> xTableAcc(xTable, UNO_QUERY); + if (!xTableAcc.is()) + return nullptr; + + return QAccessible::queryAccessibleInterface(QtAccessibleRegistry::getQObject(xTableAcc)); +} + +// QAccessibleSelectionInterface +int QtAccessibleWidget::selectedItemCount() const +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return 0; + + Reference<XAccessibleSelection> xSelection(xAcc, UNO_QUERY); + if (!xSelection.is()) + return 0; + + sal_Int64 nSelected = xSelection->getSelectedAccessibleChildCount(); + if (nSelected > std::numeric_limits<int>::max()) + { + SAL_WARN("vcl.qt", + "QtAccessibleWidget::selectedItemCount: Cell count exceeds maximum int value, " + "using max int."); + nSelected = std::numeric_limits<int>::max(); + } + return nSelected; +} + +QList<QAccessibleInterface*> QtAccessibleWidget::selectedItems() const +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return QList<QAccessibleInterface*>(); + + Reference<XAccessibleSelection> xSelection(xAcc, UNO_QUERY); + if (!xSelection.is()) + return QList<QAccessibleInterface*>(); + + QList<QAccessibleInterface*> aSelectedItems; + sal_Int64 nSelected = xSelection->getSelectedAccessibleChildCount(); + if (nSelected > std::numeric_limits<int>::max()) + { + SAL_WARN("vcl.qt", + "QtAccessibleWidget::selectedItems: Cell count exceeds maximum int value, " + "using max int."); + nSelected = std::numeric_limits<int>::max(); + } + for (sal_Int64 i = 0; i < nSelected; i++) + { + Reference<XAccessible> xChild = xSelection->getSelectedAccessibleChild(i); + QAccessibleInterface* pInterface + = QAccessible::queryAccessibleInterface(QtAccessibleRegistry::getQObject(xChild)); + aSelectedItems.push_back(pInterface); + } + return aSelectedItems; +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +QAccessibleInterface* QtAccessibleWidget::selectedItem(int nSelectionIndex) const +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return nullptr; + + Reference<XAccessibleSelection> xSelection(xAcc, UNO_QUERY); + if (!xSelection.is()) + return nullptr; + + if (nSelectionIndex < 0 || nSelectionIndex >= xSelection->getSelectedAccessibleChildCount()) + { + SAL_WARN("vcl.qt", + "QtAccessibleWidget::selectedItem called with invalid index: " << nSelectionIndex); + return nullptr; + } + + Reference<XAccessible> xChild = xSelection->getSelectedAccessibleChild(nSelectionIndex); + if (!xChild) + return nullptr; + + return QAccessible::queryAccessibleInterface(QtAccessibleRegistry::getQObject(xChild)); +} + +bool QtAccessibleWidget::isSelected(QAccessibleInterface* pItem) const +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return false; + + Reference<XAccessibleSelection> xSelection(xAcc, UNO_QUERY); + if (!xSelection.is()) + return false; + + int nChildIndex = indexOfChild(pItem); + if (nChildIndex < 0) + return false; + + return xSelection->isAccessibleChildSelected(nChildIndex); +} + +bool QtAccessibleWidget::select(QAccessibleInterface* pItem) +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return false; + + Reference<XAccessibleSelection> xSelection(xAcc, UNO_QUERY); + if (!xSelection.is()) + return false; + + int nChildIndex = indexOfChild(pItem); + if (nChildIndex < 0) + return false; + + xSelection->selectAccessibleChild(nChildIndex); + return true; +} + +bool QtAccessibleWidget::unselect(QAccessibleInterface* pItem) +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return false; + + Reference<XAccessibleSelection> xSelection(xAcc, UNO_QUERY); + if (!xSelection.is()) + return false; + + int nChildIndex = indexOfChild(pItem); + if (nChildIndex < 0) + return false; + + xSelection->deselectAccessibleChild(nChildIndex); + return true; +} + +bool QtAccessibleWidget::selectAll() +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return false; + + Reference<XAccessibleSelection> xSelection(xAcc, UNO_QUERY); + if (!xSelection.is()) + return false; + + xSelection->selectAllAccessibleChildren(); + return true; +} + +bool QtAccessibleWidget::clear() +{ + Reference<XAccessibleContext> xAcc = getAccessibleContextImpl(); + if (!xAcc.is()) + return false; + + Reference<XAccessibleSelection> xSelection(xAcc, UNO_QUERY); + if (!xSelection.is()) + return false; + + xSelection->clearAccessibleSelection(); + return true; +} +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtBitmap.cxx b/vcl/qt5/QtBitmap.cxx new file mode 100644 index 0000000000..dd83c57c23 --- /dev/null +++ b/vcl/qt5/QtBitmap.cxx @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtBitmap.hxx> +#include <QtTools.hxx> +#include <QtGraphics.hxx> + +#include <QtGui/QImage> +#include <QtCore/QVector> +#include <QtGui/QColor> + +#include <o3tl/safeint.hxx> +#include <sal/log.hxx> +#include <tools/helpers.hxx> + +QtBitmap::QtBitmap() {} + +QtBitmap::QtBitmap(const QImage& rImage) { m_pImage.reset(new QImage(rImage)); } + +bool QtBitmap::Create(const Size& rSize, vcl::PixelFormat ePixelFormat, const BitmapPalette& rPal) +{ + if (ePixelFormat == vcl::PixelFormat::INVALID) + return false; + + if (ePixelFormat == vcl::PixelFormat::N8_BPP) + assert(256 >= rPal.GetEntryCount()); + + m_pImage.reset(new QImage(toQSize(rSize), getBitFormat(ePixelFormat))); + m_pImage->fill(Qt::transparent); + m_aPalette = rPal; + + auto count = rPal.GetEntryCount(); + if (count && m_pImage) + { + QVector<QRgb> aColorTable(count); + for (unsigned i = 0; i < count; ++i) + aColorTable[i] = qRgb(rPal[i].GetRed(), rPal[i].GetGreen(), rPal[i].GetBlue()); + m_pImage->setColorTable(std::move(aColorTable)); + } + return true; +} + +bool QtBitmap::Create(const SalBitmap& rSalBmp) +{ + const QtBitmap* pBitmap = static_cast<const QtBitmap*>(&rSalBmp); + m_pImage.reset(new QImage(*pBitmap->m_pImage)); + m_aPalette = pBitmap->m_aPalette; + return true; +} + +bool QtBitmap::Create(const SalBitmap& rSalBmp, SalGraphics* pSalGraphics) +{ + const QtBitmap* pBitmap = static_cast<const QtBitmap*>(&rSalBmp); + QtGraphics* pGraphics = static_cast<QtGraphics*>(pSalGraphics); + QImage* pImage = pGraphics->getQImage(); + m_pImage.reset(new QImage(pBitmap->m_pImage->convertToFormat(pImage->format()))); + return true; +} + +bool QtBitmap::Create(const SalBitmap& rSalBmp, vcl::PixelFormat eNewPixelFormat) +{ + if (eNewPixelFormat == vcl::PixelFormat::INVALID) + return false; + const QtBitmap* pBitmap = static_cast<const QtBitmap*>(&rSalBmp); + m_pImage.reset(new QImage(pBitmap->m_pImage->convertToFormat(getBitFormat(eNewPixelFormat)))); + return true; +} + +bool QtBitmap::Create(const css::uno::Reference<css::rendering::XBitmapCanvas>& /*rBitmapCanvas*/, + Size& /*rSize*/, bool /*bMask*/) +{ + return false; +} + +void QtBitmap::Destroy() { m_pImage.reset(); } + +Size QtBitmap::GetSize() const +{ + if (m_pImage) + return toSize(m_pImage->size()); + return Size(); +} + +sal_uInt16 QtBitmap::GetBitCount() const +{ + if (m_pImage) + return getFormatBits(m_pImage->format()); + return 0; +} + +BitmapBuffer* QtBitmap::AcquireBuffer(BitmapAccessMode /*nMode*/) +{ + static const BitmapPalette aEmptyPalette; + + if (!m_pImage) + return nullptr; + + BitmapBuffer* pBuffer = new BitmapBuffer; + + pBuffer->mnWidth = m_pImage->width(); + pBuffer->mnHeight = m_pImage->height(); + pBuffer->mnBitCount = getFormatBits(m_pImage->format()); + pBuffer->mpBits = m_pImage->bits(); + pBuffer->mnScanlineSize = m_pImage->bytesPerLine(); + + switch (pBuffer->mnBitCount) + { + case 1: + pBuffer->mnFormat = ScanlineFormat::N1BitMsbPal | ScanlineFormat::TopDown; + pBuffer->maPalette = m_aPalette; + break; + case 8: + pBuffer->mnFormat = ScanlineFormat::N8BitPal | ScanlineFormat::TopDown; + pBuffer->maPalette = m_aPalette; + break; + case 24: + pBuffer->mnFormat = ScanlineFormat::N24BitTcRgb | ScanlineFormat::TopDown; + pBuffer->maPalette = aEmptyPalette; + break; + case 32: + { +#ifdef OSL_BIGENDIAN + pBuffer->mnFormat = ScanlineFormat::N32BitTcArgb | ScanlineFormat::TopDown; +#else + pBuffer->mnFormat = ScanlineFormat::N32BitTcBgra | ScanlineFormat::TopDown; +#endif + pBuffer->maPalette = aEmptyPalette; + break; + } + default: + assert(false); + } + + return pBuffer; +} + +void QtBitmap::ReleaseBuffer(BitmapBuffer* pBuffer, BitmapAccessMode nMode) +{ + m_aPalette = pBuffer->maPalette; + auto count = m_aPalette.GetEntryCount(); + if (pBuffer->mnBitCount != 4 && count) + { + QVector<QRgb> aColorTable(count); + for (unsigned i = 0; i < count; ++i) + aColorTable[i] + = qRgb(m_aPalette[i].GetRed(), m_aPalette[i].GetGreen(), m_aPalette[i].GetBlue()); + m_pImage->setColorTable(std::move(aColorTable)); + } + delete pBuffer; + if (nMode == BitmapAccessMode::Write) + InvalidateChecksum(); +} + +bool QtBitmap::GetSystemData(BitmapSystemData& /*rData*/) { return false; } + +bool QtBitmap::ScalingSupported() const { return false; } + +bool QtBitmap::Scale(const double& /*rScaleX*/, const double& /*rScaleY*/, + BmpScaleFlag /*nScaleFlag*/) +{ + return false; +} + +bool QtBitmap::Replace(const Color& /*rSearchColor*/, const Color& /*rReplaceColor*/, + sal_uInt8 /*nTol*/) +{ + return false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtClipboard.cxx b/vcl/qt5/QtClipboard.cxx new file mode 100644 index 0000000000..e9eb476fb2 --- /dev/null +++ b/vcl/qt5/QtClipboard.cxx @@ -0,0 +1,259 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + +#include <QtClipboard.hxx> +#include <QtClipboard.moc> + +#include <cppuhelper/supportsservice.hxx> +#include <sal/log.hxx> + +#include <QtWidgets/QApplication> + +#include <QtInstance.hxx> +#include <QtTransferable.hxx> +#include <QtTools.hxx> + +#include <cassert> +#include <map> +#include <utility> + +QtClipboard::QtClipboard(OUString aModeString, const QClipboard::Mode aMode) + : cppu::WeakComponentImplHelper<css::datatransfer::clipboard::XSystemClipboard, + css::datatransfer::clipboard::XFlushableClipboard, + XServiceInfo>(m_aMutex) + , m_aClipboardName(std::move(aModeString)) + , m_aClipboardMode(aMode) + , m_bOwnClipboardChange(false) + , m_bDoClear(false) +{ + assert(isSupported(m_aClipboardMode)); + // DirectConnection guarantees the changed slot runs in the same thread as the QClipboard + connect(QApplication::clipboard(), &QClipboard::changed, this, &QtClipboard::handleChanged, + Qt::DirectConnection); + + // explicitly queue an event, so we can eventually ignore it + connect(this, &QtClipboard::clearClipboard, this, &QtClipboard::handleClearClipboard, + Qt::QueuedConnection); +} + +css::uno::Reference<css::uno::XInterface> QtClipboard::create(const OUString& aModeString) +{ + static const std::map<OUString, QClipboard::Mode> aNameToClipboardMap + = { { "CLIPBOARD", QClipboard::Clipboard }, { "PRIMARY", QClipboard::Selection } }; + + assert(QApplication::clipboard()->thread() == qApp->thread()); + + auto iter = aNameToClipboardMap.find(aModeString); + if (iter != aNameToClipboardMap.end() && isSupported(iter->second)) + return cppu::getXWeak(new QtClipboard(aModeString, iter->second)); + SAL_WARN("vcl.qt", "Ignoring unrecognized clipboard type: '" << aModeString << "'"); + return css::uno::Reference<css::uno::XInterface>(); +} + +void QtClipboard::flushClipboard() +{ + auto* pSalInst(GetQtInstance()); + SolarMutexGuard g; + pSalInst->RunInMainThread([this]() { + if (!isOwner(m_aClipboardMode)) + return; + + QClipboard* pClipboard = QApplication::clipboard(); + const QtMimeData* pQtMimeData + = dynamic_cast<const QtMimeData*>(pClipboard->mimeData(m_aClipboardMode)); + assert(pQtMimeData); + + QMimeData* pMimeCopy = nullptr; + if (pQtMimeData && pQtMimeData->deepCopy(&pMimeCopy)) + { + m_bOwnClipboardChange = true; + pClipboard->setMimeData(pMimeCopy, m_aClipboardMode); + m_bOwnClipboardChange = false; + } + }); +} + +css::uno::Reference<css::datatransfer::XTransferable> QtClipboard::getContents() +{ +#if defined(EMSCRIPTEN) + static QMimeData aMimeData; +#endif + osl::MutexGuard aGuard(m_aMutex); + + // if we're the owner, we might have the XTransferable from setContents. but + // maybe a non-LO clipboard change from within LO, like some C'n'P in the + // QFileDialog, might have invalidated m_aContents, so we need to check it too. + if (isOwner(m_aClipboardMode) && m_aContents.is()) + return m_aContents; + + // check if we can still use the shared QtClipboardTransferable + const QMimeData* pMimeData = QApplication::clipboard()->mimeData(m_aClipboardMode); +#if defined(EMSCRIPTEN) + if (!pMimeData) + pMimeData = &aMimeData; +#endif + if (m_aContents.is()) + { + const auto* pTrans = dynamic_cast<QtClipboardTransferable*>(m_aContents.get()); + assert(pTrans); + if (pTrans && pTrans->mimeData() == pMimeData) + return m_aContents; + } + + m_aContents = new QtClipboardTransferable(m_aClipboardMode, pMimeData); + return m_aContents; +} + +void QtClipboard::handleClearClipboard() +{ + if (!m_bDoClear) + return; + QApplication::clipboard()->clear(m_aClipboardMode); +} + +void QtClipboard::setContents( + const css::uno::Reference<css::datatransfer::XTransferable>& xTrans, + const css::uno::Reference<css::datatransfer::clipboard::XClipboardOwner>& xClipboardOwner) +{ + // it's actually possible to get a non-empty xTrans and an empty xClipboardOwner! + osl::ClearableMutexGuard aGuard(m_aMutex); + + css::uno::Reference<css::datatransfer::clipboard::XClipboardOwner> xOldOwner(m_aOwner); + css::uno::Reference<css::datatransfer::XTransferable> xOldContents(m_aContents); + m_aContents = xTrans; + m_aOwner = xClipboardOwner; + + m_bDoClear = !m_aContents.is(); + if (!m_bDoClear) + { + m_bOwnClipboardChange = true; + QApplication::clipboard()->setMimeData(new QtMimeData(m_aContents), m_aClipboardMode); + m_bOwnClipboardChange = false; + } + else + { + assert(!m_aOwner.is()); + Q_EMIT clearClipboard(); + } + + aGuard.clear(); + + // we have to notify only an owner change, since handleChanged can't + // access the previous owner anymore and can just handle lost ownership. + if (xOldOwner.is() && xOldOwner != xClipboardOwner) + xOldOwner->lostOwnership(this, xOldContents); +} + +void QtClipboard::handleChanged(QClipboard::Mode aMode) +{ + if (aMode != m_aClipboardMode) + return; + + osl::ClearableMutexGuard aGuard(m_aMutex); + + // QtWayland will send a second change notification (seemingly without any + // trigger). And any C'n'P operation in the Qt file picker emits a signal, + // with LO still holding the clipboard ownership, but internally having lost + // it. So ignore any signal, which still delivers the internal QtMimeData + // as the clipboard content and is no "advertised" change. + if (!m_bOwnClipboardChange && isOwner(aMode) + && dynamic_cast<const QtMimeData*>(QApplication::clipboard()->mimeData(aMode))) + return; + + css::uno::Reference<css::datatransfer::clipboard::XClipboardOwner> xOldOwner(m_aOwner); + css::uno::Reference<css::datatransfer::XTransferable> xOldContents(m_aContents); + // ownership change from LO POV is handled in setContents + if (!m_bOwnClipboardChange) + { + m_aContents.clear(); + m_aOwner.clear(); + } + + std::vector<css::uno::Reference<css::datatransfer::clipboard::XClipboardListener>> aListeners( + m_aListeners); + css::datatransfer::clipboard::ClipboardEvent aEv; + aEv.Contents = getContents(); + + aGuard.clear(); + + if (!m_bOwnClipboardChange && xOldOwner.is()) + xOldOwner->lostOwnership(this, xOldContents); + for (auto const& listener : aListeners) + listener->changedContents(aEv); +} + +OUString QtClipboard::getImplementationName() { return "com.sun.star.datatransfer.QtClipboard"; } + +css::uno::Sequence<OUString> QtClipboard::getSupportedServiceNames() +{ + return { "com.sun.star.datatransfer.clipboard.SystemClipboard" }; +} + +sal_Bool QtClipboard::supportsService(const OUString& ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +OUString QtClipboard::getName() { return m_aClipboardName; } + +sal_Int8 QtClipboard::getRenderingCapabilities() { return 0; } + +void QtClipboard::addClipboardListener( + const css::uno::Reference<css::datatransfer::clipboard::XClipboardListener>& listener) +{ + osl::MutexGuard aGuard(m_aMutex); + m_aListeners.push_back(listener); +} + +void QtClipboard::removeClipboardListener( + const css::uno::Reference<css::datatransfer::clipboard::XClipboardListener>& listener) +{ + osl::MutexGuard aGuard(m_aMutex); + std::erase(m_aListeners, listener); +} + +bool QtClipboard::isSupported(const QClipboard::Mode aMode) +{ + const QClipboard* pClipboard = QApplication::clipboard(); + switch (aMode) + { + case QClipboard::Selection: + return pClipboard->supportsSelection(); + + case QClipboard::FindBuffer: + return pClipboard->supportsFindBuffer(); + + case QClipboard::Clipboard: + return true; + } + return false; +} + +bool QtClipboard::isOwner(const QClipboard::Mode aMode) +{ + if (!isSupported(aMode)) + return false; + + const QClipboard* pClipboard = QApplication::clipboard(); + switch (aMode) + { + case QClipboard::Selection: + return pClipboard->ownsSelection(); + + case QClipboard::FindBuffer: + return pClipboard->ownsFindBuffer(); + + case QClipboard::Clipboard: + return pClipboard->ownsClipboard(); + } + return false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtData.cxx b/vcl/qt5/QtData.cxx new file mode 100644 index 0000000000..cc2883ae80 --- /dev/null +++ b/vcl/qt5/QtData.cxx @@ -0,0 +1,227 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtData.hxx> + +#include <QtGui/QBitmap> +#include <QtGui/QCursor> +#include <QtWidgets/QApplication> +#include <QtWidgets/QStyle> + +#include <i18nlangtag/languagetag.hxx> +#include <sal/log.hxx> +#include <tools/stream.hxx> +#include <vcl/ImageTree.hxx> + +#include <bitmaps.hlst> +#include <cursor_hotspots.hxx> +#include <unx/glyphcache.hxx> + +QtData::QtData() + : GenericUnixSalData() +{ + ImplSVData* pSVData = ImplGetSVData(); + + pSVData->maNWFData.mbDockingAreaSeparateTB = true; + pSVData->maNWFData.mbFlatMenu = true; + pSVData->maNWFData.mbRolloverMenubar = true; + pSVData->maNWFData.mbNoFocusRects = true; + pSVData->maNWFData.mbNoFocusRectsForFlatButtons = true; + + QStyle* style = QApplication::style(); + pSVData->maNWFData.mnMenuFormatBorderX = style->pixelMetric(QStyle::PM_MenuPanelWidth) + + style->pixelMetric(QStyle::PM_MenuHMargin); + pSVData->maNWFData.mnMenuFormatBorderY = style->pixelMetric(QStyle::PM_MenuPanelWidth) + + style->pixelMetric(QStyle::PM_MenuVMargin); +} + +// outline dtor b/c of FreetypeManager incomplete type +QtData::~QtData() {} + +static QCursor* getQCursorFromIconTheme(const OUString& rIconName, int nXHot, int nYHot) +{ + const OUString sIconTheme = Application::GetSettings().GetStyleSettings().DetermineIconTheme(); + const OUString sUILang = Application::GetSettings().GetUILanguageTag().getBcp47(); + auto xMemStream = ImageTree::get().getImageStream(rIconName, sIconTheme, sUILang); + if (!xMemStream) + return nullptr; + auto nLength = xMemStream->TellEnd(); + if (!nLength) + { + SAL_WARN("vcl.qt", "Cannot load cursor pixmap from empty stream."); + return nullptr; + } + + const unsigned char* pData = static_cast<const unsigned char*>(xMemStream->GetData()); + QPixmap aPixmap; + aPixmap.loadFromData(pData, nLength); + return new QCursor(aPixmap, nXHot, nYHot); +} + +#define MAKE_CURSOR(vcl_name, name, icon_name) \ + case vcl_name: \ + pCursor = getQCursorFromIconTheme(icon_name, name##curs_x_hot, name##curs_y_hot); \ + break + +#define MAP_BUILTIN(vcl_name, qt_enum) \ + case vcl_name: \ + pCursor = new QCursor(qt_enum); \ + break + +QCursor& QtData::getCursor(PointerStyle ePointerStyle) +{ + if (!m_aCursors[ePointerStyle]) + { + QCursor* pCursor = nullptr; + + switch (ePointerStyle) + { + MAP_BUILTIN(PointerStyle::Arrow, Qt::ArrowCursor); + MAP_BUILTIN(PointerStyle::Text, Qt::IBeamCursor); + MAP_BUILTIN(PointerStyle::Help, Qt::WhatsThisCursor); + MAP_BUILTIN(PointerStyle::Cross, Qt::CrossCursor); + MAP_BUILTIN(PointerStyle::Wait, Qt::WaitCursor); + MAP_BUILTIN(PointerStyle::NSize, Qt::SizeVerCursor); + MAP_BUILTIN(PointerStyle::SSize, Qt::SizeVerCursor); + MAP_BUILTIN(PointerStyle::WSize, Qt::SizeHorCursor); + MAP_BUILTIN(PointerStyle::ESize, Qt::SizeHorCursor); + + MAP_BUILTIN(PointerStyle::NWSize, Qt::SizeFDiagCursor); + MAP_BUILTIN(PointerStyle::NESize, Qt::SizeBDiagCursor); + MAP_BUILTIN(PointerStyle::SWSize, Qt::SizeBDiagCursor); + MAP_BUILTIN(PointerStyle::SESize, Qt::SizeFDiagCursor); + MAP_BUILTIN(PointerStyle::WindowNSize, Qt::SizeVerCursor); + MAP_BUILTIN(PointerStyle::WindowSSize, Qt::SizeVerCursor); + MAP_BUILTIN(PointerStyle::WindowWSize, Qt::SizeHorCursor); + MAP_BUILTIN(PointerStyle::WindowESize, Qt::SizeHorCursor); + MAP_BUILTIN(PointerStyle::WindowNWSize, Qt::SizeFDiagCursor); + MAP_BUILTIN(PointerStyle::WindowNESize, Qt::SizeBDiagCursor); + MAP_BUILTIN(PointerStyle::WindowSWSize, Qt::SizeBDiagCursor); + MAP_BUILTIN(PointerStyle::WindowSESize, Qt::SizeFDiagCursor); + + MAP_BUILTIN(PointerStyle::HSizeBar, Qt::SizeHorCursor); + MAP_BUILTIN(PointerStyle::VSizeBar, Qt::SizeVerCursor); + + MAP_BUILTIN(PointerStyle::RefHand, Qt::PointingHandCursor); + MAP_BUILTIN(PointerStyle::Hand, Qt::OpenHandCursor); +#if 0 + MAP_BUILTIN( PointerStyle::Pen, GDK_PENCIL ); +#endif + MAP_BUILTIN(PointerStyle::HSplit, Qt::SizeHorCursor); + MAP_BUILTIN(PointerStyle::VSplit, Qt::SizeVerCursor); + + MAP_BUILTIN(PointerStyle::Move, Qt::SizeAllCursor); + + MAP_BUILTIN(PointerStyle::Null, Qt::BlankCursor); + MAKE_CURSOR(PointerStyle::Magnify, magnify_, RID_CURSOR_MAGNIFY); + MAKE_CURSOR(PointerStyle::Fill, fill_, RID_CURSOR_FILL); + MAKE_CURSOR(PointerStyle::MoveData, movedata_, RID_CURSOR_MOVE_DATA); + MAKE_CURSOR(PointerStyle::CopyData, copydata_, RID_CURSOR_COPY_DATA); + MAKE_CURSOR(PointerStyle::MoveFile, movefile_, RID_CURSOR_MOVE_FILE); + MAKE_CURSOR(PointerStyle::CopyFile, copyfile_, RID_CURSOR_COPY_FILE); + MAKE_CURSOR(PointerStyle::MoveFiles, movefiles_, RID_CURSOR_MOVE_FILES); + MAKE_CURSOR(PointerStyle::CopyFiles, copyfiles_, RID_CURSOR_COPY_FILES); + MAKE_CURSOR(PointerStyle::NotAllowed, nodrop_, RID_CURSOR_NOT_ALLOWED); + MAKE_CURSOR(PointerStyle::Rotate, rotate_, RID_CURSOR_ROTATE); + MAKE_CURSOR(PointerStyle::HShear, hshear_, RID_CURSOR_H_SHEAR); + MAKE_CURSOR(PointerStyle::VShear, vshear_, RID_CURSOR_V_SHEAR); + MAKE_CURSOR(PointerStyle::DrawLine, drawline_, RID_CURSOR_DRAW_LINE); + MAKE_CURSOR(PointerStyle::DrawRect, drawrect_, RID_CURSOR_DRAW_RECT); + MAKE_CURSOR(PointerStyle::DrawPolygon, drawpolygon_, RID_CURSOR_DRAW_POLYGON); + MAKE_CURSOR(PointerStyle::DrawBezier, drawbezier_, RID_CURSOR_DRAW_BEZIER); + MAKE_CURSOR(PointerStyle::DrawArc, drawarc_, RID_CURSOR_DRAW_ARC); + MAKE_CURSOR(PointerStyle::DrawPie, drawpie_, RID_CURSOR_DRAW_PIE); + MAKE_CURSOR(PointerStyle::DrawCircleCut, drawcirclecut_, RID_CURSOR_DRAW_CIRCLE_CUT); + MAKE_CURSOR(PointerStyle::DrawEllipse, drawellipse_, RID_CURSOR_DRAW_ELLIPSE); + MAKE_CURSOR(PointerStyle::DrawConnect, drawconnect_, RID_CURSOR_DRAW_CONNECT); + MAKE_CURSOR(PointerStyle::DrawText, drawtext_, RID_CURSOR_DRAW_TEXT); + MAKE_CURSOR(PointerStyle::Mirror, mirror_, RID_CURSOR_MIRROR); + MAKE_CURSOR(PointerStyle::Crook, crook_, RID_CURSOR_CROOK); + MAKE_CURSOR(PointerStyle::Crop, crop_, RID_CURSOR_CROP); + MAKE_CURSOR(PointerStyle::MovePoint, movepoint_, RID_CURSOR_MOVE_POINT); + MAKE_CURSOR(PointerStyle::MoveBezierWeight, movebezierweight_, + RID_CURSOR_MOVE_BEZIER_WEIGHT); + MAKE_CURSOR(PointerStyle::DrawFreehand, drawfreehand_, RID_CURSOR_DRAW_FREEHAND); + MAKE_CURSOR(PointerStyle::DrawCaption, drawcaption_, RID_CURSOR_DRAW_CAPTION); + MAKE_CURSOR(PointerStyle::LinkData, linkdata_, RID_CURSOR_LINK_DATA); + MAKE_CURSOR(PointerStyle::MoveDataLink, movedlnk_, RID_CURSOR_MOVE_DATA_LINK); + MAKE_CURSOR(PointerStyle::CopyDataLink, copydlnk_, RID_CURSOR_COPY_DATA_LINK); + MAKE_CURSOR(PointerStyle::LinkFile, linkfile_, RID_CURSOR_LINK_FILE); + MAKE_CURSOR(PointerStyle::MoveFileLink, moveflnk_, RID_CURSOR_MOVE_FILE_LINK); + MAKE_CURSOR(PointerStyle::CopyFileLink, copyflnk_, RID_CURSOR_COPY_FILE_LINK); + MAKE_CURSOR(PointerStyle::Chart, chart_, RID_CURSOR_CHART); + MAKE_CURSOR(PointerStyle::Detective, detective_, RID_CURSOR_DETECTIVE); + MAKE_CURSOR(PointerStyle::PivotCol, pivotcol_, RID_CURSOR_PIVOT_COLUMN); + MAKE_CURSOR(PointerStyle::PivotRow, pivotrow_, RID_CURSOR_PIVOT_ROW); + MAKE_CURSOR(PointerStyle::PivotField, pivotfld_, RID_CURSOR_PIVOT_FIELD); + MAKE_CURSOR(PointerStyle::PivotDelete, pivotdel_, RID_CURSOR_PIVOT_DELETE); + MAKE_CURSOR(PointerStyle::Chain, chain_, RID_CURSOR_CHAIN); + MAKE_CURSOR(PointerStyle::ChainNotAllowed, chainnot_, RID_CURSOR_CHAIN_NOT_ALLOWED); + MAKE_CURSOR(PointerStyle::AutoScrollN, asn_, RID_CURSOR_AUTOSCROLL_N); + MAKE_CURSOR(PointerStyle::AutoScrollS, ass_, RID_CURSOR_AUTOSCROLL_S); + MAKE_CURSOR(PointerStyle::AutoScrollW, asw_, RID_CURSOR_AUTOSCROLL_W); + MAKE_CURSOR(PointerStyle::AutoScrollE, ase_, RID_CURSOR_AUTOSCROLL_E); + MAKE_CURSOR(PointerStyle::AutoScrollNW, asnw_, RID_CURSOR_AUTOSCROLL_NW); + MAKE_CURSOR(PointerStyle::AutoScrollNE, asne_, RID_CURSOR_AUTOSCROLL_NE); + MAKE_CURSOR(PointerStyle::AutoScrollSW, assw_, RID_CURSOR_AUTOSCROLL_SW); + MAKE_CURSOR(PointerStyle::AutoScrollSE, asse_, RID_CURSOR_AUTOSCROLL_SE); + MAKE_CURSOR(PointerStyle::AutoScrollNS, asns_, RID_CURSOR_AUTOSCROLL_NS); + MAKE_CURSOR(PointerStyle::AutoScrollWE, aswe_, RID_CURSOR_AUTOSCROLL_WE); + MAKE_CURSOR(PointerStyle::AutoScrollNSWE, asnswe_, RID_CURSOR_AUTOSCROLL_NSWE); + MAKE_CURSOR(PointerStyle::TextVertical, vertcurs_, RID_CURSOR_TEXT_VERTICAL); + + MAKE_CURSOR(PointerStyle::TabSelectS, tblsels_, RID_CURSOR_TAB_SELECT_S); + MAKE_CURSOR(PointerStyle::TabSelectE, tblsele_, RID_CURSOR_TAB_SELECT_E); + MAKE_CURSOR(PointerStyle::TabSelectSE, tblselse_, RID_CURSOR_TAB_SELECT_SE); + MAKE_CURSOR(PointerStyle::TabSelectW, tblselw_, RID_CURSOR_TAB_SELECT_W); + MAKE_CURSOR(PointerStyle::TabSelectSW, tblselsw_, RID_CURSOR_TAB_SELECT_SW); + + MAKE_CURSOR(PointerStyle::HideWhitespace, hidewhitespace_, RID_CURSOR_HIDE_WHITESPACE); + MAKE_CURSOR(PointerStyle::ShowWhitespace, showwhitespace_, RID_CURSOR_SHOW_WHITESPACE); + + MAKE_CURSOR(PointerStyle::FatCross, fatcross_, RID_CURSOR_FATCROSS); + default: + break; + } + if (!pCursor) + { + pCursor = new QCursor(Qt::ArrowCursor); + SAL_WARN("vcl.qt", "pointer " << static_cast<int>(ePointerStyle) << " not implemented"); + } + + m_aCursors[ePointerStyle].reset(pCursor); + } + + return *m_aCursors[ePointerStyle]; +} + +void QtData::ErrorTrapPush() {} + +bool QtData::ErrorTrapPop(bool /*bIgnoreError*/) { return false; } + +bool QtData::noNativeControls() +{ + static const bool bNoNative + = ((nullptr != getenv("SAL_VCL_QT5_NO_NATIVE")) && (nullptr != ImplGetSVData()) + && ImplGetSVData()->maAppData.mxToolkitName + && ImplGetSVData()->maAppData.mxToolkitName->match("qt5")); + return bNoNative; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtDragAndDrop.cxx b/vcl/qt5/QtDragAndDrop.cxx new file mode 100644 index 0000000000..ffabc1bbba --- /dev/null +++ b/vcl/qt5/QtDragAndDrop.cxx @@ -0,0 +1,249 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + +#include <com/sun/star/awt/MouseButton.hpp> +#include <com/sun/star/datatransfer/DataFlavor.hpp> +#include <com/sun/star/datatransfer/dnd/DNDConstants.hpp> +#include <cppuhelper/supportsservice.hxx> +#include <sal/log.hxx> + +#include <QtDragAndDrop.hxx> +#include <QtFrame.hxx> +#include <QtTransferable.hxx> +#include <QtWidget.hxx> + +#include <QtGui/QDrag> + +using namespace com::sun::star; + +QtDragSource::~QtDragSource() {} + +void QtDragSource::deinitialize() { m_pFrame = nullptr; } + +sal_Bool QtDragSource::isDragImageSupported() { return true; } + +sal_Int32 QtDragSource::getDefaultCursor(sal_Int8) { return 0; } + +void QtDragSource::initialize(const css::uno::Sequence<css::uno::Any>& rArguments) +{ + if (rArguments.getLength() < 2) + { + throw uno::RuntimeException("DragSource::initialize: Cannot install window event handler", + getXWeak()); + } + + sal_IntPtr nFrame = 0; + rArguments.getConstArray()[1] >>= nFrame; + + if (!nFrame) + { + throw uno::RuntimeException("DragSource::initialize: missing SalFrame", getXWeak()); + } + + m_pFrame = reinterpret_cast<QtFrame*>(nFrame); + m_pFrame->registerDragSource(this); +} + +void QtDragSource::startDrag( + const datatransfer::dnd::DragGestureEvent& /*rEvent*/, sal_Int8 sourceActions, + sal_Int32 /*cursor*/, sal_Int32 /*image*/, + const css::uno::Reference<css::datatransfer::XTransferable>& rTrans, + const css::uno::Reference<css::datatransfer::dnd::XDragSourceListener>& rListener) +{ + m_xListener = rListener; + + if (m_pFrame) + { + QDrag* drag = new QDrag(m_pFrame->GetQWidget()); + drag->setMimeData(new QtMimeData(rTrans)); + // just a reminder that exec starts a nested event loop, so everything after + // this call is just executed, after D'n'D has finished! + drag->exec(toQtDropActions(sourceActions), getPreferredDropAction(sourceActions)); + } + + // the drop will eventually call fire_dragEnd, which will clear the listener. + // if D'n'D ends without success, we just get a leave event without any indicator, + // but the event loop will be terminated, so we have to try to inform the source of + // a failure in any way. + fire_dragEnd(datatransfer::dnd::DNDConstants::ACTION_NONE, false); +} + +void QtDragSource::fire_dragEnd(sal_Int8 nAction, bool bDropSuccessful) +{ + if (!m_xListener.is()) + return; + + datatransfer::dnd::DragSourceDropEvent aEv; + aEv.DropAction = nAction; + aEv.DropSuccess = bDropSuccessful; + + auto xListener = m_xListener; + m_xListener.clear(); + xListener->dragDropEnd(aEv); +} + +OUString SAL_CALL QtDragSource::getImplementationName() +{ + return "com.sun.star.datatransfer.dnd.VclQtDragSource"; +} + +sal_Bool SAL_CALL QtDragSource::supportsService(OUString const& ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +css::uno::Sequence<OUString> SAL_CALL QtDragSource::getSupportedServiceNames() +{ + return { "com.sun.star.datatransfer.dnd.QtDragSource" }; +} + +QtDropTarget::QtDropTarget() + : WeakComponentImplHelper(m_aMutex) + , m_pFrame(nullptr) + , m_bActive(false) + , m_nDefaultActions(0) +{ +} + +OUString SAL_CALL QtDropTarget::getImplementationName() +{ + return "com.sun.star.datatransfer.dnd.VclQtDropTarget"; +} + +sal_Bool SAL_CALL QtDropTarget::supportsService(OUString const& ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +css::uno::Sequence<OUString> SAL_CALL QtDropTarget::getSupportedServiceNames() +{ + return { "com.sun.star.datatransfer.dnd.QtDropTarget" }; +} + +QtDropTarget::~QtDropTarget() {} + +void QtDropTarget::deinitialize() +{ + m_pFrame = nullptr; + m_bActive = false; +} + +void QtDropTarget::initialize(const uno::Sequence<uno::Any>& rArguments) +{ + if (rArguments.getLength() < 2) + { + throw uno::RuntimeException("DropTarget::initialize: Cannot install window event handler", + getXWeak()); + } + + sal_IntPtr nFrame = 0; + rArguments.getConstArray()[1] >>= nFrame; + + if (!nFrame) + { + throw uno::RuntimeException("DropTarget::initialize: missing SalFrame", getXWeak()); + } + + m_nDropAction = datatransfer::dnd::DNDConstants::ACTION_NONE; + + m_pFrame = reinterpret_cast<QtFrame*>(nFrame); + m_pFrame->registerDropTarget(this); + m_bActive = true; +} + +void QtDropTarget::addDropTargetListener( + const uno::Reference<css::datatransfer::dnd::XDropTargetListener>& xListener) +{ + ::osl::Guard<::osl::Mutex> aGuard(m_aMutex); + + m_aListeners.push_back(xListener); +} + +void QtDropTarget::removeDropTargetListener( + const uno::Reference<css::datatransfer::dnd::XDropTargetListener>& xListener) +{ + ::osl::Guard<::osl::Mutex> aGuard(m_aMutex); + + std::erase(m_aListeners, xListener); +} + +sal_Bool QtDropTarget::isActive() { return m_bActive; } + +void QtDropTarget::setActive(sal_Bool bActive) { m_bActive = bActive; } + +sal_Int8 QtDropTarget::getDefaultActions() { return m_nDefaultActions; } + +void QtDropTarget::setDefaultActions(sal_Int8 nDefaultActions) +{ + m_nDefaultActions = nDefaultActions; +} + +void QtDropTarget::fire_dragEnter(const css::datatransfer::dnd::DropTargetDragEnterEvent& dtde) +{ + osl::ClearableGuard<::osl::Mutex> aGuard(m_aMutex); + std::vector<css::uno::Reference<css::datatransfer::dnd::XDropTargetListener>> aListeners( + m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->dragEnter(dtde); + } +} + +void QtDropTarget::fire_dragOver(const css::datatransfer::dnd::DropTargetDragEnterEvent& dtde) +{ + osl::ClearableGuard<::osl::Mutex> aGuard(m_aMutex); + std::vector<css::uno::Reference<css::datatransfer::dnd::XDropTargetListener>> aListeners( + m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + listener->dragOver(dtde); +} + +void QtDropTarget::fire_drop(const css::datatransfer::dnd::DropTargetDropEvent& dtde) +{ + m_bDropSuccessful = true; + + osl::ClearableGuard<osl::Mutex> aGuard(m_aMutex); + std::vector<css::uno::Reference<css::datatransfer::dnd::XDropTargetListener>> aListeners( + m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + listener->drop(dtde); +} + +void QtDropTarget::fire_dragExit(const css::datatransfer::dnd::DropTargetEvent& dte) +{ + osl::ClearableGuard<::osl::Mutex> aGuard(m_aMutex); + std::vector<css::uno::Reference<css::datatransfer::dnd::XDropTargetListener>> aListeners( + m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + listener->dragExit(dte); +} + +void QtDropTarget::acceptDrag(sal_Int8 dragOperation) { m_nDropAction = dragOperation; } + +void QtDropTarget::rejectDrag() { m_nDropAction = 0; } + +void QtDropTarget::acceptDrop(sal_Int8 dropOperation) { m_nDropAction = dropOperation; } + +void QtDropTarget::rejectDrop() { m_nDropAction = 0; } + +void QtDropTarget::dropComplete(sal_Bool success) +{ + m_bDropSuccessful = (m_bDropSuccessful && success); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtFilePicker.cxx b/vcl/qt5/QtFilePicker.cxx new file mode 100644 index 0000000000..8a13ec6723 --- /dev/null +++ b/vcl/qt5/QtFilePicker.cxx @@ -0,0 +1,983 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <fpicker/fpsofficeResMgr.hxx> +#include <QtFilePicker.hxx> +#include <QtFilePicker.moc> + +#include <QtFrame.hxx> +#include <QtTools.hxx> +#include <QtWidget.hxx> +#include <QtInstance.hxx> + +#include <com/sun/star/awt/SystemDependentXWindow.hpp> +#include <com/sun/star/awt/XSystemDependentWindowPeer.hpp> +#include <com/sun/star/awt/XWindow.hpp> +#include <com/sun/star/frame/Desktop.hpp> +#include <com/sun/star/frame/TerminationVetoException.hpp> +#include <com/sun/star/frame/XDesktop.hpp> +#include <com/sun/star/lang/DisposedException.hpp> +#include <com/sun/star/lang/IllegalArgumentException.hpp> +#include <com/sun/star/lang/SystemDependent.hpp> +#include <com/sun/star/lang/XMultiServiceFactory.hpp> +#include <com/sun/star/ui/dialogs/CommonFilePickerElementIds.hpp> +#include <com/sun/star/ui/dialogs/ControlActions.hpp> +#include <com/sun/star/ui/dialogs/ExecutableDialogResults.hpp> +#include <com/sun/star/ui/dialogs/ExtendedFilePickerElementIds.hpp> +#include <com/sun/star/ui/dialogs/TemplateDescription.hpp> +#include <com/sun/star/uri/ExternalUriReferenceTranslator.hpp> +#include <cppuhelper/interfacecontainer.h> +#include <cppuhelper/supportsservice.hxx> +#include <rtl/process.h> +#include <sal/log.hxx> + +#include <QtCore/QDebug> +#include <QtCore/QRegularExpression> +#include <QtCore/QThread> +#include <QtCore/QUrl> +#include <QtGui/QClipboard> +#include <QtGui/QWindow> +#include <QtWidgets/QApplication> +#include <QtWidgets/QCheckBox> +#include <QtWidgets/QComboBox> +#include <QtWidgets/QGridLayout> +#include <QtWidgets/QHBoxLayout> +#include <QtWidgets/QLabel> +#include <QtWidgets/QMessageBox> +#include <QtWidgets/QPushButton> +#include <QtWidgets/QWidget> + +#include <unx/geninst.h> +#include <fpicker/strings.hrc> +#include <utility> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::ui::dialogs; +using namespace ::com::sun::star::ui::dialogs::TemplateDescription; +using namespace ::com::sun::star::ui::dialogs::ExtendedFilePickerElementIds; +using namespace ::com::sun::star::ui::dialogs::CommonFilePickerElementIds; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::uno; + +namespace +{ +uno::Sequence<OUString> FilePicker_getSupportedServiceNames() +{ + return { "com.sun.star.ui.dialogs.FilePicker", "com.sun.star.ui.dialogs.SystemFilePicker", + "com.sun.star.ui.dialogs.QtFilePicker" }; +} +} + +QtFilePicker::QtFilePicker(css::uno::Reference<css::uno::XComponentContext> context, + QFileDialog::FileMode eMode, bool bUseNative) + : QtFilePicker_Base(m_aHelperMutex) + , m_context(std::move(context)) + , m_bIsFolderPicker(eMode == QFileDialog::Directory) + , m_pParentWidget(nullptr) + , m_pFileDialog(new QFileDialog(nullptr, {}, QDir::homePath())) + , m_pExtraControls(new QWidget()) +{ + m_pFileDialog->setOption(QFileDialog::DontUseNativeDialog, !bUseNative); + + m_pFileDialog->setFileMode(eMode); + m_pFileDialog->setWindowModality(Qt::ApplicationModal); + + if (m_bIsFolderPicker) + { + m_pFileDialog->setOption(QFileDialog::ShowDirsOnly, true); + m_pFileDialog->setWindowTitle(toQString(FpsResId(STR_SVT_FOLDERPICKER_DEFAULT_TITLE))); + } + + m_pLayout = dynamic_cast<QGridLayout*>(m_pFileDialog->layout()); + + setMultiSelectionMode(false); + + // XFilePickerListener notifications + connect(m_pFileDialog.get(), SIGNAL(filterSelected(const QString&)), this, + SLOT(filterSelected(const QString&))); + connect(m_pFileDialog.get(), SIGNAL(currentChanged(const QString&)), this, + SLOT(currentChanged(const QString&))); + + // update automatic file extension when filter is changed + connect(m_pFileDialog.get(), SIGNAL(filterSelected(const QString&)), this, + SLOT(updateAutomaticFileExtension())); + + connect(m_pFileDialog.get(), SIGNAL(finished(int)), this, SLOT(finished(int))); +} + +QtFilePicker::~QtFilePicker() +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread([this]() { + // must delete it in main thread, otherwise + // QSocketNotifier::setEnabled() will crash us + m_pFileDialog.reset(); + }); +} + +void SAL_CALL +QtFilePicker::addFilePickerListener(const uno::Reference<XFilePickerListener>& xListener) +{ + SolarMutexGuard aGuard; + m_xListener = xListener; +} + +void SAL_CALL QtFilePicker::removeFilePickerListener(const uno::Reference<XFilePickerListener>&) +{ + SolarMutexGuard aGuard; + m_xListener.clear(); +} + +void SAL_CALL QtFilePicker::setTitle(const OUString& title) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread( + [this, &title]() { m_pFileDialog->setWindowTitle(toQString(title)); }); +} + +void QtFilePicker::prepareExecute() +{ + QWidget* pTransientParent = m_pParentWidget; + if (!pTransientParent) + { + vcl::Window* pWindow = ::Application::GetActiveTopWindow(); + if (pWindow) + { + QtFrame* pFrame = dynamic_cast<QtFrame*>(pWindow->ImplGetFrame()); + assert(pFrame); + if (pFrame) + pTransientParent = pFrame->asChild(); + } + } + + if (!m_aNamedFilterList.isEmpty()) + m_pFileDialog->setNameFilters(m_aNamedFilterList); + if (!m_aCurrentFilter.isEmpty()) + m_pFileDialog->selectNameFilter(m_aCurrentFilter); + + updateAutomaticFileExtension(); + + uno::Reference<css::frame::XDesktop> xDesktop(css::frame::Desktop::create(m_context), + UNO_QUERY_THROW); + + // will hide the window, so do before show + m_pFileDialog->setParent(pTransientParent, m_pFileDialog->windowFlags()); + m_pFileDialog->show(); + xDesktop->addTerminateListener(this); +} + +void QtFilePicker::finished(int nResult) +{ + SolarMutexGuard g; + uno::Reference<css::frame::XDesktop> xDesktop(css::frame::Desktop::create(m_context), + UNO_QUERY_THROW); + xDesktop->removeTerminateListener(this); + m_pFileDialog->setParent(nullptr, m_pFileDialog->windowFlags()); + + if (m_xClosedListener.is()) + { + const sal_Int16 nRet = (QFileDialog::Rejected == nResult) ? ExecutableDialogResults::CANCEL + : ExecutableDialogResults::OK; + css::ui::dialogs::DialogClosedEvent aEvent(*this, nRet); + m_xClosedListener->dialogClosed(aEvent); + m_xClosedListener.clear(); + } +} + +sal_Int16 SAL_CALL QtFilePicker::execute() +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + if (!pSalInst->IsMainThread()) + { + sal_uInt16 ret; + pSalInst->RunInMainThread([&ret, this]() { ret = execute(); }); + return ret; + } + + prepareExecute(); + int result = m_pFileDialog->exec(); + + if (QFileDialog::Rejected == result) + return ExecutableDialogResults::CANCEL; + return ExecutableDialogResults::OK; +} + +// XAsynchronousExecutableDialog functions +void SAL_CALL QtFilePicker::setDialogTitle(const OUString& _rTitle) { setTitle(_rTitle); } + +void SAL_CALL +QtFilePicker::startExecuteModal(const Reference<css::ui::dialogs::XDialogClosedListener>& xListener) +{ + m_xClosedListener = xListener; + prepareExecute(); + m_pFileDialog->show(); +} + +void SAL_CALL QtFilePicker::setMultiSelectionMode(sal_Bool multiSelect) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread([this, multiSelect]() { + if (m_bIsFolderPicker || m_pFileDialog->acceptMode() == QFileDialog::AcceptSave) + return; + + m_pFileDialog->setFileMode(multiSelect ? QFileDialog::ExistingFiles + : QFileDialog::ExistingFile); + }); +} + +void SAL_CALL QtFilePicker::setDefaultName(const OUString& name) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread([this, &name]() { m_pFileDialog->selectFile(toQString(name)); }); +} + +void SAL_CALL QtFilePicker::setDisplayDirectory(const OUString& dir) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread([this, &dir]() { + QString qDir(toQString(dir)); + m_pFileDialog->setDirectoryUrl(QUrl(qDir)); + }); +} + +OUString SAL_CALL QtFilePicker::getDisplayDirectory() +{ + SolarMutexGuard g; + OUString ret; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread( + [&ret, this]() { ret = toOUString(m_pFileDialog->directoryUrl().toString()); }); + return ret; +} + +uno::Sequence<OUString> SAL_CALL QtFilePicker::getFiles() +{ + uno::Sequence<OUString> seq = getSelectedFiles(); + if (seq.getLength() > 1) + seq.realloc(1); + return seq; +} + +uno::Sequence<OUString> SAL_CALL QtFilePicker::getSelectedFiles() +{ + SolarMutexGuard g; + QList<QUrl> urls; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread([&urls, this]() { urls = m_pFileDialog->selectedUrls(); }); + + uno::Sequence<OUString> seq(urls.size()); + auto seqRange = asNonConstRange(seq); + + auto const trans = css::uri::ExternalUriReferenceTranslator::create(m_context); + size_t i = 0; + for (const QUrl& aURL : urls) + { + // Unlike LO, QFileDialog (<https://doc.qt.io/qt-5/qfiledialog.html>) apparently always + // treats file-system pathnames as UTF-8--encoded, regardless of LANG/LC_CTYPE locale + // setting. And pathnames containing byte sequences that are not valid UTF-8 are apparently + // filtered out and not even displayed by QFileDialog, so aURL will always have a "payload" + // that matches the pathname's byte sequence. So the pathname's byte sequence (which + // happens to also be aURL's payload) in the LANG/LC_CTYPE encoding needs to be converted + // into LO's internal UTF-8 file URL encoding via + // XExternalUriReferenceTranslator::translateToInternal (which looks somewhat paradoxical as + // aURL.toEncoded() nominally already has a UTF-8 payload): + auto const extUrl = toOUString(aURL.toEncoded()); + auto intUrl = trans->translateToInternal(extUrl); + if (intUrl.isEmpty()) + { + // If translation failed, fall back to original URL: + SAL_WARN("vcl.qt", "cannot convert <" << extUrl << "> from locale encoding to UTF-8"); + intUrl = extUrl; + } + seqRange[i++] = intUrl; + } + + return seq; +} + +void SAL_CALL QtFilePicker::appendFilter(const OUString& title, const OUString& filter) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + if (!pSalInst->IsMainThread()) + { + pSalInst->RunInMainThread([this, &title, &filter]() { appendFilter(title, filter); }); + return; + } + + // '/' need to be escaped else they are assumed to be mime types + QString sTitle = toQString(title).replace("/", "\\/"); + + QString sFilterName = sTitle; + // the Qt non-native file picker adds the extensions to the filter title, so strip them + if (m_pFileDialog->testOption(QFileDialog::DontUseNativeDialog)) + { + int pos = sFilterName.indexOf(" ("); + if (pos >= 0) + sFilterName.truncate(pos); + } + + QString sGlobFilter = toQString(filter); + + // LibreOffice gives us filters separated by ';' qt dialogs just want space separated + sGlobFilter.replace(";", " "); + + // make sure "*.*" is not used as "all files" + sGlobFilter.replace("*.*", "*"); + + m_aNamedFilterList << QStringLiteral("%1 (%2)").arg(sFilterName, sGlobFilter); + m_aTitleToFilterMap[sTitle] = m_aNamedFilterList.constLast(); + m_aNamedFilterToExtensionMap[m_aNamedFilterList.constLast()] = sGlobFilter; +} + +void SAL_CALL QtFilePicker::setCurrentFilter(const OUString& title) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread([this, &title]() { + m_aCurrentFilter = m_aTitleToFilterMap.value(toQString(title).replace("/", "\\/")); + }); +} + +OUString SAL_CALL QtFilePicker::getCurrentFilter() +{ + SolarMutexGuard g; + QString filter; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread([&filter, this]() { + filter = m_aTitleToFilterMap.key(m_pFileDialog->selectedNameFilter()); + }); + + if (filter.isEmpty()) + filter = "ODF Text Document (.odt)"; + return toOUString(filter); +} + +void SAL_CALL QtFilePicker::appendFilterGroup(const OUString& rGroupTitle, + const uno::Sequence<beans::StringPair>& filters) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + if (!pSalInst->IsMainThread()) + { + pSalInst->RunInMainThread( + [this, &rGroupTitle, &filters]() { appendFilterGroup(rGroupTitle, filters); }); + return; + } + + const sal_uInt16 length = filters.getLength(); + for (sal_uInt16 i = 0; i < length; ++i) + { + beans::StringPair aPair = filters[i]; + appendFilter(aPair.First, aPair.Second); + } +} + +uno::Any QtFilePicker::handleGetListValue(const QComboBox* pWidget, sal_Int16 nControlAction) +{ + uno::Any aAny; + switch (nControlAction) + { + case ControlActions::GET_ITEMS: + { + Sequence<OUString> aItemList(pWidget->count()); + auto aItemListRange = asNonConstRange(aItemList); + for (sal_Int32 i = 0; i < pWidget->count(); ++i) + aItemListRange[i] = toOUString(pWidget->itemText(i)); + aAny <<= aItemList; + break; + } + case ControlActions::GET_SELECTED_ITEM: + { + if (!pWidget->currentText().isEmpty()) + aAny <<= toOUString(pWidget->currentText()); + break; + } + case ControlActions::GET_SELECTED_ITEM_INDEX: + { + if (pWidget->currentIndex() >= 0) + aAny <<= static_cast<sal_Int32>(pWidget->currentIndex()); + break; + } + default: + SAL_WARN("vcl.qt", + "undocumented/unimplemented ControlAction for a list " << nControlAction); + break; + } + return aAny; +} + +void QtFilePicker::handleSetListValue(QComboBox* pWidget, sal_Int16 nControlAction, + const uno::Any& rValue) +{ + switch (nControlAction) + { + case ControlActions::ADD_ITEM: + { + OUString sItem; + rValue >>= sItem; + pWidget->addItem(toQString(sItem)); + break; + } + case ControlActions::ADD_ITEMS: + { + Sequence<OUString> aStringList; + rValue >>= aStringList; + for (auto const& sItem : std::as_const(aStringList)) + pWidget->addItem(toQString(sItem)); + break; + } + case ControlActions::DELETE_ITEM: + { + sal_Int32 nPos = 0; + rValue >>= nPos; + pWidget->removeItem(nPos); + break; + } + case ControlActions::DELETE_ITEMS: + { + pWidget->clear(); + break; + } + case ControlActions::SET_SELECT_ITEM: + { + sal_Int32 nPos = 0; + rValue >>= nPos; + pWidget->setCurrentIndex(nPos); + break; + } + default: + SAL_WARN("vcl.qt", + "undocumented/unimplemented ControlAction for a list " << nControlAction); + break; + } + + pWidget->setEnabled(pWidget->count() > 0); +} + +void SAL_CALL QtFilePicker::setValue(sal_Int16 controlId, sal_Int16 nControlAction, + const uno::Any& value) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + if (!pSalInst->IsMainThread()) + { + pSalInst->RunInMainThread([this, controlId, nControlAction, &value]() { + setValue(controlId, nControlAction, value); + }); + return; + } + + if (m_aCustomWidgetsMap.contains(controlId)) + { + QWidget* widget = m_aCustomWidgetsMap.value(controlId); + QCheckBox* cb = dynamic_cast<QCheckBox*>(widget); + if (cb) + cb->setChecked(value.get<bool>()); + else + { + QComboBox* combo = dynamic_cast<QComboBox*>(widget); + if (combo) + handleSetListValue(combo, nControlAction, value); + } + } + else + SAL_WARN("vcl.qt", "set value on unknown control " << controlId); +} + +uno::Any SAL_CALL QtFilePicker::getValue(sal_Int16 controlId, sal_Int16 nControlAction) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + if (!pSalInst->IsMainThread()) + { + uno::Any ret; + pSalInst->RunInMainThread([&ret, this, controlId, nControlAction]() { + ret = getValue(controlId, nControlAction); + }); + return ret; + } + + uno::Any res(false); + if (m_aCustomWidgetsMap.contains(controlId)) + { + QWidget* widget = m_aCustomWidgetsMap.value(controlId); + QCheckBox* cb = dynamic_cast<QCheckBox*>(widget); + if (cb) + res <<= cb->isChecked(); + else + { + QComboBox* combo = dynamic_cast<QComboBox*>(widget); + if (combo) + res = handleGetListValue(combo, nControlAction); + } + } + else + SAL_WARN("vcl.qt", "get value on unknown control " << controlId); + + return res; +} + +void SAL_CALL QtFilePicker::enableControl(sal_Int16 controlId, sal_Bool enable) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread([this, controlId, enable]() { + if (m_aCustomWidgetsMap.contains(controlId)) + m_aCustomWidgetsMap.value(controlId)->setEnabled(enable); + else + SAL_WARN("vcl.qt", "enable unknown control " << controlId); + }); +} + +void SAL_CALL QtFilePicker::setLabel(sal_Int16 controlId, const OUString& label) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + if (!pSalInst->IsMainThread()) + { + pSalInst->RunInMainThread([this, controlId, label]() { setLabel(controlId, label); }); + return; + } + + if (m_aCustomWidgetsMap.contains(controlId)) + { + QCheckBox* cb = dynamic_cast<QCheckBox*>(m_aCustomWidgetsMap.value(controlId)); + if (cb) + cb->setText(toQString(label)); + } + else + SAL_WARN("vcl.qt", "set label on unknown control " << controlId); +} + +OUString SAL_CALL QtFilePicker::getLabel(sal_Int16 controlId) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + if (!pSalInst->IsMainThread()) + { + OUString ret; + pSalInst->RunInMainThread([&ret, this, controlId]() { ret = getLabel(controlId); }); + return ret; + } + + QString label; + if (m_aCustomWidgetsMap.contains(controlId)) + { + QCheckBox* cb = dynamic_cast<QCheckBox*>(m_aCustomWidgetsMap.value(controlId)); + if (cb) + label = cb->text(); + } + else + SAL_WARN("vcl.qt", "get label on unknown control " << controlId); + + return toOUString(label); +} + +QString QtFilePicker::getResString(TranslateId pResId) +{ + QString aResString; + + if (!pResId) + return aResString; + + aResString = toQString(FpsResId(pResId)); + + return aResString.replace('~', '&'); +} + +void QtFilePicker::addCustomControl(sal_Int16 controlId) +{ + QWidget* widget = nullptr; + QLabel* label = nullptr; + TranslateId resId; + QCheckBox* pCheckbox = nullptr; + + switch (controlId) + { + case CHECKBOX_AUTOEXTENSION: + resId = STR_SVT_FILEPICKER_AUTO_EXTENSION; + break; + case CHECKBOX_PASSWORD: + resId = STR_SVT_FILEPICKER_PASSWORD; + break; + case CHECKBOX_FILTEROPTIONS: + resId = STR_SVT_FILEPICKER_FILTER_OPTIONS; + break; + case CHECKBOX_READONLY: + resId = STR_SVT_FILEPICKER_READONLY; + break; + case CHECKBOX_LINK: + resId = STR_SVT_FILEPICKER_INSERT_AS_LINK; + break; + case CHECKBOX_PREVIEW: + resId = STR_SVT_FILEPICKER_SHOW_PREVIEW; + break; + case CHECKBOX_SELECTION: + resId = STR_SVT_FILEPICKER_SELECTION; + break; + case CHECKBOX_GPGENCRYPTION: + resId = STR_SVT_FILEPICKER_GPGENCRYPT; + break; + case PUSHBUTTON_PLAY: + resId = STR_SVT_FILEPICKER_PLAY; + break; + case LISTBOX_VERSION: + resId = STR_SVT_FILEPICKER_VERSION; + break; + case LISTBOX_TEMPLATE: + resId = STR_SVT_FILEPICKER_TEMPLATES; + break; + case LISTBOX_IMAGE_TEMPLATE: + resId = STR_SVT_FILEPICKER_IMAGE_TEMPLATE; + break; + case LISTBOX_IMAGE_ANCHOR: + resId = STR_SVT_FILEPICKER_IMAGE_ANCHOR; + break; + case LISTBOX_VERSION_LABEL: + case LISTBOX_TEMPLATE_LABEL: + case LISTBOX_IMAGE_TEMPLATE_LABEL: + case LISTBOX_IMAGE_ANCHOR_LABEL: + case LISTBOX_FILTER_SELECTOR: + break; + } + + switch (controlId) + { + case CHECKBOX_AUTOEXTENSION: + pCheckbox = new QCheckBox(getResString(resId), m_pExtraControls); + // to add/remove automatic file extension based on checkbox + connect(pCheckbox, SIGNAL(stateChanged(int)), this, + SLOT(updateAutomaticFileExtension())); + widget = pCheckbox; + break; + case CHECKBOX_PASSWORD: + case CHECKBOX_FILTEROPTIONS: + case CHECKBOX_READONLY: + case CHECKBOX_LINK: + case CHECKBOX_PREVIEW: + case CHECKBOX_SELECTION: + case CHECKBOX_GPGENCRYPTION: + widget = new QCheckBox(getResString(resId), m_pExtraControls); + break; + case PUSHBUTTON_PLAY: + break; + case LISTBOX_VERSION: + case LISTBOX_TEMPLATE: + case LISTBOX_IMAGE_ANCHOR: + case LISTBOX_IMAGE_TEMPLATE: + case LISTBOX_FILTER_SELECTOR: + label = new QLabel(getResString(resId), m_pExtraControls); + widget = new QComboBox(m_pExtraControls); + label->setBuddy(widget); + break; + case LISTBOX_VERSION_LABEL: + case LISTBOX_TEMPLATE_LABEL: + case LISTBOX_IMAGE_TEMPLATE_LABEL: + case LISTBOX_IMAGE_ANCHOR_LABEL: + break; + } + + if (widget) + { + const int row = m_pLayout->rowCount(); + if (label) + m_pLayout->addWidget(label, row, 0); + m_pLayout->addWidget(widget, row, 1); + m_aCustomWidgetsMap.insert(controlId, widget); + } +} + +void SAL_CALL QtFilePicker::initialize(const uno::Sequence<uno::Any>& args) +{ + // parameter checking + uno::Any arg; + if (args.getLength() == 0) + throw lang::IllegalArgumentException("no arguments", static_cast<XFilePicker2*>(this), 1); + + arg = args[0]; + + if ((arg.getValueType() != cppu::UnoType<sal_Int16>::get()) + && (arg.getValueType() != cppu::UnoType<sal_Int8>::get())) + { + throw lang::IllegalArgumentException("invalid argument type", + static_cast<XFilePicker2*>(this), 1); + } + + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + if (!pSalInst->IsMainThread()) + { + pSalInst->RunInMainThread([this, args]() { initialize(args); }); + return; + } + + m_aNamedFilterToExtensionMap.clear(); + m_aNamedFilterList.clear(); + m_aTitleToFilterMap.clear(); + m_aCurrentFilter.clear(); + + sal_Int16 templateId = -1; + arg >>= templateId; + + QFileDialog::AcceptMode acceptMode = QFileDialog::AcceptOpen; + switch (templateId) + { + case FILEOPEN_SIMPLE: + break; + + case FILESAVE_SIMPLE: + acceptMode = QFileDialog::AcceptSave; + break; + + case FILESAVE_AUTOEXTENSION: + acceptMode = QFileDialog::AcceptSave; + addCustomControl(CHECKBOX_AUTOEXTENSION); + break; + + case FILESAVE_AUTOEXTENSION_PASSWORD: + acceptMode = QFileDialog::AcceptSave; + addCustomControl(CHECKBOX_AUTOEXTENSION); + addCustomControl(CHECKBOX_PASSWORD); + addCustomControl(CHECKBOX_GPGENCRYPTION); + break; + + case FILESAVE_AUTOEXTENSION_PASSWORD_FILTEROPTIONS: + acceptMode = QFileDialog::AcceptSave; + addCustomControl(CHECKBOX_AUTOEXTENSION); + addCustomControl(CHECKBOX_PASSWORD); + addCustomControl(CHECKBOX_GPGENCRYPTION); + addCustomControl(CHECKBOX_FILTEROPTIONS); + break; + + case FILESAVE_AUTOEXTENSION_SELECTION: + acceptMode = QFileDialog::AcceptSave; + addCustomControl(CHECKBOX_AUTOEXTENSION); + addCustomControl(CHECKBOX_SELECTION); + break; + + case FILESAVE_AUTOEXTENSION_TEMPLATE: + acceptMode = QFileDialog::AcceptSave; + addCustomControl(CHECKBOX_AUTOEXTENSION); + addCustomControl(LISTBOX_TEMPLATE); + break; + + case FILEOPEN_LINK_PREVIEW_IMAGE_TEMPLATE: + addCustomControl(CHECKBOX_LINK); + addCustomControl(CHECKBOX_PREVIEW); + addCustomControl(LISTBOX_IMAGE_TEMPLATE); + break; + + case FILEOPEN_LINK_PREVIEW_IMAGE_ANCHOR: + addCustomControl(CHECKBOX_LINK); + addCustomControl(CHECKBOX_PREVIEW); + addCustomControl(LISTBOX_IMAGE_ANCHOR); + break; + + case FILEOPEN_PLAY: + addCustomControl(PUSHBUTTON_PLAY); + break; + + case FILEOPEN_LINK_PLAY: + addCustomControl(CHECKBOX_LINK); + addCustomControl(PUSHBUTTON_PLAY); + break; + + case FILEOPEN_READONLY_VERSION: + addCustomControl(CHECKBOX_READONLY); + addCustomControl(LISTBOX_VERSION); + break; + + case FILEOPEN_LINK_PREVIEW: + addCustomControl(CHECKBOX_LINK); + addCustomControl(CHECKBOX_PREVIEW); + break; + + case FILEOPEN_PREVIEW: + addCustomControl(CHECKBOX_PREVIEW); + break; + + default: + throw lang::IllegalArgumentException("Unknown template", + static_cast<XFilePicker2*>(this), 1); + } + + TranslateId resId; + switch (acceptMode) + { + case QFileDialog::AcceptOpen: + resId = STR_FILEDLG_OPEN; + break; + case QFileDialog::AcceptSave: + resId = STR_FILEDLG_SAVE; + m_pFileDialog->setFileMode(QFileDialog::AnyFile); + break; + } + + m_pFileDialog->setAcceptMode(acceptMode); + m_pFileDialog->setWindowTitle(getResString(resId)); + + css::uno::Reference<css::awt::XWindow> xParentWindow; + if (args.getLength() > 1) + args[1] >>= xParentWindow; + if (!xParentWindow.is()) + return; + + css::uno::Reference<css::awt::XSystemDependentWindowPeer> xSysWinPeer(xParentWindow, + css::uno::UNO_QUERY); + if (!xSysWinPeer.is()) + return; + + // the sal_*Int8 handling is strange, but it's public API - no way around + css::uno::Sequence<sal_Int8> aProcessIdent(16); + rtl_getGlobalProcessId(reinterpret_cast<sal_uInt8*>(aProcessIdent.getArray())); + uno::Any aAny + = xSysWinPeer->getWindowHandle(aProcessIdent, css::lang::SystemDependent::SYSTEM_XWINDOW); + css::awt::SystemDependentXWindow xSysWin; + aAny >>= xSysWin; + + const auto& pFrames = pSalInst->getFrames(); + const tools::Long aWindowHandle = xSysWin.WindowHandle; + const auto it + = std::find_if(pFrames.begin(), pFrames.end(), [&aWindowHandle](auto pFrame) -> bool { + const SystemEnvData* pData = pFrame->GetSystemData(); + return pData && tools::Long(pData->GetWindowHandle(pFrame)) == aWindowHandle; + }); + if (it != pFrames.end()) + m_pParentWidget = static_cast<QtFrame*>(*it)->asChild(); +} + +void SAL_CALL QtFilePicker::cancel() { m_pFileDialog->reject(); } + +void SAL_CALL QtFilePicker::disposing(const lang::EventObject& rEvent) +{ + uno::Reference<XFilePickerListener> xFilePickerListener(rEvent.Source, uno::UNO_QUERY); + + if (xFilePickerListener.is()) + { + removeFilePickerListener(xFilePickerListener); + } +} + +void SAL_CALL QtFilePicker::queryTermination(const css::lang::EventObject&) +{ + throw css::frame::TerminationVetoException(); +} + +void SAL_CALL QtFilePicker::notifyTermination(const css::lang::EventObject&) +{ + SolarMutexGuard aGuard; + m_pFileDialog->reject(); +} + +OUString SAL_CALL QtFilePicker::getImplementationName() +{ + return "com.sun.star.ui.dialogs.QtFilePicker"; +} + +sal_Bool SAL_CALL QtFilePicker::supportsService(const OUString& ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +uno::Sequence<OUString> SAL_CALL QtFilePicker::getSupportedServiceNames() +{ + return FilePicker_getSupportedServiceNames(); +} + +void QtFilePicker::updateAutomaticFileExtension() +{ + bool bSetAutoExtension + = getValue(CHECKBOX_AUTOEXTENSION, ControlActions::GET_SELECTED_ITEM).get<bool>(); + if (bSetAutoExtension) + { + QString sSuffix = m_aNamedFilterToExtensionMap.value(m_pFileDialog->selectedNameFilter()); + // string is "*.<SUFFIX>" if a specific filter was selected that has exactly one possible file extension + if (sSuffix.lastIndexOf("*.") == 0) + { + sSuffix = sSuffix.remove("*."); + m_pFileDialog->setDefaultSuffix(sSuffix); + } + else + { + // fall back to setting none otherwise + SAL_INFO( + "vcl.qt", + "Unable to retrieve unambiguous file extension. Will not add any automatically."); + bSetAutoExtension = false; + } + } + + if (!bSetAutoExtension) + m_pFileDialog->setDefaultSuffix(""); +} + +void QtFilePicker::filterSelected(const QString&) +{ + FilePickerEvent aEvent; + aEvent.ElementId = LISTBOX_FILTER; + SAL_INFO("vcl.qt", "filter changed"); + if (m_xListener.is()) + m_xListener->controlStateChanged(aEvent); +} + +void QtFilePicker::currentChanged(const QString&) +{ + FilePickerEvent aEvent; + SAL_INFO("vcl.qt", "file selection changed"); + if (m_xListener.is()) + m_xListener->fileSelectionChanged(aEvent); +} + +OUString QtFilePicker::getDirectory() +{ + uno::Sequence<OUString> seq = getSelectedFiles(); + if (seq.getLength() > 1) + seq.realloc(1); + return seq[0]; +} + +void QtFilePicker::setDescription(const OUString&) {} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtFont.cxx b/vcl/qt5/QtFont.cxx new file mode 100644 index 0000000000..e3a6c0b0a9 --- /dev/null +++ b/vcl/qt5/QtFont.cxx @@ -0,0 +1,190 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <QtFont.hxx> +#include <QtTools.hxx> + +#include <QtGui/QFont> +#include <QtGui/QRawFont> +#include <QtGui/QPainterPath> + +static inline void applyWeight(QtFont& rFont, FontWeight eWeight) +{ + switch (eWeight) + { + case WEIGHT_THIN: + rFont.setWeight(QFont::Thin); + break; + case WEIGHT_ULTRALIGHT: + rFont.setWeight(QFont::ExtraLight); + break; + case WEIGHT_LIGHT: + rFont.setWeight(QFont::Light); + break; + case WEIGHT_SEMILIGHT: + [[fallthrough]]; + case WEIGHT_NORMAL: + rFont.setWeight(QFont::Normal); + break; + case WEIGHT_MEDIUM: + rFont.setWeight(QFont::Medium); + break; + case WEIGHT_SEMIBOLD: + rFont.setWeight(QFont::DemiBold); + break; + case WEIGHT_BOLD: + rFont.setWeight(QFont::Bold); + break; + case WEIGHT_ULTRABOLD: + rFont.setWeight(QFont::ExtraBold); + break; + case WEIGHT_BLACK: + rFont.setWeight(QFont::Black); + break; + default: + break; + } +} + +static inline void applyStretch(QtFont& rFont, FontWidth eWidthType) +{ + switch (eWidthType) + { + case WIDTH_DONTKNOW: + rFont.setStretch(QFont::AnyStretch); + break; + case WIDTH_ULTRA_CONDENSED: + rFont.setStretch(QFont::UltraCondensed); + break; + case WIDTH_EXTRA_CONDENSED: + rFont.setStretch(QFont::ExtraCondensed); + break; + case WIDTH_CONDENSED: + rFont.setStretch(QFont::Condensed); + break; + case WIDTH_SEMI_CONDENSED: + rFont.setStretch(QFont::SemiCondensed); + break; + case WIDTH_NORMAL: + rFont.setStretch(QFont::Unstretched); + break; + case WIDTH_SEMI_EXPANDED: + rFont.setStretch(QFont::SemiExpanded); + break; + case WIDTH_EXPANDED: + rFont.setStretch(QFont::Expanded); + break; + case WIDTH_EXTRA_EXPANDED: + rFont.setStretch(QFont::ExtraExpanded); + break; + case WIDTH_ULTRA_EXPANDED: + rFont.setStretch(QFont::UltraExpanded); + break; + default: + break; + } +} + +static inline void applyStyle(QtFont& rFont, FontItalic eItalic) +{ + switch (eItalic) + { + case ITALIC_NONE: + rFont.setStyle(QFont::Style::StyleNormal); + break; + case ITALIC_OBLIQUE: + rFont.setStyle(QFont::Style::StyleOblique); + break; + case ITALIC_NORMAL: + rFont.setStyle(QFont::Style::StyleItalic); + break; + default: + break; + } +} + +QtFont::QtFont(const vcl::font::PhysicalFontFace& rPFF, const vcl::font::FontSelectPattern& rFSP) + : LogicalFontInstance(rPFF, rFSP) +{ + setFamily(toQString(rPFF.GetFamilyName())); + applyWeight(*this, rPFF.GetWeight()); + setPixelSize(rFSP.mnHeight); + applyStretch(*this, rPFF.GetWidthType()); + applyStyle(*this, rFSP.GetItalic()); +} + +bool QtFont::GetGlyphOutline(sal_GlyphId nId, basegfx::B2DPolyPolygon& rB2DPolyPoly, bool) const +{ + rB2DPolyPoly.clear(); + basegfx::B2DPolygon aPart; + QRawFont aRawFont(QRawFont::fromFont(*this)); + QPainterPath aQPath = aRawFont.pathForGlyph(nId); + + for (int a(0); a < aQPath.elementCount(); a++) + { + const QPainterPath::Element aQElement = aQPath.elementAt(a); + + switch (aQElement.type) + { + case QPainterPath::MoveToElement: + { + if (aPart.count()) + { + aPart.setClosed(true); + rB2DPolyPoly.append(aPart); + aPart.clear(); + } + + aPart.append(basegfx::B2DPoint(aQElement.x, aQElement.y)); + break; + } + case QPainterPath::LineToElement: + { + aPart.append(basegfx::B2DPoint(aQElement.x, aQElement.y)); + break; + } + case QPainterPath::CurveToElement: + { + const QPainterPath::Element aQ2 = aQPath.elementAt(++a); + const QPainterPath::Element aQ3 = aQPath.elementAt(++a); + aPart.appendBezierSegment(basegfx::B2DPoint(aQElement.x, aQElement.y), + basegfx::B2DPoint(aQ2.x, aQ2.y), + basegfx::B2DPoint(aQ3.x, aQ3.y)); + break; + } + case QPainterPath::CurveToDataElement: + { + break; + } + } + } + + if (aPart.count()) + { + aPart.setClosed(true); + rB2DPolyPoly.append(aPart); + aPart.clear(); + } + + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtFontFace.cxx b/vcl/qt5/QtFontFace.cxx new file mode 100644 index 0000000000..351f597395 --- /dev/null +++ b/vcl/qt5/QtFontFace.cxx @@ -0,0 +1,217 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <unotools/fontdefs.hxx> + +#include <QtFontFace.hxx> +#include <QtFont.hxx> +#include <QtTools.hxx> + +#include <font/LogicalFontInstance.hxx> +#include <font/FontSelectPattern.hxx> +#include <font/PhysicalFontCollection.hxx> + +#include <QtGui/QFont> +#include <QtGui/QFontDatabase> +#include <QtGui/QFontInfo> +#include <QtGui/QRawFont> +#include <utility> + +using namespace vcl; + +QtFontFace::QtFontFace(const QtFontFace& rSrc) + : vcl::font::PhysicalFontFace(rSrc) + , m_aFontId(rSrc.m_aFontId) + , m_eFontIdType(rSrc.m_eFontIdType) +{ +} + +FontWeight QtFontFace::toFontWeight(const int nWeight) +{ + if (nWeight <= QFont::Thin) + return WEIGHT_THIN; + if (nWeight <= QFont::ExtraLight) + return WEIGHT_ULTRALIGHT; + if (nWeight <= QFont::Light) + return WEIGHT_LIGHT; + if (nWeight <= QFont::Normal) + return WEIGHT_NORMAL; + if (nWeight <= QFont::Medium) + return WEIGHT_MEDIUM; + if (nWeight <= QFont::DemiBold) + return WEIGHT_SEMIBOLD; + if (nWeight <= QFont::Bold) + return WEIGHT_BOLD; + if (nWeight <= QFont::ExtraBold) + return WEIGHT_ULTRABOLD; + return WEIGHT_BLACK; +} + +FontWidth QtFontFace::toFontWidth(const int nStretch) +{ + if (nStretch == 0) // QFont::AnyStretch since Qt 5.8 + return WIDTH_DONTKNOW; + if (nStretch <= QFont::UltraCondensed) + return WIDTH_ULTRA_CONDENSED; + if (nStretch <= QFont::ExtraCondensed) + return WIDTH_EXTRA_CONDENSED; + if (nStretch <= QFont::Condensed) + return WIDTH_CONDENSED; + if (nStretch <= QFont::SemiCondensed) + return WIDTH_SEMI_CONDENSED; + if (nStretch <= QFont::Unstretched) + return WIDTH_NORMAL; + if (nStretch <= QFont::SemiExpanded) + return WIDTH_SEMI_EXPANDED; + if (nStretch <= QFont::Expanded) + return WIDTH_EXPANDED; + if (nStretch <= QFont::ExtraExpanded) + return WIDTH_EXTRA_EXPANDED; + return WIDTH_ULTRA_EXPANDED; +} + +FontItalic QtFontFace::toFontItalic(const QFont::Style eStyle) +{ + switch (eStyle) + { + case QFont::StyleNormal: + return ITALIC_NONE; + case QFont::StyleItalic: + return ITALIC_NORMAL; + case QFont::StyleOblique: + return ITALIC_OBLIQUE; + } + + return ITALIC_NONE; +} + +void QtFontFace::fillAttributesFromQFont(const QFont& rFont, FontAttributes& rFA) +{ + QFontInfo aFontInfo(rFont); + + rFA.SetFamilyName(toOUString(aFontInfo.family())); + rFA.SetStyleName(toOUString(aFontInfo.styleName())); + rFA.SetPitch(aFontInfo.fixedPitch() ? PITCH_FIXED : PITCH_VARIABLE); + rFA.SetWeight(QtFontFace::toFontWeight(aFontInfo.weight())); + rFA.SetItalic(QtFontFace::toFontItalic(aFontInfo.style())); + rFA.SetWidthType(QtFontFace::toFontWidth(rFont.stretch())); +} + +QtFontFace* QtFontFace::fromQFont(const QFont& rFont) +{ + FontAttributes aFA; + fillAttributesFromQFont(rFont, aFA); + return new QtFontFace(aFA, rFont.toString(), FontIdType::Font); +} + +QtFontFace* QtFontFace::fromQFontDatabase(const QString& aFamily, const QString& aStyle) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + auto const isFixedPitch = QFontDatabase::isFixedPitch(aFamily, aStyle); + auto const weigh = QFontDatabase::weight(aFamily, aStyle); + auto const italic = QFontDatabase::italic(aFamily, aStyle); + auto const aPointList = QFontDatabase::pointSizes(aFamily, aStyle); +#else + QFontDatabase aFDB; + auto const isFixedPitch = aFDB.isFixedPitch(aFamily, aStyle); + auto const weigh = aFDB.weight(aFamily, aStyle); + auto const italic = aFDB.italic(aFamily, aStyle); + auto const aPointList = aFDB.pointSizes(aFamily, aStyle); +#endif + + FontAttributes aFA; + + aFA.SetFamilyName(toOUString(aFamily)); + aFA.SetStyleName(toOUString(aStyle)); + aFA.SetPitch(isFixedPitch ? PITCH_FIXED : PITCH_VARIABLE); + aFA.SetWeight(QtFontFace::toFontWeight(weigh)); + aFA.SetItalic(italic ? ITALIC_NORMAL : ITALIC_NONE); + + int nPointSize = 0; + if (!aPointList.empty()) + nPointSize = aPointList[0]; + + return new QtFontFace(aFA, aFamily + "," + aStyle + "," + QString::number(nPointSize), + FontIdType::FontDB); +} + +QtFontFace::QtFontFace(const FontAttributes& rFA, QString aFontID, const FontIdType eFontIdType) + : PhysicalFontFace(rFA) + , m_aFontId(std::move(aFontID)) + , m_eFontIdType(eFontIdType) +{ +} + +sal_IntPtr QtFontFace::GetFontId() const { return reinterpret_cast<sal_IntPtr>(&m_aFontId); } + +QFont QtFontFace::CreateFont() const +{ + QFont aFont; + switch (m_eFontIdType) + { + case FontDB: + { + QStringList aStrList = m_aFontId.split(","); + if (3 == aStrList.size()) + { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + aFont = QFontDatabase::font(aStrList[0], aStrList[1], aStrList[2].toInt()); +#else + QFontDatabase aFDB; + aFont = aFDB.font(aStrList[0], aStrList[1], aStrList[2].toInt()); +#endif + } + else + SAL_WARN("vcl.qt", "Invalid QFontDatabase font ID " << m_aFontId); + break; + } + case Font: + bool bRet = aFont.fromString(m_aFontId); + SAL_WARN_IF(!bRet, "vcl.qt", "Failed to create QFont from ID: " << m_aFontId); + Q_UNUSED(bRet); + break; + } + return aFont; +} + +rtl::Reference<LogicalFontInstance> +QtFontFace::CreateFontInstance(const vcl::font::FontSelectPattern& rFSD) const +{ + return new QtFont(*this, rFSD); +} + +hb_blob_t* QtFontFace::GetHbTable(hb_tag_t nTag) const +{ + char pTagName[5] = { '\0' }; + hb_tag_to_string(nTag, pTagName); + + QFont aFont = CreateFont(); + QRawFont aRawFont(QRawFont::fromFont(aFont)); + QByteArray aTable = aRawFont.fontTable(pTagName); + const sal_uInt32 nLength = aTable.size(); + + hb_blob_t* pBlob = nullptr; + if (nLength > 0) + pBlob = hb_blob_create(aTable.data(), nLength, HB_MEMORY_MODE_DUPLICATE, nullptr, nullptr); + return pBlob; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtFrame.cxx b/vcl/qt5/QtFrame.cxx new file mode 100644 index 0000000000..24dcb5ff6f --- /dev/null +++ b/vcl/qt5/QtFrame.cxx @@ -0,0 +1,1520 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtFrame.hxx> +#include <QtFrame.moc> + +#include <QtData.hxx> +#include <QtDragAndDrop.hxx> +#include <QtFontFace.hxx> +#include <QtGraphics.hxx> +#include <QtInstance.hxx> +#include <QtMainWindow.hxx> +#include <QtMenu.hxx> +#include <QtSvpGraphics.hxx> +#include <QtSystem.hxx> +#include <QtTools.hxx> +#include <QtTransferable.hxx> +#if CHECK_ANY_QT_USING_X11 +#include <QtX11Support.hxx> +#endif + +#include <QtCore/QMimeData> +#include <QtCore/QPoint> +#include <QtCore/QSize> +#include <QtCore/QThread> +#include <QtGui/QDragMoveEvent> +#include <QtGui/QDropEvent> +#include <QtGui/QIcon> +#include <QtGui/QWindow> +#include <QtGui/QScreen> +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +#include <QtGui/QStyleHints> +#endif +#include <QtWidgets/QStyle> +#include <QtWidgets/QToolTip> +#include <QtWidgets/QApplication> +#include <QtWidgets/QMenuBar> +#include <QtWidgets/QMainWindow> +#if CHECK_QT5_USING_X11 +#include <QtX11Extras/QX11Info> +#endif + +#include <window.h> +#include <vcl/syswin.hxx> + +#include <com/sun/star/datatransfer/dnd/DNDConstants.hpp> + +#include <cairo.h> +#include <headless/svpgdi.hxx> + +#include <unx/fontmanager.hxx> + +static void SvpDamageHandler(void* handle, sal_Int32 nExtentsX, sal_Int32 nExtentsY, + sal_Int32 nExtentsWidth, sal_Int32 nExtentsHeight) +{ + QtFrame* pThis = static_cast<QtFrame*>(handle); + pThis->Damage(nExtentsX, nExtentsY, nExtentsWidth, nExtentsHeight); +} + +namespace +{ +sal_Int32 screenNumber(const QScreen* pScreen) +{ + const QList<QScreen*> screens = QApplication::screens(); + + sal_Int32 nScreen = 0; + bool bFound = false; + for (const QScreen* pCurScreen : screens) + { + if (pScreen == pCurScreen) + { + bFound = true; + break; + } + nScreen++; + } + + return bFound ? nScreen : -1; +} +} + +QtFrame::QtFrame(QtFrame* pParent, SalFrameStyleFlags nStyle, bool bUseCairo) + : m_pTopLevel(nullptr) + , m_bUseCairo(bUseCairo) + , m_bNullRegion(true) + , m_bGraphicsInUse(false) + , m_ePointerStyle(PointerStyle::Arrow) + , m_pDragSource(nullptr) + , m_pDropTarget(nullptr) + , m_bInDrag(false) + , m_bDefaultSize(true) + , m_bDefaultPos(true) + , m_bFullScreen(false) + , m_bFullScreenSpanAll(false) +#if CHECK_ANY_QT_USING_X11 + , m_nKeyModifiers(ModKeyFlags::NONE) +#endif + , m_nInputLanguage(LANGUAGE_DONTKNOW) +{ + QtInstance* pInst = GetQtInstance(); + pInst->insertFrame(this); + + m_aDamageHandler.handle = this; + m_aDamageHandler.damaged = ::SvpDamageHandler; + + if (nStyle & SalFrameStyleFlags::DEFAULT) // ensure default style + { + nStyle |= SalFrameStyleFlags::MOVEABLE | SalFrameStyleFlags::SIZEABLE + | SalFrameStyleFlags::CLOSEABLE; + nStyle &= ~SalFrameStyleFlags::FLOAT; + } + + m_nStyle = nStyle; + m_pParent = pParent; + + Qt::WindowFlags aWinFlags(Qt::Widget); + if (!(nStyle & SalFrameStyleFlags::SYSTEMCHILD)) + { + if (nStyle & SalFrameStyleFlags::INTRO) + aWinFlags = Qt::SplashScreen; + // floating toolbars are frameless tool windows + // + they must be able to receive keyboard focus + else if ((nStyle & SalFrameStyleFlags::FLOAT) + && (nStyle & SalFrameStyleFlags::OWNERDRAWDECORATION)) + aWinFlags = Qt::Tool | Qt::FramelessWindowHint; + else if (nStyle & SalFrameStyleFlags::TOOLTIP) + aWinFlags = Qt::ToolTip; + // Can't use Qt::Popup, because it grabs the input focus and generates a focus-out event, + // instantly auto-closing the LO's editable ComboBox popup. + // On X11, the alternative Qt::Window | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint + // seems to work well enough, but at least on Wayland and WASM, this results in problems. + // So while using Qt::ToolTip, the popups are wrongly advertised via accessibility, at least + // the GUI seems to work on all platforms... what a mess. + else if (isPopup()) + aWinFlags = Qt::ToolTip | Qt::FramelessWindowHint; + else if (nStyle & SalFrameStyleFlags::TOOLWINDOW) + aWinFlags = Qt::Tool; + // top level windows can't be transient in Qt, so make them dialogs, if they have a parent. At least + // the plasma shell relies on this setting to skip dialogs in the window list. And Qt Xcb will just + // set transient for the types Dialog, Sheet, Tool, SplashScreen, ToolTip, Drawer and Popup. + else if (nStyle & SalFrameStyleFlags::DIALOG || m_pParent) + aWinFlags = Qt::Dialog; + else + aWinFlags = Qt::Window; + } + + if (aWinFlags == Qt::Window) + { + m_pTopLevel = new QtMainWindow(*this, aWinFlags); + m_pQWidget = new QtWidget(*this); + m_pTopLevel->setCentralWidget(m_pQWidget); + m_pTopLevel->setFocusProxy(m_pQWidget); + } + else + { + m_pQWidget = new QtWidget(*this, aWinFlags); + // from Qt's POV the popup window doesn't have the input focus, so we must force tooltips... + if (isPopup()) + m_pQWidget->setAttribute(Qt::WA_AlwaysShowToolTips); + } + + FillSystemEnvData(m_aSystemData, reinterpret_cast<sal_IntPtr>(this), m_pQWidget); + + QWindow* pChildWindow = windowHandle(); + connect(pChildWindow, &QWindow::screenChanged, this, &QtFrame::screenChanged); + + if (pParent && !(pParent->m_nStyle & SalFrameStyleFlags::PLUG)) + { + QWindow* pParentWindow = pParent->windowHandle(); + if (pParentWindow && pChildWindow && (pParentWindow != pChildWindow)) + pChildWindow->setTransientParent(pParentWindow); + } + + SetIcon(SV_ICON_ID_OFFICE); +} + +void QtFrame::screenChanged(QScreen*) { m_pQWidget->fakeResize(); } + +void QtFrame::FillSystemEnvData(SystemEnvData& rData, sal_IntPtr pWindow, QWidget* pWidget) +{ + assert(rData.platform == SystemEnvData::Platform::Invalid); + assert(rData.toolkit == SystemEnvData::Toolkit::Invalid); + if (QGuiApplication::platformName() == "wayland") + rData.platform = SystemEnvData::Platform::Wayland; + else if (QGuiApplication::platformName() == "xcb") + rData.platform = SystemEnvData::Platform::Xcb; + else if (QGuiApplication::platformName() == "wasm") + rData.platform = SystemEnvData::Platform::WASM; + else + { + // maybe add a SystemEnvData::Platform::Unsupported to avoid special cases and not abort? + SAL_WARN("vcl.qt", + "Unsupported qt VCL platform: " << toOUString(QGuiApplication::platformName())); + std::abort(); + } + + rData.toolkit = SystemEnvData::Toolkit::Qt; + rData.aShellWindow = pWindow; + rData.pWidget = pWidget; +} + +QtFrame::~QtFrame() +{ + QtInstance* pInst = GetQtInstance(); + pInst->eraseFrame(this); + delete asChild(); + m_aSystemData.aShellWindow = 0; +} + +void QtFrame::Damage(sal_Int32 nExtentsX, sal_Int32 nExtentsY, sal_Int32 nExtentsWidth, + sal_Int32 nExtentsHeight) const +{ + m_pQWidget->update(scaledQRect(QRect(nExtentsX, nExtentsY, nExtentsWidth, nExtentsHeight), + 1 / devicePixelRatioF())); +} + +SalGraphics* QtFrame::AcquireGraphics() +{ + if (m_bGraphicsInUse) + return nullptr; + + m_bGraphicsInUse = true; + + if (m_bUseCairo) + { + if (!m_pSvpGraphics) + { + QSize aSize = m_pQWidget->size() * devicePixelRatioF(); + m_pSvpGraphics.reset(new QtSvpGraphics(this)); + m_pSurface.reset( + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, aSize.width(), aSize.height())); + m_pSvpGraphics->setSurface(m_pSurface.get(), + basegfx::B2IVector(aSize.width(), aSize.height())); + cairo_surface_set_user_data(m_pSurface.get(), QtSvpGraphics::getDamageKey(), + &m_aDamageHandler, nullptr); + } + return m_pSvpGraphics.get(); + } + else + { + if (!m_pQtGraphics) + { + m_pQtGraphics.reset(new QtGraphics(this)); + m_pQImage.reset( + new QImage(m_pQWidget->size() * devicePixelRatioF(), Qt_DefaultFormat32)); + m_pQImage->fill(Qt::transparent); + m_pQtGraphics->ChangeQImage(m_pQImage.get()); + } + return m_pQtGraphics.get(); + } +} + +void QtFrame::ReleaseGraphics(SalGraphics* pSalGraph) +{ + (void)pSalGraph; + if (m_bUseCairo) + assert(pSalGraph == m_pSvpGraphics.get()); + else + assert(pSalGraph == m_pQtGraphics.get()); + m_bGraphicsInUse = false; +} + +bool QtFrame::PostEvent(std::unique_ptr<ImplSVEvent> pData) +{ + QtInstance* pInst = GetQtInstance(); + pInst->PostEvent(this, pData.release(), SalEvent::UserEvent); + return true; +} + +QWidget* QtFrame::asChild() const +{ + if (m_pTopLevel) + return m_pTopLevel; + return m_pQWidget; +} + +qreal QtFrame::devicePixelRatioF() const { return asChild()->devicePixelRatioF(); } + +bool QtFrame::isWindow() const { return asChild()->isWindow(); } + +QWindow* QtFrame::windowHandle() const +{ + // set attribute 'Qt::WA_NativeWindow' first to make sure a window handle actually exists + QWidget* pChild = asChild(); + assert(pChild->window() == pChild); + switch (m_aSystemData.platform) + { + case SystemEnvData::Platform::Wayland: + case SystemEnvData::Platform::Xcb: + pChild->setAttribute(Qt::WA_NativeWindow); + break; + case SystemEnvData::Platform::WASM: + // no idea, why Qt::WA_NativeWindow breaks the menubar for EMSCRIPTEN + break; + case SystemEnvData::Platform::Invalid: + std::abort(); + break; + } + return pChild->windowHandle(); +} + +QScreen* QtFrame::screen() const { return asChild()->screen(); } + +bool QtFrame::GetUseDarkMode() const +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + const QStyleHints* pStyleHints = QApplication::styleHints(); + return pStyleHints->colorScheme() == Qt::ColorScheme::Dark; +#else + // use same mechanism for determining dark mode preference as xdg-desktop-portal-kde, s. + // https://invent.kde.org/plasma/xdg-desktop-portal-kde/-/blob/0a4237549debf9518f8cfbaf531456850c0729bd/src/settings.cpp#L213-227 + const QPalette aPalette = QApplication::palette(); + const int nWindowBackGroundGray = qGray(aPalette.window().color().rgb()); + return nWindowBackGroundGray < 192; +#endif +} + +bool QtFrame::isMinimized() const { return asChild()->isMinimized(); } + +bool QtFrame::isMaximized() const { return asChild()->isMaximized(); } + +void QtFrame::SetWindowStateImpl(Qt::WindowStates eState) +{ + return asChild()->setWindowState(eState); +} + +void QtFrame::SetTitle(const OUString& rTitle) +{ + QtInstance* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread( + [this, rTitle]() { m_pQWidget->window()->setWindowTitle(toQString(rTitle)); }); +} + +void QtFrame::SetIcon(sal_uInt16 nIcon) +{ + if (m_nStyle + & (SalFrameStyleFlags::PLUG | SalFrameStyleFlags::SYSTEMCHILD + | SalFrameStyleFlags::FLOAT | SalFrameStyleFlags::INTRO + | SalFrameStyleFlags::OWNERDRAWDECORATION) + || !isWindow()) + return; + + QString appicon; + + if (nIcon == SV_ICON_ID_TEXT) + appicon = "libreoffice-writer"; + else if (nIcon == SV_ICON_ID_SPREADSHEET) + appicon = "libreoffice-calc"; + else if (nIcon == SV_ICON_ID_DRAWING) + appicon = "libreoffice-draw"; + else if (nIcon == SV_ICON_ID_PRESENTATION) + appicon = "libreoffice-impress"; + else if (nIcon == SV_ICON_ID_DATABASE) + appicon = "libreoffice-base"; + else if (nIcon == SV_ICON_ID_FORMULA) + appicon = "libreoffice-math"; + else + appicon = "libreoffice-startcenter"; + + QIcon aIcon = QIcon::fromTheme(appicon); + m_pQWidget->window()->setWindowIcon(aIcon); +} + +void QtFrame::SetMenu(SalMenu*) {} + +void QtFrame::SetExtendedFrameStyle(SalExtStyle /*nExtStyle*/) { /* not needed */} + +void QtFrame::Show(bool bVisible, bool bNoActivate) +{ + assert(m_pQWidget); + if (bVisible == asChild()->isVisible()) + return; + + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + + if (!bVisible) // hide + { + pSalInst->RunInMainThread([this]() { asChild()->setVisible(false); }); + return; + } + + // show + SetDefaultSize(); + + pSalInst->RunInMainThread([this, bNoActivate]() { + QWidget* const pChild = asChild(); + pChild->setVisible(true); + pChild->raise(); + if (!bNoActivate) + { + pChild->activateWindow(); + pChild->setFocus(); + } + }); +} + +void QtFrame::SetMinClientSize(tools::Long nWidth, tools::Long nHeight) +{ + if (!isChild()) + { + const qreal fRatio = devicePixelRatioF(); + asChild()->setMinimumSize(round(nWidth / fRatio), round(nHeight / fRatio)); + } +} + +void QtFrame::SetMaxClientSize(tools::Long nWidth, tools::Long nHeight) +{ + if (!isChild()) + { + const qreal fRatio = devicePixelRatioF(); + asChild()->setMaximumSize(round(nWidth / fRatio), round(nHeight / fRatio)); + } +} + +int QtFrame::menuBarOffset() const +{ + QtMainWindow* pTopLevel = m_pParent->GetTopLevelWindow(); + if (pTopLevel && pTopLevel->menuBar() && pTopLevel->menuBar()->isVisible()) + return round(pTopLevel->menuBar()->geometry().height() * devicePixelRatioF()); + return 0; +} + +void QtFrame::SetDefaultPos() +{ + if (!m_bDefaultPos) + return; + + // center on parent + if (m_pParent) + { + const qreal fRatio = devicePixelRatioF(); + QWidget* const pParentWin = m_pParent->asChild()->window(); + QWidget* const pChildWin = asChild()->window(); + QPoint aPos = (pParentWin->rect().center() - pChildWin->rect().center()) * fRatio; + aPos.ry() -= menuBarOffset(); + SetPosSize(aPos.x(), aPos.y(), 0, 0, SAL_FRAME_POSSIZE_X | SAL_FRAME_POSSIZE_Y); + assert(!m_bDefaultPos); + } + else + m_bDefaultPos = false; +} + +Size QtFrame::CalcDefaultSize() +{ + assert(isWindow()); + + Size aSize; + if (!m_bFullScreen) + { + const QScreen* pScreen = screen(); + if (!pScreen) + pScreen = QGuiApplication::screens().at(0); + aSize = bestmaxFrameSizeForScreenSize(toSize(pScreen->size())); + } + else + { + if (!m_bFullScreenSpanAll) + { + aSize = toSize(QGuiApplication::screens().at(maGeometry.screen())->size()); + } + else + { + QScreen* pScreen = QGuiApplication::screenAt(QPoint(0, 0)); + aSize = toSize(pScreen->availableVirtualGeometry().size()); + } + } + + return aSize; +} + +void QtFrame::SetDefaultSize() +{ + if (!m_bDefaultSize) + return; + + Size aDefSize = CalcDefaultSize(); + SetPosSize(0, 0, aDefSize.Width(), aDefSize.Height(), + SAL_FRAME_POSSIZE_WIDTH | SAL_FRAME_POSSIZE_HEIGHT); + assert(!m_bDefaultSize); +} + +void QtFrame::SetPosSize(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, + sal_uInt16 nFlags) +{ + if (!isWindow() || isChild(true, false)) + return; + + if (nFlags & (SAL_FRAME_POSSIZE_WIDTH | SAL_FRAME_POSSIZE_HEIGHT)) + { + if (isChild(false) || !m_pQWidget->isMaximized()) + { + if (!(nFlags & SAL_FRAME_POSSIZE_WIDTH)) + nWidth = maGeometry.width(); + else if (!(nFlags & SAL_FRAME_POSSIZE_HEIGHT)) + nHeight = maGeometry.height(); + + if (nWidth > 0 && nHeight > 0) + { + m_bDefaultSize = false; + const int nNewWidth = round(nWidth / devicePixelRatioF()); + const int nNewHeight = round(nHeight / devicePixelRatioF()); + if (m_nStyle & SalFrameStyleFlags::SIZEABLE) + asChild()->resize(nNewWidth, nNewHeight); + else + asChild()->setFixedSize(nNewWidth, nNewHeight); + } + + // assume the resize happened + // needed for calculations and will eventually be corrected by events + if (nWidth > 0) + maGeometry.setWidth(nWidth); + if (nHeight > 0) + maGeometry.setHeight(nHeight); + } + } + + if (!(nFlags & (SAL_FRAME_POSSIZE_X | SAL_FRAME_POSSIZE_Y))) + { + if (nFlags & (SAL_FRAME_POSSIZE_WIDTH | SAL_FRAME_POSSIZE_HEIGHT)) + SetDefaultPos(); + return; + } + + if (m_pParent) + { + const SalFrameGeometry& aParentGeometry = m_pParent->maGeometry; + if (QGuiApplication::isRightToLeft()) + nX = aParentGeometry.x() + aParentGeometry.width() - nX - maGeometry.width() - 1; + else + nX += aParentGeometry.x(); + nY += aParentGeometry.y() + menuBarOffset(); + } + + if (!(nFlags & SAL_FRAME_POSSIZE_X)) + nX = maGeometry.x(); + else if (!(nFlags & SAL_FRAME_POSSIZE_Y)) + nY = maGeometry.y(); + + // assume the reposition happened + // needed for calculations and will eventually be corrected by events later + maGeometry.setPos({ nX, nY }); + + m_bDefaultPos = false; + asChild()->move(round(nX / devicePixelRatioF()), round(nY / devicePixelRatioF())); +} + +void QtFrame::GetClientSize(tools::Long& rWidth, tools::Long& rHeight) +{ + rWidth = round(m_pQWidget->width() * devicePixelRatioF()); + rHeight = round(m_pQWidget->height() * devicePixelRatioF()); +} + +void QtFrame::GetWorkArea(AbsoluteScreenPixelRectangle& rRect) +{ + if (!isWindow()) + return; + QScreen* pScreen = screen(); + if (!pScreen) + return; + + QSize aSize = pScreen->availableVirtualSize() * devicePixelRatioF(); + rRect = AbsoluteScreenPixelRectangle(0, 0, aSize.width(), aSize.height()); +} + +SalFrame* QtFrame::GetParent() const { return m_pParent; } + +void QtFrame::SetModal(bool bModal) +{ + if (!isWindow() || asChild()->isModal() == bModal) + return; + + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread([this, bModal]() { + + QWidget* const pChild = asChild(); + const bool bWasVisible = pChild->isVisible(); + + // modality change is only effective if the window is hidden + if (bWasVisible) + { + pChild->hide(); + if (QGuiApplication::platformName() == "xcb") + { + SAL_WARN("vcl.qt", "SetModal called after Show - apply delay"); + // tdf#152979 give QXcbConnection some time to avoid + // "qt.qpa.xcb: internal error: void QXcbWindow::setNetWmStateOnUnmappedWindow() called on mapped window" + QThread::msleep(100); + } + } + + pChild->setWindowModality(bModal ? Qt::WindowModal : Qt::NonModal); + + if (bWasVisible) + pChild->show(); + }); +} + +bool QtFrame::GetModal() const { return isWindow() && windowHandle()->isModal(); } + +void QtFrame::SetWindowState(const vcl::WindowData* pState) +{ + if (!isWindow() || !pState || isChild(true, false)) + return; + + const vcl::WindowDataMask nMaxGeometryMask + = vcl::WindowDataMask::PosSize | vcl::WindowDataMask::MaximizedX + | vcl::WindowDataMask::MaximizedY | vcl::WindowDataMask::MaximizedWidth + | vcl::WindowDataMask::MaximizedHeight; + + if ((pState->mask() & vcl::WindowDataMask::State) + && (pState->state() & vcl::WindowState::Maximized) && !isMaximized() + && (pState->mask() & nMaxGeometryMask) == nMaxGeometryMask) + { + const qreal fRatio = devicePixelRatioF(); + QWidget* const pChild = asChild(); + pChild->resize(ceil(pState->width() / fRatio), ceil(pState->height() / fRatio)); + pChild->move(ceil(pState->x() / fRatio), ceil(pState->y() / fRatio)); + SetWindowStateImpl(Qt::WindowMaximized); + } + else if (pState->mask() & vcl::WindowDataMask::PosSize) + { + sal_uInt16 nPosSizeFlags = 0; + if (pState->mask() & vcl::WindowDataMask::X) + nPosSizeFlags |= SAL_FRAME_POSSIZE_X; + if (pState->mask() & vcl::WindowDataMask::Y) + nPosSizeFlags |= SAL_FRAME_POSSIZE_Y; + if (pState->mask() & vcl::WindowDataMask::Width) + nPosSizeFlags |= SAL_FRAME_POSSIZE_WIDTH; + if (pState->mask() & vcl::WindowDataMask::Height) + nPosSizeFlags |= SAL_FRAME_POSSIZE_HEIGHT; + SetPosSize(pState->x(), pState->y(), pState->width(), pState->height(), nPosSizeFlags); + } + else if (pState->mask() & vcl::WindowDataMask::State && !isChild()) + { + if (pState->state() & vcl::WindowState::Maximized) + SetWindowStateImpl(Qt::WindowMaximized); + else if (pState->state() & vcl::WindowState::Minimized) + SetWindowStateImpl(Qt::WindowMinimized); + else + SetWindowStateImpl(Qt::WindowNoState); + } +} + +bool QtFrame::GetWindowState(vcl::WindowData* pState) +{ + pState->setState(vcl::WindowState::Normal); + pState->setMask(vcl::WindowDataMask::State); + if (isMinimized()) + pState->rState() |= vcl::WindowState::Minimized; + else if (isMaximized()) + pState->rState() |= vcl::WindowState::Maximized; + else + { + // we want the frame position and the client area size + QRect rect = scaledQRect({ asChild()->pos(), asChild()->size() }, devicePixelRatioF()); + pState->setPosSize(toRectangle(rect)); + pState->rMask() |= vcl::WindowDataMask::PosSize; + } + + return true; +} + +void QtFrame::ShowFullScreen(bool bFullScreen, sal_Int32 nScreen) +{ + // only top-level windows can go fullscreen + assert(m_pTopLevel); + + if (m_bFullScreen == bFullScreen) + return; + + m_bFullScreen = bFullScreen; + m_bFullScreenSpanAll = m_bFullScreen && (nScreen < 0); + + // show it if it isn't shown yet + if (!isWindow()) + m_pTopLevel->show(); + + if (m_bFullScreen) + { + m_aRestoreGeometry = m_pTopLevel->geometry(); + m_nRestoreScreen = maGeometry.screen(); + SetScreenNumber(m_bFullScreenSpanAll ? m_nRestoreScreen : nScreen); + if (!m_bFullScreenSpanAll) + windowHandle()->showFullScreen(); + else + windowHandle()->showNormal(); + } + else + { + SetScreenNumber(m_nRestoreScreen); + windowHandle()->showNormal(); + m_pTopLevel->setGeometry(m_aRestoreGeometry); + } +} + +void QtFrame::StartPresentation(bool bStart) +{ +#if CHECK_ANY_QT_USING_X11 + // meh - so there's no Qt platform independent solution + // https://forum.qt.io/topic/38504/solved-qdialog-in-fullscreen-disable-os-screensaver + assert(m_aSystemData.platform != SystemEnvData::Platform::Invalid); + unsigned int nRootWindow(0); + std::optional<Display*> aDisplay; + +#if CHECK_QT5_USING_X11 + if (QX11Info::isPlatformX11()) + { + nRootWindow = QX11Info::appRootWindow(); + aDisplay = QX11Info::display(); + } +#endif + + m_SessionManagerInhibitor.inhibit(bStart, u"presentation", APPLICATION_INHIBIT_IDLE, + nRootWindow, aDisplay); +#else + Q_UNUSED(bStart) +#endif +} + +void QtFrame::SetAlwaysOnTop(bool bOnTop) +{ + QWidget* const pWidget = asChild(); + const Qt::WindowFlags flags = pWidget->windowFlags(); + if (bOnTop) + pWidget->setWindowFlags(flags | Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint); + else + pWidget->setWindowFlags(flags & ~(Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint)); +} + +void QtFrame::ToTop(SalFrameToTop nFlags) +{ + QtInstance* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread([this, nFlags]() { + QWidget* const pWidget = asChild(); + if (isWindow() && !(nFlags & SalFrameToTop::GrabFocusOnly)) + pWidget->raise(); + if ((nFlags & SalFrameToTop::RestoreWhenMin) || (nFlags & SalFrameToTop::ForegroundTask)) + { + if (nFlags & SalFrameToTop::RestoreWhenMin) + pWidget->setWindowState(pWidget->windowState() & ~Qt::WindowMinimized); + pWidget->activateWindow(); + } + else if ((nFlags & SalFrameToTop::GrabFocus) || (nFlags & SalFrameToTop::GrabFocusOnly)) + { + if (!(nFlags & SalFrameToTop::GrabFocusOnly)) + pWidget->activateWindow(); + pWidget->setFocus(Qt::OtherFocusReason); + } + }); +} + +void QtFrame::SetPointer(PointerStyle ePointerStyle) +{ + if (ePointerStyle == m_ePointerStyle) + return; + m_ePointerStyle = ePointerStyle; + + m_pQWidget->setCursor(GetQtData()->getCursor(ePointerStyle)); +} + +void QtFrame::CaptureMouse(bool bMouse) +{ + static const char* pEnv = getenv("SAL_NO_MOUSEGRABS"); + if (pEnv && *pEnv) + return; + + if (bMouse) + m_pQWidget->grabMouse(); + else + m_pQWidget->releaseMouse(); +} + +void QtFrame::SetPointerPos(tools::Long nX, tools::Long nY) +{ + // some cursor already exists (and it has m_ePointerStyle shape) + // so here we just reposition it + QCursor::setPos(m_pQWidget->mapToGlobal(QPoint(nX, nY) / devicePixelRatioF())); +} + +void QtFrame::Flush() +{ + // was: QGuiApplication::sync(); + // but FIXME it causes too many issues, figure out sth better + + // unclear if we need to also flush cairo surface - gtk3 backend + // does not do it. QPainter in QtWidget::paintEvent() is + // destroyed, so that state should be safely flushed. +} + +bool QtFrame::ShowTooltip(const OUString& rText, const tools::Rectangle& rHelpArea) +{ + QRect aHelpArea(toQRect(rHelpArea)); + if (QGuiApplication::isRightToLeft()) + aHelpArea.moveLeft(maGeometry.width() - aHelpArea.width() - aHelpArea.left() - 1); + m_aTooltipText = rText; + m_aTooltipArea = aHelpArea; + return true; +} + +void QtFrame::SetInputContext(SalInputContext* pContext) +{ + if (!pContext) + return; + + if (!(pContext->mnOptions & InputContextFlags::Text)) + return; + + m_pQWidget->setAttribute(Qt::WA_InputMethodEnabled); +} + +void QtFrame::EndExtTextInput(EndExtTextInputFlags /*nFlags*/) +{ + if (m_pQWidget) + m_pQWidget->endExtTextInput(); +} + +OUString QtFrame::GetKeyName(sal_uInt16 nKeyCode) +{ + vcl::KeyCode vclKeyCode(nKeyCode); + int nCode = vclKeyCode.GetCode(); + int nRetCode = 0; + + if (nCode >= KEY_0 && nCode <= KEY_9) + nRetCode = (nCode - KEY_0) + Qt::Key_0; + else if (nCode >= KEY_A && nCode <= KEY_Z) + nRetCode = (nCode - KEY_A) + Qt::Key_A; + else if (nCode >= KEY_F1 && nCode <= KEY_F26) + nRetCode = (nCode - KEY_F1) + Qt::Key_F1; + else + { + switch (nCode) + { + case KEY_DOWN: + nRetCode = Qt::Key_Down; + break; + case KEY_UP: + nRetCode = Qt::Key_Up; + break; + case KEY_LEFT: + nRetCode = Qt::Key_Left; + break; + case KEY_RIGHT: + nRetCode = Qt::Key_Right; + break; + case KEY_HOME: + nRetCode = Qt::Key_Home; + break; + case KEY_END: + nRetCode = Qt::Key_End; + break; + case KEY_PAGEUP: + nRetCode = Qt::Key_PageUp; + break; + case KEY_PAGEDOWN: + nRetCode = Qt::Key_PageDown; + break; + case KEY_RETURN: + nRetCode = Qt::Key_Return; + break; + case KEY_ESCAPE: + nRetCode = Qt::Key_Escape; + break; + case KEY_TAB: + nRetCode = Qt::Key_Tab; + break; + case KEY_BACKSPACE: + nRetCode = Qt::Key_Backspace; + break; + case KEY_SPACE: + nRetCode = Qt::Key_Space; + break; + case KEY_INSERT: + nRetCode = Qt::Key_Insert; + break; + case KEY_DELETE: + nRetCode = Qt::Key_Delete; + break; + case KEY_ADD: + nRetCode = Qt::Key_Plus; + break; + case KEY_SUBTRACT: + nRetCode = Qt::Key_Minus; + break; + case KEY_MULTIPLY: + nRetCode = Qt::Key_Asterisk; + break; + case KEY_DIVIDE: + nRetCode = Qt::Key_Slash; + break; + case KEY_POINT: + nRetCode = Qt::Key_Period; + break; + case KEY_COMMA: + nRetCode = Qt::Key_Comma; + break; + case KEY_LESS: + nRetCode = Qt::Key_Less; + break; + case KEY_GREATER: + nRetCode = Qt::Key_Greater; + break; + case KEY_EQUAL: + nRetCode = Qt::Key_Equal; + break; + case KEY_FIND: + nRetCode = Qt::Key_Find; + break; + case KEY_CONTEXTMENU: + nRetCode = Qt::Key_Menu; + break; + case KEY_HELP: + nRetCode = Qt::Key_Help; + break; + case KEY_UNDO: + nRetCode = Qt::Key_Undo; + break; + case KEY_REPEAT: + nRetCode = Qt::Key_Redo; + break; + case KEY_TILDE: + nRetCode = Qt::Key_AsciiTilde; + break; + case KEY_QUOTELEFT: + nRetCode = Qt::Key_QuoteLeft; + break; + case KEY_BRACKETLEFT: + nRetCode = Qt::Key_BracketLeft; + break; + case KEY_BRACKETRIGHT: + nRetCode = Qt::Key_BracketRight; + break; + case KEY_NUMBERSIGN: + nRetCode = Qt::Key_NumberSign; + break; + case KEY_XF86FORWARD: + nRetCode = Qt::Key_Forward; + break; + case KEY_XF86BACK: + nRetCode = Qt::Key_Back; + break; + case KEY_COLON: + nRetCode = Qt::Key_Colon; + break; + case KEY_SEMICOLON: + nRetCode = Qt::Key_Semicolon; + break; + + // Special cases + case KEY_COPY: + nRetCode = Qt::Key_Copy; + break; + case KEY_CUT: + nRetCode = Qt::Key_Cut; + break; + case KEY_PASTE: + nRetCode = Qt::Key_Paste; + break; + case KEY_OPEN: + nRetCode = Qt::Key_Open; + break; + } + } + + if (vclKeyCode.IsShift()) + nRetCode += Qt::SHIFT; + if (vclKeyCode.IsMod1()) + nRetCode += Qt::CTRL; + if (vclKeyCode.IsMod2()) + nRetCode += Qt::ALT; + + QKeySequence keySeq(nRetCode); + OUString sKeyName = toOUString(keySeq.toString()); + + return sKeyName; +} + +bool QtFrame::MapUnicodeToKeyCode(sal_Unicode /*aUnicode*/, LanguageType /*aLangType*/, + vcl::KeyCode& /*rKeyCode*/) +{ + // not supported yet + return false; +} + +LanguageType QtFrame::GetInputLanguage() { return m_nInputLanguage; } + +void QtFrame::setInputLanguage(LanguageType nInputLanguage) +{ + if (nInputLanguage == m_nInputLanguage) + return; + m_nInputLanguage = nInputLanguage; + CallCallback(SalEvent::InputLanguageChange, nullptr); +} + +static Color toColor(const QColor& rColor) +{ + return Color(rColor.red(), rColor.green(), rColor.blue()); +} + +static bool toVclFont(const QFont& rQFont, const css::lang::Locale& rLocale, vcl::Font& rVclFont) +{ + FontAttributes aFA; + QtFontFace::fillAttributesFromQFont(rQFont, aFA); + + bool bFound = psp::PrintFontManager::get().matchFont(aFA, rLocale); + SAL_INFO("vcl.qt", "font match result for '" + << rQFont.family() << "': " + << (bFound ? OUString::Concat("'") + aFA.GetFamilyName() + "'" + : OUString("failed"))); + + if (!bFound) + return false; + + QFontInfo qFontInfo(rQFont); + int nPointHeight = qFontInfo.pointSize(); + if (nPointHeight <= 0) + nPointHeight = rQFont.pointSize(); + + vcl::Font aFont(aFA.GetFamilyName(), Size(0, nPointHeight)); + if (aFA.GetWeight() != WEIGHT_DONTKNOW) + aFont.SetWeight(aFA.GetWeight()); + if (aFA.GetWidthType() != WIDTH_DONTKNOW) + aFont.SetWidthType(aFA.GetWidthType()); + if (aFA.GetItalic() != ITALIC_DONTKNOW) + aFont.SetItalic(aFA.GetItalic()); + if (aFA.GetPitch() != PITCH_DONTKNOW) + aFont.SetPitch(aFA.GetPitch()); + + rVclFont = aFont; + return true; +} + +void QtFrame::UpdateSettings(AllSettings& rSettings) +{ + if (QtData::noNativeControls()) + return; + + StyleSettings style(rSettings.GetStyleSettings()); + const css::lang::Locale aLocale = rSettings.GetUILanguageTag().getLocale(); + + // General settings + QPalette pal = QApplication::palette(); + + style.SetToolbarIconSize(ToolbarIconSize::Large); + + Color aFore = toColor(pal.color(QPalette::Active, QPalette::WindowText)); + Color aBack = toColor(pal.color(QPalette::Active, QPalette::Window)); + Color aText = toColor(pal.color(QPalette::Active, QPalette::Text)); + Color aBase = toColor(pal.color(QPalette::Active, QPalette::Base)); + Color aButn = toColor(pal.color(QPalette::Active, QPalette::ButtonText)); + Color aMid = toColor(pal.color(QPalette::Active, QPalette::Mid)); + Color aHigh = toColor(pal.color(QPalette::Active, QPalette::Highlight)); + Color aHighText = toColor(pal.color(QPalette::Active, QPalette::HighlightedText)); + Color aLink = toColor(pal.color(QPalette::Active, QPalette::Link)); + Color aVisitedLink = toColor(pal.color(QPalette::Active, QPalette::LinkVisited)); + + style.SetSkipDisabledInMenus(true); + + // Foreground + style.SetRadioCheckTextColor(aFore); + style.SetLabelTextColor(aFore); + style.SetDialogTextColor(aFore); + style.SetGroupTextColor(aFore); + + // Text + style.SetFieldTextColor(aText); + style.SetFieldRolloverTextColor(aText); + style.SetListBoxWindowTextColor(aText); + style.SetWindowTextColor(aText); + style.SetToolTextColor(aText); + + // Base + style.SetFieldColor(aBase); + style.SetWindowColor(aBase); + style.SetActiveTabColor(aBase); + style.SetListBoxWindowBackgroundColor(aBase); + style.SetAlternatingRowColor(toColor(pal.color(QPalette::Active, QPalette::AlternateBase))); + + // Buttons + style.SetDefaultButtonTextColor(aButn); + style.SetButtonTextColor(aButn); + style.SetDefaultActionButtonTextColor(aButn); + style.SetActionButtonTextColor(aButn); + style.SetFlatButtonTextColor(aButn); + style.SetDefaultButtonRolloverTextColor(aButn); + style.SetButtonRolloverTextColor(aButn); + style.SetDefaultActionButtonRolloverTextColor(aButn); + style.SetActionButtonRolloverTextColor(aButn); + style.SetFlatButtonRolloverTextColor(aButn); + style.SetDefaultButtonPressedRolloverTextColor(aButn); + style.SetButtonPressedRolloverTextColor(aButn); + style.SetDefaultActionButtonPressedRolloverTextColor(aButn); + style.SetActionButtonPressedRolloverTextColor(aButn); + style.SetFlatButtonPressedRolloverTextColor(aButn); + + // Tabs + style.SetTabTextColor(aButn); + style.SetTabRolloverTextColor(aButn); + style.SetTabHighlightTextColor(aButn); + + // Disable color + style.SetDisableColor(toColor(pal.color(QPalette::Disabled, QPalette::WindowText))); + + // Background + style.BatchSetBackgrounds(aBack); + style.SetInactiveTabColor(aBack); + + // Workspace + style.SetWorkspaceColor(aMid); + + // Selection + // https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/305 + style.SetAccentColor(aHigh); + style.SetHighlightColor(aHigh); + style.SetHighlightTextColor(aHighText); + style.SetListBoxWindowHighlightColor(aHigh); + style.SetListBoxWindowHighlightTextColor(aHighText); + style.SetActiveColor(aHigh); + style.SetActiveTextColor(aHighText); + + // Links + style.SetLinkColor(aLink); + style.SetVisitedLinkColor(aVisitedLink); + + // Tooltip + style.SetHelpColor(toColor(QToolTip::palette().color(QPalette::Active, QPalette::ToolTipBase))); + style.SetHelpTextColor( + toColor(QToolTip::palette().color(QPalette::Active, QPalette::ToolTipText))); + + // Menu + std::unique_ptr<QMenuBar> pMenuBar = std::make_unique<QMenuBar>(); + QPalette qMenuCG = pMenuBar->palette(); + + // Menu text and background color, theme specific + Color aMenuFore = toColor(qMenuCG.color(QPalette::WindowText)); + Color aMenuBack = toColor(qMenuCG.color(QPalette::Window)); + + style.SetMenuTextColor(aMenuFore); + style.SetMenuBarTextColor(style.GetPersonaMenuBarTextColor().value_or(aMenuFore)); + style.SetMenuColor(aMenuBack); + style.SetMenuBarColor(aMenuBack); + style.SetMenuHighlightColor(toColor(qMenuCG.color(QPalette::Highlight))); + style.SetMenuHighlightTextColor(toColor(qMenuCG.color(QPalette::HighlightedText))); + + // set special menubar highlight text color + if (QApplication::style()->inherits("HighContrastStyle")) + ImplGetSVData()->maNWFData.maMenuBarHighlightTextColor + = toColor(qMenuCG.color(QPalette::HighlightedText)); + else + ImplGetSVData()->maNWFData.maMenuBarHighlightTextColor = aMenuFore; + + // set menubar rollover color + if (pMenuBar->style()->styleHint(QStyle::SH_MenuBar_MouseTracking)) + { + style.SetMenuBarRolloverColor(toColor(qMenuCG.color(QPalette::Highlight))); + style.SetMenuBarRolloverTextColor(ImplGetSVData()->maNWFData.maMenuBarHighlightTextColor); + } + else + { + style.SetMenuBarRolloverColor(aMenuBack); + style.SetMenuBarRolloverTextColor(aMenuFore); + } + style.SetMenuBarHighlightTextColor(style.GetMenuHighlightTextColor()); + + // Default fonts + vcl::Font aFont; + if (toVclFont(QApplication::font(), aLocale, aFont)) + { + style.BatchSetFonts(aFont, aFont); + aFont.SetWeight(WEIGHT_BOLD); + style.SetTitleFont(aFont); + style.SetFloatTitleFont(aFont); + } + + // Tooltip font + if (toVclFont(QToolTip::font(), aLocale, aFont)) + style.SetHelpFont(aFont); + + // Menu bar font + if (toVclFont(pMenuBar->font(), aLocale, aFont)) + style.SetMenuFont(aFont); + + // Icon theme + const bool bPreferDarkTheme = GetUseDarkMode(); + style.SetPreferredIconTheme(toOUString(QIcon::themeName()), bPreferDarkTheme); + + // Scroll bar size + style.SetScrollBarSize(QApplication::style()->pixelMetric(QStyle::PM_ScrollBarExtent)); + style.SetMinThumbSize(QApplication::style()->pixelMetric(QStyle::PM_ScrollBarSliderMin)); + + // These colors are used for the ruler text and marks + style.SetShadowColor(toColor(pal.color(QPalette::Disabled, QPalette::WindowText))); + style.SetDarkShadowColor(toColor(pal.color(QPalette::Inactive, QPalette::WindowText))); + + // Cursor blink interval + int nFlashTime = QApplication::cursorFlashTime(); + style.SetCursorBlinkTime(nFlashTime != 0 ? nFlashTime / 2 : STYLE_CURSOR_NOBLINKTIME); + + rSettings.SetStyleSettings(style); +} + +void QtFrame::Beep() { QApplication::beep(); } + +SalFrame::SalPointerState QtFrame::GetPointerState() +{ + SalPointerState aState; + aState.maPos = toPoint(QCursor::pos() * devicePixelRatioF()); + aState.maPos.Move(-maGeometry.x(), -maGeometry.y()); + aState.mnState = GetMouseModCode(QGuiApplication::mouseButtons()) + | GetKeyModCode(QGuiApplication::keyboardModifiers()); + return aState; +} + +KeyIndicatorState QtFrame::GetIndicatorState() { return KeyIndicatorState(); } + +void QtFrame::SimulateKeyPress(sal_uInt16 nKeyCode) +{ + SAL_WARN("vcl.qt", "missing simulate keypress " << nKeyCode); +} + +// don't set QWidget parents; this breaks popups on Wayland, like the LO ComboBox or ColorPicker! +void QtFrame::SetParent(SalFrame* pNewParent) { m_pParent = static_cast<QtFrame*>(pNewParent); } + +void QtFrame::SetPluginParent(SystemParentData* /*pNewParent*/) +{ + //FIXME: no SetPluginParent impl. for qt5 +} + +void QtFrame::ResetClipRegion() { m_bNullRegion = true; } + +void QtFrame::BeginSetClipRegion(sal_uInt32) +{ + m_aRegion = QRegion(QRect(QPoint(0, 0), m_pQWidget->size())); +} + +void QtFrame::UnionClipRegion(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight) +{ + m_aRegion + = m_aRegion.united(scaledQRect(QRect(nX, nY, nWidth, nHeight), 1 / devicePixelRatioF())); +} + +void QtFrame::EndSetClipRegion() { m_bNullRegion = false; } + +void QtFrame::SetScreenNumber(unsigned int nScreen) +{ + if (!isWindow()) + return; + + QWindow* const pWindow = windowHandle(); + if (!pWindow) + return; + + QList<QScreen*> screens = QApplication::screens(); + if (static_cast<int>(nScreen) < screens.size() || m_bFullScreenSpanAll) + { + QRect screenGeo; + + if (!m_bFullScreenSpanAll) + { + screenGeo = QGuiApplication::screens().at(nScreen)->geometry(); + pWindow->setScreen(QApplication::screens()[nScreen]); + } + else // special case: fullscreen over all available screens + { + assert(m_bFullScreen); + // left-most screen + QScreen* pScreen = QGuiApplication::screenAt(QPoint(0, 0)); + // entire virtual desktop + screenGeo = pScreen->availableVirtualGeometry(); + pWindow->setScreen(pScreen); + pWindow->setGeometry(screenGeo); + nScreen = screenNumber(pScreen); + } + + // setScreen by itself has no effect, explicitly move the widget to + // the new screen + asChild()->move(screenGeo.topLeft()); + } + else + { + // index outta bounds, use primary screen + QScreen* primaryScreen = QApplication::primaryScreen(); + pWindow->setScreen(primaryScreen); + nScreen = static_cast<sal_uInt32>(screenNumber(primaryScreen)); + } + + maGeometry.setScreen(nScreen); +} + +void QtFrame::SetApplicationID(const OUString& rWMClass) +{ +#if CHECK_QT5_USING_X11 + assert(m_aSystemData.platform != SystemEnvData::Platform::Invalid); + if (m_aSystemData.platform != SystemEnvData::Platform::Xcb || !m_pTopLevel) + return; + + QtX11Support::setApplicationID(m_pTopLevel->winId(), rWMClass); +#else + Q_UNUSED(rWMClass); +#endif +} + +void QtFrame::ResolveWindowHandle(SystemEnvData& rData) const +{ + if (!rData.pWidget) + return; + assert(rData.platform != SystemEnvData::Platform::Invalid); + if (rData.platform != SystemEnvData::Platform::Wayland) + rData.SetWindowHandle(static_cast<QWidget*>(rData.pWidget)->winId()); +} + +bool QtFrame::GetUseReducedAnimation() const { return GetQtInstance()->GetUseReducedAnimation(); } + +// Drag'n'drop foo + +void QtFrame::registerDragSource(QtDragSource* pDragSource) +{ + assert(!m_pDragSource); + m_pDragSource = pDragSource; +} + +void QtFrame::deregisterDragSource(QtDragSource const* pDragSource) +{ + assert(m_pDragSource == pDragSource); + (void)pDragSource; + m_pDragSource = nullptr; +} + +void QtFrame::registerDropTarget(QtDropTarget* pDropTarget) +{ + assert(!m_pDropTarget); + m_pDropTarget = pDropTarget; + + QtInstance* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->RunInMainThread([this]() { m_pQWidget->setAcceptDrops(true); }); +} + +void QtFrame::deregisterDropTarget(QtDropTarget const* pDropTarget) +{ + assert(m_pDropTarget == pDropTarget); + (void)pDropTarget; + m_pDropTarget = nullptr; +} + +static css::uno::Reference<css::datatransfer::XTransferable> +lcl_getXTransferable(const QMimeData* pMimeData) +{ + css::uno::Reference<css::datatransfer::XTransferable> xTransferable; + const QtMimeData* pQtMimeData = dynamic_cast<const QtMimeData*>(pMimeData); + if (!pQtMimeData) + xTransferable = new QtDnDTransferable(pMimeData); + else + xTransferable = pQtMimeData->xTransferable(); + return xTransferable; +} + +static sal_Int8 lcl_getUserDropAction(const QDropEvent* pEvent, const sal_Int8 nSourceActions, + const QMimeData* pMimeData) +{ +// we completely ignore all proposals by the Qt event, as they don't +// match at all with the preferred LO DnD actions. +// check the key modifiers to detect a user-overridden DnD action +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const Qt::KeyboardModifiers eKeyMod = pEvent->modifiers(); +#else + const Qt::KeyboardModifiers eKeyMod = pEvent->keyboardModifiers(); +#endif + sal_Int8 nUserDropAction = 0; + if ((eKeyMod & Qt::ShiftModifier) && !(eKeyMod & Qt::ControlModifier)) + nUserDropAction = css::datatransfer::dnd::DNDConstants::ACTION_MOVE; + else if ((eKeyMod & Qt::ControlModifier) && !(eKeyMod & Qt::ShiftModifier)) + nUserDropAction = css::datatransfer::dnd::DNDConstants::ACTION_COPY; + else if ((eKeyMod & Qt::ShiftModifier) && (eKeyMod & Qt::ControlModifier)) + nUserDropAction = css::datatransfer::dnd::DNDConstants::ACTION_LINK; + nUserDropAction &= nSourceActions; + + // select the default DnD action, if there isn't a user preference + if (0 == nUserDropAction) + { + // default LO internal action is move, but default external action is copy + nUserDropAction = dynamic_cast<const QtMimeData*>(pMimeData) + ? css::datatransfer::dnd::DNDConstants::ACTION_MOVE + : css::datatransfer::dnd::DNDConstants::ACTION_COPY; + nUserDropAction &= nSourceActions; + + // if the default doesn't match any allowed source action, fall back to the + // preferred of all allowed source actions + if (0 == nUserDropAction) + nUserDropAction = toVclDropAction(getPreferredDropAction(nSourceActions)); + + // this is "our" preference, but actually we would even prefer any default, + // if there is any + nUserDropAction |= css::datatransfer::dnd::DNDConstants::ACTION_DEFAULT; + } + return nUserDropAction; +} + +void QtFrame::handleDragMove(QDragMoveEvent* pEvent) +{ + assert(m_pDropTarget); + + // prepare our suggested drop action for the drop target + const sal_Int8 nSourceActions = toVclDropActions(pEvent->possibleActions()); + const QMimeData* pMimeData = pEvent->mimeData(); + const sal_Int8 nUserDropAction = lcl_getUserDropAction(pEvent, nSourceActions, pMimeData); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const Point aPos = toPoint(pEvent->position().toPoint() * devicePixelRatioF()); +#else + const Point aPos = toPoint(pEvent->pos() * devicePixelRatioF()); +#endif + + css::datatransfer::dnd::DropTargetDragEnterEvent aEvent; + aEvent.Source = static_cast<css::datatransfer::dnd::XDropTarget*>(m_pDropTarget); + aEvent.Context = static_cast<css::datatransfer::dnd::XDropTargetDragContext*>(m_pDropTarget); + aEvent.LocationX = aPos.X(); + aEvent.LocationY = aPos.Y(); + aEvent.DropAction = nUserDropAction; + aEvent.SourceActions = nSourceActions; + + // ask the drop target to accept our drop action + if (!m_bInDrag) + { + aEvent.SupportedDataFlavors = lcl_getXTransferable(pMimeData)->getTransferDataFlavors(); + m_pDropTarget->fire_dragEnter(aEvent); + m_bInDrag = true; + } + else + m_pDropTarget->fire_dragOver(aEvent); + + // the drop target accepted our drop action => inform Qt + if (m_pDropTarget->proposedDropAction() != 0) + { + pEvent->setDropAction(getPreferredDropAction(m_pDropTarget->proposedDropAction())); + pEvent->accept(); + } + else // or maybe someone else likes it? + pEvent->ignore(); +} + +void QtFrame::handleDrop(QDropEvent* pEvent) +{ + assert(m_pDropTarget); + + // prepare our suggested drop action for the drop target + const sal_Int8 nSourceActions = toVclDropActions(pEvent->possibleActions()); + const sal_Int8 nUserDropAction + = lcl_getUserDropAction(pEvent, nSourceActions, pEvent->mimeData()); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const Point aPos = toPoint(pEvent->position().toPoint() * devicePixelRatioF()); +#else + const Point aPos = toPoint(pEvent->pos() * devicePixelRatioF()); +#endif + + css::datatransfer::dnd::DropTargetDropEvent aEvent; + aEvent.Source = static_cast<css::datatransfer::dnd::XDropTarget*>(m_pDropTarget); + aEvent.Context = static_cast<css::datatransfer::dnd::XDropTargetDropContext*>(m_pDropTarget); + aEvent.LocationX = aPos.X(); + aEvent.LocationY = aPos.Y(); + aEvent.SourceActions = nSourceActions; + aEvent.DropAction = nUserDropAction; + aEvent.Transferable = lcl_getXTransferable(pEvent->mimeData()); + + // ask the drop target to accept our drop action + m_pDropTarget->fire_drop(aEvent); + m_bInDrag = false; + + const bool bDropSuccessful = m_pDropTarget->dropSuccessful(); + const sal_Int8 nDropAction = m_pDropTarget->proposedDropAction(); + + // inform the drag source of the drag-origin frame of the drop result + if (pEvent->source()) + { + QtWidget* pWidget = dynamic_cast<QtWidget*>(pEvent->source()); + assert(pWidget); // AFAIK there shouldn't be any non-Qt5Widget as source in LO itself + if (pWidget) + pWidget->frame().m_pDragSource->fire_dragEnd(nDropAction, bDropSuccessful); + } + + // the drop target accepted our drop action => inform Qt + if (bDropSuccessful) + { + pEvent->setDropAction(getPreferredDropAction(nDropAction)); + pEvent->accept(); + } + else // or maybe someone else likes it? + pEvent->ignore(); +} + +void QtFrame::handleDragLeave() +{ + css::datatransfer::dnd::DropTargetEvent aEvent; + aEvent.Source = static_cast<css::datatransfer::dnd::XDropTarget*>(m_pDropTarget); + m_pDropTarget->fire_dragExit(aEvent); + m_bInDrag = false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtGraphics.cxx b/vcl/qt5/QtGraphics.cxx new file mode 100644 index 0000000000..d809556ce2 --- /dev/null +++ b/vcl/qt5/QtGraphics.cxx @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtGraphics.hxx> + +#include <QtData.hxx> +#include <QtFont.hxx> +#include <QtFrame.hxx> +#include <QtGraphics_Controls.hxx> +#include <QtPainter.hxx> + +#include <QtGui/QImage> +#include <QtGui/QPainter> +#include <QtWidgets/QPushButton> +#include <QtWidgets/QWidget> + +QtGraphics::QtGraphics( QtFrame *pFrame, QImage *pQImage ) + : m_pFrame( pFrame ) + , m_pTextStyle{ nullptr, } + , m_aTextColor( 0x00, 0x00, 0x00 ) +{ + m_pBackend = std::make_unique<QtGraphicsBackend>(m_pFrame, pQImage); + + if (!initWidgetDrawBackends(false)) + { + if (!QtData::noNativeControls()) + m_pWidgetDraw.reset(new QtGraphics_Controls(*this)); + } + if (m_pFrame) + setDevicePixelRatioF(m_pFrame->devicePixelRatioF()); +} + +QtGraphics::~QtGraphics() { ReleaseFonts(); } + +void QtGraphics::ChangeQImage(QImage* pQImage) +{ + m_pBackend->setQImage(pQImage); + m_pBackend->ResetClipRegion(); +} + +SalGraphicsImpl* QtGraphics::GetImpl() const { return m_pBackend.get(); } + +SystemGraphicsData QtGraphics::GetGraphicsData() const { return SystemGraphicsData(); } + +#if ENABLE_CAIRO_CANVAS + +bool QtGraphics::SupportsCairo() const { return false; } + +cairo::SurfaceSharedPtr +QtGraphics::CreateSurface(const cairo::CairoSurfaceSharedPtr& /*rSurface*/) const +{ + return nullptr; +} + +cairo::SurfaceSharedPtr QtGraphics::CreateSurface(const OutputDevice& /*rRefDevice*/, int /*x*/, + int /*y*/, int /*width*/, int /*height*/) const +{ + return nullptr; +} + +cairo::SurfaceSharedPtr QtGraphics::CreateBitmapSurface(const OutputDevice& /*rRefDevice*/, + const BitmapSystemData& /*rData*/, + const Size& /*rSize*/) const +{ + return nullptr; +} + +css::uno::Any QtGraphics::GetNativeSurfaceHandle(cairo::SurfaceSharedPtr& /*rSurface*/, + const basegfx::B2ISize& /*rSize*/) const +{ + return css::uno::Any(); +} + +#endif + +void QtGraphics::handleDamage(const tools::Rectangle& rDamagedRegion) +{ + assert(m_pWidgetDraw); + assert(dynamic_cast<QtGraphics_Controls*>(m_pWidgetDraw.get())); + assert(!rDamagedRegion.IsEmpty()); + + QImage* pImage = static_cast<QtGraphics_Controls*>(m_pWidgetDraw.get())->getImage(); + QImage blit(*pImage); + blit.setDevicePixelRatio(1); + QtPainter aPainter(*m_pBackend); + aPainter.drawImage(QPoint(rDamagedRegion.Left(), rDamagedRegion.Top()), blit); + aPainter.update(toQRect(rDamagedRegion)); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtGraphics_Controls.cxx b/vcl/qt5/QtGraphics_Controls.cxx new file mode 100644 index 0000000000..81ab7a7edc --- /dev/null +++ b/vcl/qt5/QtGraphics_Controls.cxx @@ -0,0 +1,1168 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtGraphics_Controls.hxx> + +#include <QtGui/QPainter> +#include <QtWidgets/QApplication> +#include <QtWidgets/QFrame> +#include <QtWidgets/QLabel> +#include <QtWidgets/QLineEdit> + +#include <QtTools.hxx> +#include <QtGraphicsBase.hxx> +#include <vcl/decoview.hxx> + +/** + Conversion function between VCL ControlState together with + ImplControlValue and Qt state flags. + @param nControlState State of the widget (default, focused, ...) in Native Widget Framework. + @param aValue Value held by the widget (on, off, ...) +*/ +static QStyle::State vclStateValue2StateFlag(ControlState nControlState, + const ImplControlValue& aValue) +{ + QStyle::State nState + = ((nControlState & ControlState::ENABLED) ? QStyle::State_Enabled : QStyle::State_None) + | ((nControlState & ControlState::FOCUSED) + ? QStyle::State_HasFocus | QStyle::State_KeyboardFocusChange + : QStyle::State_None) + | ((nControlState & ControlState::PRESSED) ? QStyle::State_Sunken : QStyle::State_None) + | ((nControlState & ControlState::SELECTED) ? QStyle::State_Selected : QStyle::State_None) + | ((nControlState & ControlState::ROLLOVER) ? QStyle::State_MouseOver + : QStyle::State_None); + + switch (aValue.getTristateVal()) + { + case ButtonValue::On: + nState |= QStyle::State_On; + break; + case ButtonValue::Off: + nState |= QStyle::State_Off; + break; + case ButtonValue::Mixed: + nState |= QStyle::State_NoChange; + break; + default: + break; + } + + return nState; +} + +static void lcl_ApplyBackgroundColorToStyleOption(QStyleOption& rOption, + const Color& rBackgroundColor) +{ + if (rBackgroundColor != COL_AUTO) + { + QColor aColor = toQColor(rBackgroundColor); + for (QPalette::ColorRole role : { QPalette::Window, QPalette::Button, QPalette::Base }) + rOption.palette.setColor(role, aColor); + } +} + +QtGraphics_Controls::QtGraphics_Controls(const QtGraphicsBase& rGraphics) + : m_rGraphics(rGraphics) +{ +} + +bool QtGraphics_Controls::isNativeControlSupported(ControlType type, ControlPart part) +{ + switch (type) + { + case ControlType::Tooltip: + case ControlType::Progress: + case ControlType::ListNode: + return (part == ControlPart::Entire); + + case ControlType::Pushbutton: + case ControlType::Radiobutton: + case ControlType::Checkbox: + return (part == ControlPart::Entire) || (part == ControlPart::Focus); + + case ControlType::ListHeader: + return (part == ControlPart::Button); + + case ControlType::Menubar: + case ControlType::MenuPopup: + case ControlType::Editbox: + case ControlType::MultilineEditbox: + case ControlType::Combobox: + case ControlType::Toolbar: + case ControlType::Frame: + case ControlType::Scrollbar: + case ControlType::WindowBackground: + case ControlType::Fixedline: + return true; + + case ControlType::Listbox: + return (part == ControlPart::Entire || part == ControlPart::HasBackgroundTexture); + + case ControlType::Spinbox: + return (part == ControlPart::Entire || part == ControlPart::HasBackgroundTexture); + + case ControlType::Slider: + return (part == ControlPart::TrackHorzArea || part == ControlPart::TrackVertArea); + + case ControlType::TabItem: + case ControlType::TabPane: + return ((part == ControlPart::Entire) || part == ControlPart::TabPaneWithHeader); + + default: + break; + } + + return false; +} + +inline int QtGraphics_Controls::pixelMetric(QStyle::PixelMetric metric, const QStyleOption* option, + const QWidget* pWidget) +{ + return QApplication::style()->pixelMetric(metric, option, pWidget); +} + +inline QSize QtGraphics_Controls::sizeFromContents(QStyle::ContentsType type, + const QStyleOption* option, + const QSize& contentsSize) +{ + return QApplication::style()->sizeFromContents(type, option, contentsSize); +} + +inline QRect QtGraphics_Controls::subControlRect(QStyle::ComplexControl control, + const QStyleOptionComplex* option, + QStyle::SubControl subControl) +{ + return QApplication::style()->subControlRect(control, option, subControl); +} + +inline QRect QtGraphics_Controls::subElementRect(QStyle::SubElement element, + const QStyleOption* option) +{ + return QApplication::style()->subElementRect(element, option); +} + +void QtGraphics_Controls::draw(QStyle::ControlElement element, QStyleOption& rOption, QImage* image, + const Color& rBackgroundColor, QStyle::State const state, QRect rect) +{ + const QRect& targetRect = !rect.isNull() ? rect : image->rect(); + + rOption.state |= state; + rOption.rect = downscale(targetRect); + + lcl_ApplyBackgroundColorToStyleOption(rOption, rBackgroundColor); + + QPainter painter(image); + QApplication::style()->drawControl(element, &rOption, &painter); +} + +void QtGraphics_Controls::draw(QStyle::PrimitiveElement element, QStyleOption& rOption, + QImage* image, const Color& rBackgroundColor, + QStyle::State const state, QRect rect) +{ + const QRect& targetRect = !rect.isNull() ? rect : image->rect(); + + rOption.state |= state; + rOption.rect = downscale(targetRect); + + lcl_ApplyBackgroundColorToStyleOption(rOption, rBackgroundColor); + + QPainter painter(image); + QApplication::style()->drawPrimitive(element, &rOption, &painter); +} + +void QtGraphics_Controls::draw(QStyle::ComplexControl element, QStyleOptionComplex& rOption, + QImage* image, const Color& rBackgroundColor, + QStyle::State const state) +{ + const QRect& targetRect = image->rect(); + + rOption.state |= state; + rOption.rect = downscale(targetRect); + + lcl_ApplyBackgroundColorToStyleOption(rOption, rBackgroundColor); + + QPainter painter(image); + QApplication::style()->drawComplexControl(element, &rOption, &painter); +} + +void QtGraphics_Controls::drawFrame(QStyle::PrimitiveElement element, QImage* image, + const Color& rBackgroundColor, QStyle::State const& state, + bool bClip, QStyle::PixelMetric eLineMetric) +{ + const int fw = pixelMetric(eLineMetric); + QStyleOptionFrame option; + option.frameShape = QFrame::StyledPanel; + option.state = QStyle::State_Sunken | state; + option.lineWidth = fw; + + QRect aRect = downscale(image->rect()); + option.rect = aRect; + + lcl_ApplyBackgroundColorToStyleOption(option, rBackgroundColor); + + QPainter painter(image); + if (bClip) + painter.setClipRegion(QRegion(aRect).subtracted(aRect.adjusted(fw, fw, -fw, -fw))); + QApplication::style()->drawPrimitive(element, &option, &painter); +} + +void QtGraphics_Controls::fillQStyleOptionTab(const ImplControlValue& value, QStyleOptionTab& sot) +{ + const TabitemValue& rValue = static_cast<const TabitemValue&>(value); + if (rValue.isFirst()) + sot.position = rValue.isLast() ? QStyleOptionTab::OnlyOneTab : QStyleOptionTab::Beginning; + else if (rValue.isLast()) + sot.position = rValue.isFirst() ? QStyleOptionTab::OnlyOneTab : QStyleOptionTab::End; + else + sot.position = QStyleOptionTab::Middle; +} + +void QtGraphics_Controls::fullQStyleOptionTabWidgetFrame(QStyleOptionTabWidgetFrame& option, + bool bDownscale) +{ + option.state = QStyle::State_Enabled; + option.rightCornerWidgetSize = QSize(0, 0); + option.leftCornerWidgetSize = QSize(0, 0); + int nLineWidth = pixelMetric(QStyle::PM_DefaultFrameWidth); + option.lineWidth = bDownscale ? std::max(1, downscale(nLineWidth, Round::Ceil)) : nLineWidth; + option.midLineWidth = 0; + option.shape = QTabBar::RoundedNorth; +} + +bool QtGraphics_Controls::drawNativeControl(ControlType type, ControlPart part, + const tools::Rectangle& rControlRegion, + ControlState nControlState, + const ImplControlValue& value, const OUString&, + const Color& rBackgroundColor) +{ + bool nativeSupport = isNativeControlSupported(type, part); + if (!nativeSupport) + { + assert(!nativeSupport && "drawNativeControl called without native support!"); + return false; + } + + if (m_lastPopupRect.isValid() + && (type != ControlType::MenuPopup || part != ControlPart::MenuItem)) + m_lastPopupRect = QRect(); + + bool returnVal = true; + + QRect widgetRect = toQRect(rControlRegion); + + //if no image, or resized, make a new image + if (!m_image || m_image->size() != widgetRect.size()) + { + m_image.reset(new QImage(widgetRect.width(), widgetRect.height(), + QImage::Format_ARGB32_Premultiplied)); + m_image->setDevicePixelRatio(m_rGraphics.devicePixelRatioF()); + } + + // Default image color - just once + switch (type) + { + case ControlType::MenuPopup: + if (part == ControlPart::MenuItemCheckMark || part == ControlPart::MenuItemRadioMark) + { + // it is necessary to fill the background transparently first, as this + // is painted after menuitem highlight, otherwise there would be a grey area + m_image->fill(Qt::transparent); + break; + } + [[fallthrough]]; // QPalette::Window + case ControlType::Menubar: + case ControlType::WindowBackground: + m_image->fill(QApplication::palette().color(QPalette::Window).rgb()); + break; + case ControlType::Tooltip: + m_image->fill(QApplication::palette().color(QPalette::ToolTipBase).rgb()); + break; + case ControlType::Scrollbar: + if ((part == ControlPart::DrawBackgroundVert) + || (part == ControlPart::DrawBackgroundHorz)) + { + m_image->fill(QApplication::palette().color(QPalette::Window).rgb()); + break; + } + [[fallthrough]]; // Qt::transparent + default: + m_image->fill(Qt::transparent); + break; + } + + if (type == ControlType::Pushbutton) + { + const PushButtonValue& rPBValue = static_cast<const PushButtonValue&>(value); + if (part == ControlPart::Focus) + // Nothing to do. Drawing focus separately is not needed because that's + // already handled by the ControlState::FOCUSED state being set when + // drawing the entire control + return true; + assert(part == ControlPart::Entire); + QStyleOptionButton option; + if (nControlState & ControlState::DEFAULT) + option.features |= QStyleOptionButton::DefaultButton; + if (rPBValue.m_bFlatButton) + option.features |= QStyleOptionButton::Flat; + draw(QStyle::CE_PushButton, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else if (type == ControlType::Menubar) + { + if (part == ControlPart::MenuItem) + { + QStyleOptionMenuItem option; + option.state = vclStateValue2StateFlag(nControlState, value); + if ((nControlState & ControlState::ROLLOVER) + && QApplication::style()->styleHint(QStyle::SH_MenuBar_MouseTracking)) + option.state |= QStyle::State_Selected; + + if (nControlState + & ControlState::SELECTED) // Passing State_Sunken is currently not documented. + option.state |= QStyle::State_Sunken; // But some kinds of QStyle interpret it. + + draw(QStyle::CE_MenuBarItem, option, m_image.get(), rBackgroundColor); + } + else if (part == ControlPart::Entire) + { + QStyleOptionMenuItem option; + draw(QStyle::CE_MenuBarEmptyArea, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else + { + returnVal = false; + } + } + else if (type == ControlType::MenuPopup) + { + assert(part == ControlPart::MenuItem ? m_lastPopupRect.isValid() + : !m_lastPopupRect.isValid()); + if (part == ControlPart::MenuItem) + { + QStyleOptionMenuItem option; + draw(QStyle::CE_MenuItem, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + // HACK: LO core first paints the entire popup and only then it paints menu items, + // but QMenu::paintEvent() paints popup frame after all items. That means highlighted + // items here would paint the highlight over the frame border. Since calls to ControlPart::MenuItem + // are always preceded by calls to ControlPart::Entire, just remember the size for the whole + // popup (otherwise not possible to get here) and draw the border afterwards. + QRect framerect(m_lastPopupRect.topLeft() - widgetRect.topLeft(), + widgetRect.size().expandedTo(m_lastPopupRect.size())); + QStyleOptionFrame frame; + draw(QStyle::PE_FrameMenu, frame, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value), framerect); + } + else if (part == ControlPart::Separator) + { + QStyleOptionMenuItem option; + option.menuItemType = QStyleOptionMenuItem::Separator; + // Painting the whole menu item area results in different background + // with at least Plastique style, so clip only to the separator itself + // (QSize( 2, 2 ) is hardcoded in Qt) + option.rect = m_image->rect(); + QSize size = sizeFromContents(QStyle::CT_MenuItem, &option, QSize(2, 2)); + QRect rect = m_image->rect(); + QPoint center = rect.center(); + rect.setHeight(size.height()); + rect.moveCenter(center); + option.state |= vclStateValue2StateFlag(nControlState, value); + option.rect = rect; + + QPainter painter(m_image.get()); + // don't paint over popup frame border (like the hack above, but here it can be simpler) + const int fw = pixelMetric(QStyle::PM_MenuPanelWidth); + painter.setClipRect(rect.adjusted(fw, 0, -fw, 0)); + QApplication::style()->drawControl(QStyle::CE_MenuItem, &option, &painter); + } + else if (part == ControlPart::MenuItemCheckMark || part == ControlPart::MenuItemRadioMark) + { + QStyleOptionMenuItem option; + option.checkType = (part == ControlPart::MenuItemCheckMark) + ? QStyleOptionMenuItem::NonExclusive + : QStyleOptionMenuItem::Exclusive; + option.checked = bool(nControlState & ControlState::PRESSED); + // widgetRect is now the rectangle for the checkbox/radiobutton itself, but Qt + // paints the whole menu item, so translate position (and it'll be clipped); + // it is also necessary to fill the background transparently first, as this + // is painted after menuitem highlight, otherwise there would be a grey area + assert(value.getType() == ControlType::MenuPopup); + const MenupopupValue* menuVal = static_cast<const MenupopupValue*>(&value); + QRect menuItemRect(toQRect(menuVal->maItemRect)); + QRect rect(menuItemRect.topLeft() - widgetRect.topLeft(), + widgetRect.size().expandedTo(menuItemRect.size())); + // checkboxes are always displayed next to images in menus, so are never centered + const int focus_size = pixelMetric(QStyle::PM_FocusFrameHMargin); + rect.moveTo(-focus_size, rect.y()); + draw(QStyle::CE_MenuItem, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState & ~ControlState::PRESSED, value), rect); + } + else if (part == ControlPart::Entire) + { + QStyleOptionMenuItem option; + option.state = vclStateValue2StateFlag(nControlState, value); + draw(QStyle::PE_PanelMenu, option, m_image.get(), rBackgroundColor); + // Try hard to get any frame! + QStyleOptionFrame frame; + draw(QStyle::PE_FrameMenu, frame, m_image.get(), rBackgroundColor); + draw(QStyle::PE_FrameWindow, frame, m_image.get(), rBackgroundColor); + m_lastPopupRect = widgetRect; + } + else + returnVal = false; + } + else if ((type == ControlType::Toolbar) && (part == ControlPart::Button)) + { + QStyleOptionToolButton option; + + option.arrowType = Qt::NoArrow; + option.subControls = QStyle::SC_ToolButton; + option.state = vclStateValue2StateFlag(nControlState, value); + option.state |= QStyle::State_Raised | QStyle::State_Enabled | QStyle::State_AutoRaise; + + draw(QStyle::CC_ToolButton, option, m_image.get(), rBackgroundColor); + } + else if ((type == ControlType::Toolbar) && (part == ControlPart::Entire)) + { + QStyleOptionToolBar option; + draw(QStyle::CE_ToolBar, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else if ((type == ControlType::Toolbar) + && (part == ControlPart::ThumbVert || part == ControlPart::ThumbHorz)) + { + // reduce paint area only to the handle area + const int handleExtend = pixelMetric(QStyle::PM_ToolBarHandleExtent); + QStyleOption option; + QRect aRect = m_image->rect(); + if (part == ControlPart::ThumbVert) + { + aRect.setWidth(handleExtend); + option.state = QStyle::State_Horizontal; + } + else + aRect.setHeight(handleExtend); + draw(QStyle::PE_IndicatorToolBarHandle, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value), aRect); + } + else if (type == ControlType::Editbox || type == ControlType::MultilineEditbox) + { + drawFrame(QStyle::PE_FrameLineEdit, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value), false); + } + else if (type == ControlType::Combobox) + { + QStyleOptionComboBox option; + option.editable = true; + draw(QStyle::CC_ComboBox, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else if (type == ControlType::Listbox) + { + QStyleOptionComboBox option; + option.editable = false; + switch (part) + { + case ControlPart::ListboxWindow: + drawFrame(QStyle::PE_Frame, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value), true, + QStyle::PM_ComboBoxFrameWidth); + break; + case ControlPart::SubEdit: + draw(QStyle::CE_ComboBoxLabel, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + break; + case ControlPart::Entire: + draw(QStyle::CC_ComboBox, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + break; + case ControlPart::ButtonDown: + option.subControls = QStyle::SC_ComboBoxArrow; + draw(QStyle::CC_ComboBox, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + break; + default: + returnVal = false; + break; + } + } + else if (type == ControlType::ListNode) + { + QStyleOption option; + option.state = vclStateValue2StateFlag(nControlState, value); + option.state |= QStyle::State_Item | QStyle::State_Children; + + if (value.getTristateVal() == ButtonValue::On) + option.state |= QStyle::State_Open; + + draw(QStyle::PE_IndicatorBranch, option, m_image.get(), rBackgroundColor); + } + else if (type == ControlType::ListHeader) + { + QStyleOptionHeader option; + draw(QStyle::CE_HeaderSection, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else if (type == ControlType::Checkbox) + { + if (part == ControlPart::Entire) + { + QStyleOptionButton option; + // clear FOCUSED bit, focus is drawn separately + nControlState &= ~ControlState::FOCUSED; + draw(QStyle::CE_CheckBox, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else if (part == ControlPart::Focus) + { + QStyleOptionFocusRect option; + draw(QStyle::PE_FrameFocusRect, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + } + else if (type == ControlType::Scrollbar) + { + if ((part == ControlPart::DrawBackgroundVert) || (part == ControlPart::DrawBackgroundHorz)) + { + QStyleOptionSlider option; + assert(value.getType() == ControlType::Scrollbar); + const ScrollbarValue* sbVal = static_cast<const ScrollbarValue*>(&value); + + //if the scroll bar is active (aka not degenerate... allow for hover events) + if (sbVal->mnVisibleSize < sbVal->mnMax) + option.state = QStyle::State_MouseOver; + + bool horizontal = (part == ControlPart::DrawBackgroundHorz); //horizontal or vertical + option.orientation = horizontal ? Qt::Horizontal : Qt::Vertical; + if (horizontal) + option.state |= QStyle::State_Horizontal; + + // If the scrollbar has a mnMin == 0 and mnMax == 0 then mnVisibleSize is set to -1?! + // I don't know if a negative mnVisibleSize makes any sense, so just handle this case + // without crashing LO with a SIGFPE in the Qt library. + const tools::Long nVisibleSize + = (sbVal->mnMin == sbVal->mnMax) ? 0 : sbVal->mnVisibleSize; + + option.minimum = sbVal->mnMin; + option.maximum = sbVal->mnMax - nVisibleSize; + option.maximum = qMax(option.maximum, option.minimum); // bnc#619772 + option.sliderValue = sbVal->mnCur; + option.sliderPosition = sbVal->mnCur; + option.pageStep = nVisibleSize; + if (part == ControlPart::DrawBackgroundHorz) + option.upsideDown + = (QGuiApplication::isRightToLeft() + && sbVal->maButton1Rect.Left() < sbVal->maButton2Rect.Left()) + || (QGuiApplication::isLeftToRight() + && sbVal->maButton1Rect.Left() > sbVal->maButton2Rect.Left()); + + //setup the active control... always the slider + if (sbVal->mnThumbState & ControlState::ROLLOVER) + option.activeSubControls = QStyle::SC_ScrollBarSlider; + + draw(QStyle::CC_ScrollBar, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else + { + returnVal = false; + } + } + else if (type == ControlType::Spinbox) + { + QStyleOptionSpinBox option; + option.frame = true; + + // determine active control + if (value.getType() == ControlType::SpinButtons) + { + const SpinbuttonValue* pSpinVal = static_cast<const SpinbuttonValue*>(&value); + if (pSpinVal->mnUpperState & ControlState::PRESSED) + option.activeSubControls |= QStyle::SC_SpinBoxUp; + if (pSpinVal->mnLowerState & ControlState::PRESSED) + option.activeSubControls |= QStyle::SC_SpinBoxDown; + if (pSpinVal->mnUpperState & ControlState::ENABLED) + option.stepEnabled |= QAbstractSpinBox::StepUpEnabled; + if (pSpinVal->mnLowerState & ControlState::ENABLED) + option.stepEnabled |= QAbstractSpinBox::StepDownEnabled; + if (pSpinVal->mnUpperState & ControlState::ROLLOVER) + option.state = QStyle::State_MouseOver; + if (pSpinVal->mnLowerState & ControlState::ROLLOVER) + option.state = QStyle::State_MouseOver; + } + + draw(QStyle::CC_SpinBox, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else if (type == ControlType::Radiobutton) + { + if (part == ControlPart::Entire) + { + QStyleOptionButton option; + // clear FOCUSED bit, focus is drawn separately + nControlState &= ~ControlState::FOCUSED; + draw(QStyle::CE_RadioButton, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else if (part == ControlPart::Focus) + { + QStyleOptionFocusRect option; + draw(QStyle::PE_FrameFocusRect, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + } + else if (type == ControlType::Tooltip) + { + QStyleOption option; + draw(QStyle::PE_PanelTipLabel, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else if (type == ControlType::Frame) + { + drawFrame(QStyle::PE_Frame, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else if (type == ControlType::WindowBackground) + { + // Nothing to do - see "Default image color" switch ^^ + } + else if (type == ControlType::Fixedline) + { + QStyleOptionMenuItem option; + option.menuItemType = QStyleOptionMenuItem::Separator; + option.state = vclStateValue2StateFlag(nControlState, value); + option.state |= QStyle::State_Item; + + draw(QStyle::CE_MenuItem, option, m_image.get(), rBackgroundColor); + } + else if (type == ControlType::Slider + && (part == ControlPart::TrackHorzArea || part == ControlPart::TrackVertArea)) + { + assert(value.getType() == ControlType::Slider); + const SliderValue* slVal = static_cast<const SliderValue*>(&value); + QStyleOptionSlider option; + + option.state = vclStateValue2StateFlag(nControlState, value); + option.maximum = slVal->mnMax; + option.minimum = slVal->mnMin; + option.sliderPosition = option.sliderValue = slVal->mnCur; + bool horizontal = (part == ControlPart::TrackHorzArea); //horizontal or vertical + option.orientation = horizontal ? Qt::Horizontal : Qt::Vertical; + if (horizontal) + option.state |= QStyle::State_Horizontal; + + draw(QStyle::CC_Slider, option, m_image.get(), rBackgroundColor); + } + else if (type == ControlType::Progress && part == ControlPart::Entire) + { + QStyleOptionProgressBar option; + option.minimum = 0; + option.maximum = widgetRect.width(); + option.progress = value.getNumericVal(); + + draw(QStyle::CE_ProgressBar, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else if (type == ControlType::TabItem && part == ControlPart::Entire) + { + QStyleOptionTab sot; + fillQStyleOptionTab(value, sot); + draw(QStyle::CE_TabBarTabShape, sot, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value)); + } + else if (type == ControlType::TabPane && part == ControlPart::Entire) + { + const TabPaneValue& rValue = static_cast<const TabPaneValue&>(value); + + // get the overlap size for the tabs, so they will overlap the frame + QStyleOptionTab tabOverlap; + tabOverlap.shape = QTabBar::RoundedNorth; + TabPaneValue::m_nOverlap = pixelMetric(QStyle::PM_TabBarBaseOverlap, &tabOverlap); + + QStyleOptionTabWidgetFrame option; + fullQStyleOptionTabWidgetFrame(option, false); + option.tabBarRect = toQRect(rValue.m_aTabHeaderRect); + option.selectedTabRect + = rValue.m_aSelectedTabRect.IsEmpty() ? QRect() : toQRect(rValue.m_aSelectedTabRect); + option.tabBarSize = toQSize(rValue.m_aTabHeaderRect.GetSize()); + option.rect = m_image->rect(); + QRect aRect = subElementRect(QStyle::SE_TabWidgetTabPane, &option); + draw(QStyle::PE_FrameTabWidget, option, m_image.get(), rBackgroundColor, + vclStateValue2StateFlag(nControlState, value), aRect); + } + else + { + returnVal = false; + } + + return returnVal; +} + +bool QtGraphics_Controls::getNativeControlRegion(ControlType type, ControlPart part, + const tools::Rectangle& controlRegion, + ControlState controlState, + const ImplControlValue& val, const OUString&, + tools::Rectangle& nativeBoundingRegion, + tools::Rectangle& nativeContentRegion) +{ + bool retVal = false; + + QRect boundingRect = toQRect(controlRegion); + QRect contentRect = boundingRect; + QStyleOptionComplex styleOption; + + switch (type) + { + // Metrics of the push button + case ControlType::Pushbutton: + if (part == ControlPart::Entire) + { + styleOption.state = vclStateValue2StateFlag(controlState, val); + + if (controlState & ControlState::DEFAULT) + { + int size = upscale(pixelMetric(QStyle::PM_ButtonDefaultIndicator, &styleOption), + Round::Ceil); + boundingRect.adjust(-size, -size, size, size); + retVal = true; + } + } + else if (part == ControlPart::Focus) + retVal = true; + break; + case ControlType::Editbox: + case ControlType::MultilineEditbox: + { + // we have to get stable borders, otherwise layout loops. + // so we simply only scale the detected borders. + QStyleOptionFrame fo; + fo.frameShape = QFrame::StyledPanel; + fo.state = QStyle::State_Sunken; + fo.lineWidth = pixelMetric(QStyle::PM_DefaultFrameWidth); + fo.rect = downscale(contentRect); + fo.rect.setSize(sizeFromContents(QStyle::CT_LineEdit, &fo, fo.rect.size())); + QRect aSubRect = subElementRect(QStyle::SE_LineEditContents, &fo); + + // VCL tests borders with small defaults before layout, where Qt returns no sub-rect, + // so this gets us at least some frame. + int nLine = upscale(fo.lineWidth, Round::Ceil); + int nLeft = qMin(-nLine, upscale(fo.rect.left() - aSubRect.left(), Round::Floor)); + int nTop = qMin(-nLine, upscale(fo.rect.top() - aSubRect.top(), Round::Floor)); + int nRight = qMax(nLine, upscale(fo.rect.right() - aSubRect.right(), Round::Ceil)); + int nBottom = qMax(nLine, upscale(fo.rect.bottom() - aSubRect.bottom(), Round::Ceil)); + boundingRect.adjust(nLeft, nTop, nRight, nBottom); + + // tdf#150451: ensure a minimum size that fits text content + frame at top and bottom. + // Themes may use the widget type for determining the actual frame width to use, + // so pass a dummy QLineEdit + // + // NOTE: This is currently only done here for the minimum size calculation and + // not above because the handling for edit boxes here and in the calling code + // currently does all kinds of "interesting" things like doing extra size adjustments + // or passing the content rect where the bounding rect would be expected,... + // Ideally this should be cleaned up in the callers and all platform integrations + // to adhere to what the doc in vcl/inc/WidgetDrawInterface.hxx says, but this + // here keeps it working with existing code for now. + // (s.a. discussion in https://gerrit.libreoffice.org/c/core/+/146516 for more details) + QLineEdit aDummyEdit; + const int nFrameWidth = pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, &aDummyEdit); + QFontMetrics aFontMetrics(QApplication::font()); + const int minHeight = upscale(aFontMetrics.height() + 2 * nFrameWidth, Round::Floor); + if (boundingRect.height() < minHeight) + { + const int nDiff = minHeight - boundingRect.height(); + boundingRect.setHeight(boundingRect.height() + nDiff); + contentRect.setHeight(contentRect.height() + nDiff); + } + + retVal = true; + break; + } + case ControlType::Checkbox: + if (part == ControlPart::Entire) + { + styleOption.state = vclStateValue2StateFlag(controlState, val); + + int nWidth = pixelMetric(QStyle::PM_IndicatorWidth, &styleOption); + int nHeight = pixelMetric(QStyle::PM_IndicatorHeight, &styleOption); + contentRect.setSize(upscale(QSize(nWidth, nHeight), Round::Ceil)); + + int nHMargin = pixelMetric(QStyle::PM_FocusFrameHMargin, &styleOption); + int nVMargin = pixelMetric(QStyle::PM_FocusFrameVMargin, &styleOption); + contentRect.adjust(0, 0, 2 * upscale(nHMargin, Round::Ceil), + 2 * upscale(nVMargin, Round::Ceil)); + + boundingRect = contentRect; + retVal = true; + } + break; + case ControlType::Combobox: + case ControlType::Listbox: + { + QStyleOptionComboBox cbo; + + cbo.rect = downscale(QRect(0, 0, contentRect.width(), contentRect.height())); + cbo.state = vclStateValue2StateFlag(controlState, val); + + switch (part) + { + case ControlPart::Entire: + { + // find out the minimum size that should be used + // assume contents is a text line + QSize aContentSize = downscale(contentRect.size(), Round::Ceil); + QFontMetrics aFontMetrics(QApplication::font()); + aContentSize.setHeight(aFontMetrics.height()); + QSize aMinSize = upscale( + sizeFromContents(QStyle::CT_ComboBox, &cbo, aContentSize), Round::Ceil); + if (aMinSize.height() > contentRect.height()) + contentRect.setHeight(aMinSize.height()); + boundingRect = contentRect; + retVal = true; + break; + } + case ControlPart::ButtonDown: + { + contentRect = upscale( + subControlRect(QStyle::CC_ComboBox, &cbo, QStyle::SC_ComboBoxArrow)); + contentRect.translate(boundingRect.left(), boundingRect.top()); + retVal = true; + break; + } + case ControlPart::SubEdit: + { + contentRect = upscale( + subControlRect(QStyle::CC_ComboBox, &cbo, QStyle::SC_ComboBoxEditField)); + contentRect.translate(boundingRect.left(), boundingRect.top()); + retVal = true; + break; + } + default: + break; + } + break; + } + case ControlType::Spinbox: + { + QStyleOptionSpinBox sbo; + sbo.frame = true; + + sbo.rect = downscale(QRect(0, 0, contentRect.width(), contentRect.height())); + sbo.state = vclStateValue2StateFlag(controlState, val); + + switch (part) + { + case ControlPart::Entire: + { + QSize aContentSize = downscale(contentRect.size(), Round::Ceil); + QFontMetrics aFontMetrics(QApplication::font()); + aContentSize.setHeight(aFontMetrics.height()); + QSize aMinSize = upscale( + sizeFromContents(QStyle::CT_SpinBox, &sbo, aContentSize), Round::Ceil); + if (aMinSize.height() > contentRect.height()) + contentRect.setHeight(aMinSize.height()); + boundingRect = contentRect; + retVal = true; + break; + } + case ControlPart::ButtonUp: + contentRect + = upscale(subControlRect(QStyle::CC_SpinBox, &sbo, QStyle::SC_SpinBoxUp)); + contentRect.translate(boundingRect.left(), boundingRect.top()); + retVal = true; + break; + case ControlPart::ButtonDown: + contentRect + = upscale(subControlRect(QStyle::CC_SpinBox, &sbo, QStyle::SC_SpinBoxDown)); + contentRect.translate(boundingRect.left(), boundingRect.top()); + retVal = true; + break; + case ControlPart::SubEdit: + contentRect = upscale( + subControlRect(QStyle::CC_SpinBox, &sbo, QStyle::SC_SpinBoxEditField)); + contentRect.translate(boundingRect.left(), boundingRect.top()); + retVal = true; + break; + default: + break; + } + break; + } + case ControlType::MenuPopup: + { + int h, w; + switch (part) + { + case ControlPart::MenuItemCheckMark: + h = upscale(pixelMetric(QStyle::PM_IndicatorHeight), Round::Floor); + w = upscale(pixelMetric(QStyle::PM_IndicatorWidth), Round::Floor); + retVal = true; + break; + case ControlPart::MenuItemRadioMark: + h = upscale(pixelMetric(QStyle::PM_ExclusiveIndicatorHeight), Round::Floor); + w = upscale(pixelMetric(QStyle::PM_ExclusiveIndicatorWidth), Round::Floor); + retVal = true; + break; + default: + break; + } + if (retVal) + { + contentRect = QRect(0, 0, w, h); + boundingRect = contentRect; + } + break; + } + case ControlType::Frame: + { + if (part == ControlPart::Border) + { + int nFrameWidth = upscale(pixelMetric(QStyle::PM_DefaultFrameWidth), Round::Ceil); + contentRect.adjust(nFrameWidth, nFrameWidth, -nFrameWidth, -nFrameWidth); + retVal = true; + } + break; + } + case ControlType::Radiobutton: + { + const int h = upscale(pixelMetric(QStyle::PM_ExclusiveIndicatorHeight), Round::Ceil); + const int w = upscale(pixelMetric(QStyle::PM_ExclusiveIndicatorWidth), Round::Ceil); + + contentRect = QRect(boundingRect.left(), boundingRect.top(), w, h); + int nHMargin = pixelMetric(QStyle::PM_FocusFrameHMargin, &styleOption); + int nVMargin = pixelMetric(QStyle::PM_FocusFrameVMargin, &styleOption); + contentRect.adjust(0, 0, upscale(2 * nHMargin, Round::Ceil), + upscale(2 * nVMargin, Round::Ceil)); + boundingRect = contentRect; + + retVal = true; + break; + } + case ControlType::Slider: + { + const int w = upscale(pixelMetric(QStyle::PM_SliderLength), Round::Ceil); + if (part == ControlPart::ThumbHorz) + { + contentRect + = QRect(boundingRect.left(), boundingRect.top(), w, boundingRect.height()); + boundingRect = contentRect; + retVal = true; + } + else if (part == ControlPart::ThumbVert) + { + contentRect + = QRect(boundingRect.left(), boundingRect.top(), boundingRect.width(), w); + boundingRect = contentRect; + retVal = true; + } + break; + } + case ControlType::Toolbar: + { + const int nWorH = upscale(pixelMetric(QStyle::PM_ToolBarHandleExtent), Round::Ceil); + if (part == ControlPart::ThumbHorz) + { + contentRect + = QRect(boundingRect.left(), boundingRect.top(), boundingRect.width(), nWorH); + boundingRect = contentRect; + retVal = true; + } + else if (part == ControlPart::ThumbVert) + { + contentRect + = QRect(boundingRect.left(), boundingRect.top(), nWorH, boundingRect.height()); + boundingRect = contentRect; + retVal = true; + } + else if (part == ControlPart::Button) + { + QStyleOptionToolButton option; + option.arrowType = Qt::NoArrow; + option.features = QStyleOptionToolButton::None; + option.rect = downscale(QRect({ 0, 0 }, contentRect.size())); + contentRect = upscale( + subControlRect(QStyle::CC_ToolButton, &option, QStyle::SC_ToolButton)); + boundingRect = contentRect; + retVal = true; + } + break; + } + case ControlType::Scrollbar: + { + // core can't handle 3-button scrollbars well, so we fix that in hitTestNativeControl(), + // for the rest also provide the track area (i.e. area not taken by buttons) + if (part == ControlPart::TrackVertArea || part == ControlPart::TrackHorzArea) + { + QStyleOptionSlider option; + bool horizontal = (part == ControlPart::TrackHorzArea); //horizontal or vertical + option.orientation = horizontal ? Qt::Horizontal : Qt::Vertical; + if (horizontal) + option.state |= QStyle::State_Horizontal; + // getNativeControlRegion usually gets ImplControlValue as 'val' (i.e. not the proper + // subclass), so use random sensible values (doesn't matter anyway, as the wanted + // geometry here depends only on button sizes) + option.maximum = 10; + option.minimum = 0; + option.sliderPosition = option.sliderValue = 4; + option.pageStep = 2; + // Adjust coordinates to make the widget appear to be at (0,0), i.e. make + // widget and screen coordinates the same. QStyle functions should use screen + // coordinates but at least QPlastiqueStyle::subControlRect() is buggy + // and sometimes uses widget coordinates. + option.rect = downscale(QRect({ 0, 0 }, contentRect.size())); + contentRect = upscale( + subControlRect(QStyle::CC_ScrollBar, &option, QStyle::SC_ScrollBarGroove)); + contentRect.translate(boundingRect.left() + - (contentRect.width() - boundingRect.width()), + boundingRect.top()); + boundingRect = contentRect; + retVal = true; + } + break; + } + case ControlType::TabItem: + { + QStyleOptionTab sot; + fillQStyleOptionTab(val, sot); + QSize aMinSize = upscale(sizeFromContents(QStyle::CT_TabBarTab, &sot, + downscale(contentRect.size(), Round::Ceil)), + Round::Ceil); + contentRect.setSize(aMinSize); + boundingRect = contentRect; + retVal = true; + break; + } + case ControlType::TabPane: + { + const TabPaneValue& rValue = static_cast<const TabPaneValue&>(val); + QStyleOptionTabWidgetFrame sotwf; + fullQStyleOptionTabWidgetFrame(sotwf, true); + QSize contentSize( + std::max(rValue.m_aTabHeaderRect.GetWidth(), controlRegion.GetWidth()), + rValue.m_aTabHeaderRect.GetHeight() + controlRegion.GetHeight()); + QSize aMinSize = upscale( + sizeFromContents(QStyle::CT_TabWidget, &sotwf, downscale(contentSize, Round::Ceil)), + Round::Ceil); + contentRect.setSize(aMinSize); + boundingRect = contentRect; + retVal = true; + break; + } + default: + break; + } + if (retVal) + { + nativeBoundingRegion = toRectangle(boundingRect); + nativeContentRegion = toRectangle(contentRect); + } + + return retVal; +} + +/** Test whether the position is in the native widget. + If the return value is true, bIsInside contains information whether + aPos was or was not inside the native widget specified by the + nType/nPart combination. +*/ +bool QtGraphics_Controls::hitTestNativeControl(ControlType nType, ControlPart nPart, + const tools::Rectangle& rControlRegion, + const Point& rPos, bool& rIsInside) +{ + if (nType == ControlType::Scrollbar) + { + if (nPart != ControlPart::ButtonUp && nPart != ControlPart::ButtonDown + && nPart != ControlPart::ButtonLeft && nPart != ControlPart::ButtonRight) + { // we adjust only for buttons (because some scrollbars have 3 buttons, + // and LO core doesn't handle such scrollbars well) + return false; + } + rIsInside = false; + bool bHorizontal = (nPart == ControlPart::ButtonLeft || nPart == ControlPart::ButtonRight); + QRect rect = toQRect(rControlRegion); + QPoint pos(rPos.X(), rPos.Y()); + // Adjust coordinates to make the widget appear to be at (0,0), i.e. make + // widget and screen coordinates the same. QStyle functions should use screen + // coordinates but at least QPlastiqueStyle::subControlRect() is buggy + // and sometimes uses widget coordinates. + pos -= rect.topLeft(); + rect.moveTo(0, 0); + QStyleOptionSlider options; + options.orientation = bHorizontal ? Qt::Horizontal : Qt::Vertical; + if (bHorizontal) + options.state |= QStyle::State_Horizontal; + options.rect = rect; + // some random sensible values, since we call this code only for scrollbar buttons, + // the slider position does not exactly matter + options.maximum = 10; + options.minimum = 0; + options.sliderPosition = options.sliderValue = 4; + options.pageStep = 2; + QStyle::SubControl control + = QApplication::style()->hitTestComplexControl(QStyle::CC_ScrollBar, &options, pos); + if (nPart == ControlPart::ButtonUp || nPart == ControlPart::ButtonLeft) + rIsInside = (control == QStyle::SC_ScrollBarSubLine); + else // DOWN, RIGHT + rIsInside = (control == QStyle::SC_ScrollBarAddLine); + return true; + } + return false; +} + +inline int QtGraphics_Controls::downscale(int size, Round eRound) +{ + return static_cast<int>(eRound == Round::Ceil ? ceil(size / m_rGraphics.devicePixelRatioF()) + : floor(size / m_rGraphics.devicePixelRatioF())); +} + +inline int QtGraphics_Controls::upscale(int size, Round eRound) +{ + return static_cast<int>(eRound == Round::Ceil ? ceil(size * m_rGraphics.devicePixelRatioF()) + : floor(size * m_rGraphics.devicePixelRatioF())); +} + +inline QRect QtGraphics_Controls::downscale(const QRect& rect) +{ + return QRect(downscale(rect.x(), Round::Floor), downscale(rect.y(), Round::Floor), + downscale(rect.width(), Round::Ceil), downscale(rect.height(), Round::Ceil)); +} + +inline QRect QtGraphics_Controls::upscale(const QRect& rect) +{ + return QRect(upscale(rect.x(), Round::Floor), upscale(rect.y(), Round::Floor), + upscale(rect.width(), Round::Ceil), upscale(rect.height(), Round::Ceil)); +} + +inline QSize QtGraphics_Controls::downscale(const QSize& size, Round eRound) +{ + return QSize(downscale(size.width(), eRound), downscale(size.height(), eRound)); +} + +inline QSize QtGraphics_Controls::upscale(const QSize& size, Round eRound) +{ + return QSize(upscale(size.width(), eRound), upscale(size.height(), eRound)); +} + +inline QPoint QtGraphics_Controls::upscale(const QPoint& point, Round eRound) +{ + return QPoint(upscale(point.x(), eRound), upscale(point.y(), eRound)); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtGraphics_GDI.cxx b/vcl/qt5/QtGraphics_GDI.cxx new file mode 100644 index 0000000000..2005de80f7 --- /dev/null +++ b/vcl/qt5/QtGraphics_GDI.cxx @@ -0,0 +1,710 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtGraphics.hxx> + +#include <QtBitmap.hxx> +#include <QtPainter.hxx> + +#include <sal/log.hxx> + +#include <QtGui/QPainter> +#include <QtGui/QScreen> +#include <QtGui/QWindow> +#include <QtWidgets/QWidget> + +#include <numeric> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> + +QtGraphicsBackend::QtGraphicsBackend(QtFrame* pFrame, QImage* pQImage) + : m_pFrame(pFrame) + , m_pQImage(pQImage) + , m_oLineColor(std::in_place, 0x00, 0x00, 0x00) + , m_oFillColor(std::in_place, 0xFF, 0xFF, 0XFF) + , m_eCompositionMode(QPainter::CompositionMode_SourceOver) +{ + ResetClipRegion(); +} + +QtGraphicsBackend::~QtGraphicsBackend() {} + +const basegfx::B2DPoint aHalfPointOfs(0.5, 0.5); + +static void AddPolygonToPath(QPainterPath& rPath, const basegfx::B2DPolygon& rPolygon, + bool bClosePath, bool bPixelSnap, bool bLineDraw) +{ + const int nPointCount = rPolygon.count(); + // short circuit if there is nothing to do + if (nPointCount == 0) + return; + + const bool bHasCurves = rPolygon.areControlPointsUsed(); + for (int nPointIdx = 0, nPrevIdx = 0;; nPrevIdx = nPointIdx++) + { + int nClosedIdx = nPointIdx; + if (nPointIdx >= nPointCount) + { + // prepare to close last curve segment if needed + if (bClosePath && (nPointIdx == nPointCount)) + nClosedIdx = 0; + else + break; + } + + basegfx::B2DPoint aPoint = rPolygon.getB2DPoint(nClosedIdx); + + if (bPixelSnap) + { + // snap device coordinates to full pixels + aPoint.setX(basegfx::fround(aPoint.getX())); + aPoint.setY(basegfx::fround(aPoint.getY())); + } + + if (bLineDraw) + aPoint += aHalfPointOfs; + if (!nPointIdx) + { + // first point => just move there + rPath.moveTo(aPoint.getX(), aPoint.getY()); + continue; + } + + bool bPendingCurve = false; + if (bHasCurves) + { + bPendingCurve = rPolygon.isNextControlPointUsed(nPrevIdx); + bPendingCurve |= rPolygon.isPrevControlPointUsed(nClosedIdx); + } + + if (!bPendingCurve) // line segment + rPath.lineTo(aPoint.getX(), aPoint.getY()); + else // cubic bezier segment + { + basegfx::B2DPoint aCP1 = rPolygon.getNextControlPoint(nPrevIdx); + basegfx::B2DPoint aCP2 = rPolygon.getPrevControlPoint(nClosedIdx); + if (bLineDraw) + { + aCP1 += aHalfPointOfs; + aCP2 += aHalfPointOfs; + } + rPath.cubicTo(aCP1.getX(), aCP1.getY(), aCP2.getX(), aCP2.getY(), aPoint.getX(), + aPoint.getY()); + } + } + + if (bClosePath) + rPath.closeSubpath(); +} + +static bool AddPolyPolygonToPath(QPainterPath& rPath, const basegfx::B2DPolyPolygon& rPolyPoly, + bool bPixelSnap, bool bLineDraw) +{ + if (rPolyPoly.count() == 0) + return false; + for (auto const& rPolygon : rPolyPoly) + { + AddPolygonToPath(rPath, rPolygon, true, bPixelSnap, bLineDraw); + } + return true; +} + +void QtGraphicsBackend::setClipRegion(const vcl::Region& rRegion) +{ + if (rRegion.IsRectangle()) + { + m_aClipRegion = toQRect(rRegion.GetBoundRect()); + if (!m_aClipPath.isEmpty()) + { + QPainterPath aPath; + m_aClipPath.swap(aPath); + } + } + else if (!rRegion.HasPolyPolygonOrB2DPolyPolygon()) + { + QRegion aQRegion; + RectangleVector aRectangles; + rRegion.GetRegionRectangles(aRectangles); + for (const auto& rRect : aRectangles) + aQRegion += toQRect(rRect); + m_aClipRegion = aQRegion; + if (!m_aClipPath.isEmpty()) + { + QPainterPath aPath; + m_aClipPath.swap(aPath); + } + } + else + { + QPainterPath aPath; + const basegfx::B2DPolyPolygon aPolyClip(rRegion.GetAsB2DPolyPolygon()); + AddPolyPolygonToPath(aPath, aPolyClip, !getAntiAlias(), false); + m_aClipPath.swap(aPath); + if (!m_aClipRegion.isEmpty()) + { + QRegion aRegion; + m_aClipRegion.swap(aRegion); + } + } +} + +void QtGraphicsBackend::ResetClipRegion() +{ + if (m_pQImage) + m_aClipRegion = QRegion(m_pQImage->rect()); + else + m_aClipRegion = QRegion(); + if (!m_aClipPath.isEmpty()) + { + QPainterPath aPath; + m_aClipPath.swap(aPath); + } +} + +void QtGraphicsBackend::drawPixel(tools::Long nX, tools::Long nY) +{ + QtPainter aPainter(*this); + aPainter.drawPoint(nX, nY); + aPainter.update(nX, nY, 1, 1); +} + +void QtGraphicsBackend::drawPixel(tools::Long nX, tools::Long nY, Color nColor) +{ + QtPainter aPainter(*this); + aPainter.setPen(toQColor(nColor)); + aPainter.setPen(Qt::SolidLine); + aPainter.drawPoint(nX, nY); + aPainter.update(nX, nY, 1, 1); +} + +void QtGraphicsBackend::drawLine(tools::Long nX1, tools::Long nY1, tools::Long nX2, tools::Long nY2) +{ + QtPainter aPainter(*this); + aPainter.drawLine(nX1, nY1, nX2, nY2); + + if (nX1 > nX2) + std::swap(nX1, nX2); + if (nY1 > nY2) + std::swap(nY1, nY2); + aPainter.update(nX1, nY1, nX2 - nX1 + 1, nY2 - nY1 + 1); +} + +void QtGraphicsBackend::drawRect(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight) +{ + if (!m_oFillColor && !m_oLineColor) + return; + + QtPainter aPainter(*this, true); + if (m_oFillColor) + aPainter.fillRect(nX, nY, nWidth, nHeight, aPainter.brush()); + if (m_oLineColor) + aPainter.drawRect(nX, nY, nWidth - 1, nHeight - 1); + aPainter.update(nX, nY, nWidth, nHeight); +} + +void QtGraphicsBackend::drawPolyLine(sal_uInt32 nPoints, const Point* pPtAry) +{ + if (0 == nPoints) + return; + + QtPainter aPainter(*this); + QPoint* pPoints = new QPoint[nPoints]; + QPoint aTopLeft(pPtAry->getX(), pPtAry->getY()); + QPoint aBottomRight = aTopLeft; + for (sal_uInt32 i = 0; i < nPoints; ++i, ++pPtAry) + { + pPoints[i] = QPoint(pPtAry->getX(), pPtAry->getY()); + if (pPtAry->getX() < aTopLeft.x()) + aTopLeft.setX(pPtAry->getX()); + if (pPtAry->getY() < aTopLeft.y()) + aTopLeft.setY(pPtAry->getY()); + if (pPtAry->getX() > aBottomRight.x()) + aBottomRight.setX(pPtAry->getX()); + if (pPtAry->getY() > aBottomRight.y()) + aBottomRight.setY(pPtAry->getY()); + } + aPainter.drawPolyline(pPoints, nPoints); + delete[] pPoints; + aPainter.update(QRect(aTopLeft, aBottomRight)); +} + +void QtGraphicsBackend::drawPolygon(sal_uInt32 nPoints, const Point* pPtAry) +{ + QtPainter aPainter(*this, true); + QPolygon aPolygon(nPoints); + for (sal_uInt32 i = 0; i < nPoints; ++i, ++pPtAry) + aPolygon.setPoint(i, pPtAry->getX(), pPtAry->getY()); + aPainter.drawPolygon(aPolygon); + aPainter.update(aPolygon.boundingRect()); +} + +void QtGraphicsBackend::drawPolyPolygon(sal_uInt32 nPolyCount, const sal_uInt32* pPoints, + const Point** ppPtAry) +{ + // ignore invisible polygons + if (!m_oFillColor && !m_oLineColor) + return; + + QPainterPath aPath; + for (sal_uInt32 nPoly = 0; nPoly < nPolyCount; nPoly++) + { + const sal_uInt32 nPoints = pPoints[nPoly]; + if (nPoints > 1) + { + const Point* pPtAry = ppPtAry[nPoly]; + aPath.moveTo(pPtAry->getX(), pPtAry->getY()); + pPtAry++; + for (sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPtAry++) + aPath.lineTo(pPtAry->getX(), pPtAry->getY()); + aPath.closeSubpath(); + } + } + + QtPainter aPainter(*this, true); + aPainter.drawPath(aPath); + aPainter.update(aPath.boundingRect()); +} + +void QtGraphicsBackend::drawPolyPolygon(const basegfx::B2DHomMatrix& rObjectToDevice, + const basegfx::B2DPolyPolygon& rPolyPolygon, + double fTransparency) +{ + // ignore invisible polygons + if (!m_oFillColor && !m_oLineColor) + return; + if ((fTransparency >= 1.0) || (fTransparency < 0)) + return; + + // Fallback: Transform to DeviceCoordinates + basegfx::B2DPolyPolygon aPolyPolygon(rPolyPolygon); + aPolyPolygon.transform(rObjectToDevice); + + QPainterPath aPath; + // ignore empty polygons + if (!AddPolyPolygonToPath(aPath, aPolyPolygon, !getAntiAlias(), m_oLineColor.has_value())) + return; + + QtPainter aPainter(*this, true, 255 * (1.0 - fTransparency)); + aPainter.drawPath(aPath); + aPainter.update(aPath.boundingRect()); +} + +bool QtGraphicsBackend::drawPolyLineBezier(sal_uInt32 /*nPoints*/, const Point* /*pPtAry*/, + const PolyFlags* /*pFlgAry*/) +{ + return false; +} + +bool QtGraphicsBackend::drawPolygonBezier(sal_uInt32 /*nPoints*/, const Point* /*pPtAry*/, + const PolyFlags* /*pFlgAry*/) +{ + return false; +} + +bool QtGraphicsBackend::drawPolyPolygonBezier(sal_uInt32 /*nPoly*/, const sal_uInt32* /*pPoints*/, + const Point* const* /*pPtAry*/, + const PolyFlags* const* /*pFlgAry*/) +{ + return false; +} + +bool QtGraphicsBackend::drawPolyLine(const basegfx::B2DHomMatrix& rObjectToDevice, + const basegfx::B2DPolygon& rPolyLine, double fTransparency, + double fLineWidth, + const std::vector<double>* pStroke, // MM01 + basegfx::B2DLineJoin eLineJoin, css::drawing::LineCap eLineCap, + double fMiterMinimumAngle, bool bPixelSnapHairline) +{ + if (!m_oFillColor && !m_oLineColor) + { + return true; + } + + // MM01 check done for simple reasons + if (!rPolyLine.count() || fTransparency < 0.0 || fTransparency > 1.0) + { + return true; + } + + // MM01 need to do line dashing as fallback stuff here now + const double fDotDashLength( + nullptr != pStroke ? std::accumulate(pStroke->begin(), pStroke->end(), 0.0) : 0.0); + const bool bStrokeUsed(0.0 != fDotDashLength); + assert(!bStrokeUsed || (bStrokeUsed && pStroke)); + basegfx::B2DPolyPolygon aPolyPolygonLine; + + if (bStrokeUsed) + { + // apply LineStyle + basegfx::utils::applyLineDashing(rPolyLine, // source + *pStroke, // pattern + &aPolyPolygonLine, // target for lines + nullptr, // target for gaps + fDotDashLength); // full length if available + } + else + { + // no line dashing, just copy + aPolyPolygonLine.append(rPolyLine); + } + + // Transform to DeviceCoordinates, get DeviceLineWidth, execute PixelSnapHairline + aPolyPolygonLine.transform(rObjectToDevice); + if (bPixelSnapHairline) + { + aPolyPolygonLine = basegfx::utils::snapPointsOfHorizontalOrVerticalEdges(aPolyPolygonLine); + } + + // tdf#124848 get correct LineWidth in discrete coordinates, + if (fLineWidth == 0) // hairline + fLineWidth = 1.0; + else // Adjust line width for object-to-device scale. + fLineWidth = (rObjectToDevice * basegfx::B2DVector(fLineWidth, 0)).getLength(); + + // setup poly-polygon path + QPainterPath aPath; + + // MM01 todo - I assume that this is OKAY to be done in one run for Qt, + // but this NEEDS to be checked/verified + for (sal_uInt32 a(0); a < aPolyPolygonLine.count(); a++) + { + const basegfx::B2DPolygon aPolyLine(aPolyPolygonLine.getB2DPolygon(a)); + AddPolygonToPath(aPath, aPolyLine, aPolyLine.isClosed(), !getAntiAlias(), true); + } + + QtPainter aPainter(*this, false, 255 * (1.0 - fTransparency)); + + // setup line attributes + QPen aPen = aPainter.pen(); + aPen.setWidth(fLineWidth); + + switch (eLineJoin) + { + case basegfx::B2DLineJoin::Bevel: + aPen.setJoinStyle(Qt::BevelJoin); + break; + case basegfx::B2DLineJoin::Round: + aPen.setJoinStyle(Qt::RoundJoin); + break; + case basegfx::B2DLineJoin::NONE: + case basegfx::B2DLineJoin::Miter: + aPen.setMiterLimit(1.0 / sin(fMiterMinimumAngle / 2.0)); + aPen.setJoinStyle(Qt::MiterJoin); + break; + } + + switch (eLineCap) + { + default: // css::drawing::LineCap_BUTT: + aPen.setCapStyle(Qt::FlatCap); + break; + case css::drawing::LineCap_ROUND: + aPen.setCapStyle(Qt::RoundCap); + break; + case css::drawing::LineCap_SQUARE: + aPen.setCapStyle(Qt::SquareCap); + break; + } + + aPainter.setPen(aPen); + aPainter.drawPath(aPath); + aPainter.update(aPath.boundingRect()); + return true; +} + +bool QtGraphicsBackend::drawGradient(const tools::PolyPolygon& /*rPolyPolygon*/, + const Gradient& /*rGradient*/) +{ + return false; +} + +bool QtGraphicsBackend::implDrawGradient(basegfx::B2DPolyPolygon const& /*rPolyPolygon*/, + SalGradient const& /*rGradient*/) +{ + return false; +} + +void QtGraphicsBackend::drawScaledImage(const SalTwoRect& rPosAry, const QImage& rImage) +{ + QtPainter aPainter(*this); + QRect aSrcRect(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight); + QRect aDestRect(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight); + aPainter.drawImage(aDestRect, rImage, aSrcRect); + aPainter.update(aDestRect); +} + +void QtGraphicsBackend::copyArea(tools::Long nDestX, tools::Long nDestY, tools::Long nSrcX, + tools::Long nSrcY, tools::Long nSrcWidth, tools::Long nSrcHeight, + bool /*bWindowInvalidate*/) +{ + if (nDestX == nSrcX && nDestY == nSrcY) + return; + + SalTwoRect aTR(nSrcX, nSrcY, nSrcWidth, nSrcHeight, nDestX, nDestY, nSrcWidth, nSrcHeight); + + QImage* pImage = m_pQImage; + QImage aImage = pImage->copy(aTR.mnSrcX, aTR.mnSrcY, aTR.mnSrcWidth, aTR.mnSrcHeight); + pImage = &aImage; + aTR.mnSrcX = 0; + aTR.mnSrcY = 0; + + drawScaledImage(aTR, *pImage); +} + +void QtGraphicsBackend::copyBits(const SalTwoRect& rPosAry, SalGraphics* pSrcGraphics) +{ + if (rPosAry.mnSrcWidth <= 0 || rPosAry.mnSrcHeight <= 0 || rPosAry.mnDestWidth <= 0 + || rPosAry.mnDestHeight <= 0) + return; + + QImage aImage, *pImage; + SalTwoRect aPosAry = rPosAry; + + if (!pSrcGraphics) + { + pImage = m_pQImage; + aImage + = pImage->copy(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight); + pImage = &aImage; + aPosAry.mnSrcX = 0; + aPosAry.mnSrcY = 0; + } + else + pImage = static_cast<QtGraphics*>(pSrcGraphics)->getQImage(); + + drawScaledImage(aPosAry, *pImage); +} + +void QtGraphicsBackend::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap) +{ + if (rPosAry.mnSrcWidth <= 0 || rPosAry.mnSrcHeight <= 0 || rPosAry.mnDestWidth <= 0 + || rPosAry.mnDestHeight <= 0) + return; + + const QImage* pImage = static_cast<const QtBitmap*>(&rSalBitmap)->GetQImage(); + + assert(pImage); + + drawScaledImage(rPosAry, *pImage); +} + +void QtGraphicsBackend::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& /*rSalBitmap*/, + const SalBitmap& /*rTransparentBitmap*/) +{ + if (rPosAry.mnSrcWidth <= 0 || rPosAry.mnSrcHeight <= 0 || rPosAry.mnDestWidth <= 0 + || rPosAry.mnDestHeight <= 0) + return; + + assert(rPosAry.mnSrcWidth == rPosAry.mnDestWidth); + assert(rPosAry.mnSrcHeight == rPosAry.mnDestHeight); +} + +void QtGraphicsBackend::drawMask(const SalTwoRect& rPosAry, const SalBitmap& /*rSalBitmap*/, + Color /*nMaskColor*/) +{ + if (rPosAry.mnSrcWidth <= 0 || rPosAry.mnSrcHeight <= 0 || rPosAry.mnDestWidth <= 0 + || rPosAry.mnDestHeight <= 0) + return; + + assert(rPosAry.mnSrcWidth == rPosAry.mnDestWidth); + assert(rPosAry.mnSrcHeight == rPosAry.mnDestHeight); +} + +std::shared_ptr<SalBitmap> QtGraphicsBackend::getBitmap(tools::Long nX, tools::Long nY, + tools::Long nWidth, tools::Long nHeight) +{ + return std::make_shared<QtBitmap>(m_pQImage->copy(nX, nY, nWidth, nHeight)); +} + +Color QtGraphicsBackend::getPixel(tools::Long nX, tools::Long nY) +{ + return Color(ColorTransparency, m_pQImage->pixel(nX, nY)); +} + +void QtGraphicsBackend::invert(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight, SalInvert nFlags) +{ + QtPainter aPainter(*this); + if (SalInvert::N50 & nFlags) + { + aPainter.setCompositionMode(QPainter::RasterOp_SourceXorDestination); + QBrush aBrush(Qt::white, Qt::Dense4Pattern); + aPainter.fillRect(nX, nY, nWidth, nHeight, aBrush); + } + else + { + if (SalInvert::TrackFrame & nFlags) + { + aPainter.setCompositionMode(QPainter::RasterOp_SourceXorDestination); + QPen aPen(Qt::white); + aPen.setStyle(Qt::DotLine); + aPainter.setPen(aPen); + aPainter.drawRect(nX, nY, nWidth, nHeight); + } + else + { + aPainter.setCompositionMode(QPainter::RasterOp_SourceXorDestination); + aPainter.fillRect(nX, nY, nWidth, nHeight, Qt::white); + } + } + aPainter.update(nX, nY, nWidth, nHeight); +} + +void QtGraphicsBackend::invert(sal_uInt32 /*nPoints*/, const Point* /*pPtAry*/, + SalInvert /*nFlags*/) +{ +} + +bool QtGraphicsBackend::drawEPS(tools::Long /*nX*/, tools::Long /*nY*/, tools::Long /*nWidth*/, + tools::Long /*nHeight*/, void* /*pPtr*/, sal_uInt32 /*nSize*/) +{ + return false; +} + +bool QtGraphicsBackend::blendBitmap(const SalTwoRect&, const SalBitmap& /*rBitmap*/) +{ + return false; +} + +bool QtGraphicsBackend::blendAlphaBitmap(const SalTwoRect&, const SalBitmap& /*rSrcBitmap*/, + const SalBitmap& /*rMaskBitmap*/, + const SalBitmap& /*rAlphaBitmap*/) +{ + return false; +} + +static QImage getAlphaImage(const SalBitmap& rSourceBitmap, const SalBitmap& rAlphaBitmap) +{ + assert(rSourceBitmap.GetSize() == rAlphaBitmap.GetSize()); + assert(rAlphaBitmap.GetBitCount() == 8 || rAlphaBitmap.GetBitCount() == 1); + + QImage aAlphaMask = *static_cast<const QtBitmap*>(&rAlphaBitmap)->GetQImage(); + + const QImage* pBitmap = static_cast<const QtBitmap*>(&rSourceBitmap)->GetQImage(); + QImage aImage = pBitmap->convertToFormat(Qt_DefaultFormat32); + aImage.setAlphaChannel(aAlphaMask); + return aImage; +} + +bool QtGraphicsBackend::drawAlphaBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSourceBitmap, + const SalBitmap& rAlphaBitmap) +{ + drawScaledImage(rPosAry, getAlphaImage(rSourceBitmap, rAlphaBitmap)); + return true; +} + +bool QtGraphicsBackend::drawTransformedBitmap(const basegfx::B2DPoint& rNull, + const basegfx::B2DPoint& rX, + const basegfx::B2DPoint& rY, + const SalBitmap& rSourceBitmap, + const SalBitmap* pAlphaBitmap, double fAlpha) +{ + QImage aImage; + if (!pAlphaBitmap) + aImage = *static_cast<const QtBitmap*>(&rSourceBitmap)->GetQImage(); + else + aImage = getAlphaImage(rSourceBitmap, *pAlphaBitmap); + + const basegfx::B2DVector aXRel = rX - rNull; + const basegfx::B2DVector aYRel = rY - rNull; + + QtPainter aPainter(*this); + aPainter.setOpacity(fAlpha); + aPainter.setTransform(QTransform(aXRel.getX() / aImage.width(), aXRel.getY() / aImage.width(), + aYRel.getX() / aImage.height(), aYRel.getY() / aImage.height(), + rNull.getX(), rNull.getY())); + aPainter.drawImage(QPoint(0, 0), aImage); + aPainter.update(aImage.rect()); + return true; +} + +bool QtGraphicsBackend::hasFastDrawTransformedBitmap() const { return false; } + +bool QtGraphicsBackend::drawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight, sal_uInt8 nTransparency) +{ + if (!m_oFillColor && !m_oLineColor) + return true; + assert(nTransparency <= 100); + if (nTransparency > 100) + nTransparency = 100; + QtPainter aPainter(*this, true, (100 - nTransparency) * (255.0 / 100)); + if (m_oFillColor) + aPainter.fillRect(nX, nY, nWidth, nHeight, aPainter.brush()); + if (m_oLineColor) + aPainter.drawRect(nX, nY, nWidth - 1, nHeight - 1); + aPainter.update(nX, nY, nWidth, nHeight); + return true; +} + +sal_uInt16 QtGraphicsBackend::GetBitCount() const { return getFormatBits(m_pQImage->format()); } + +tools::Long QtGraphicsBackend::GetGraphicsWidth() const { return m_pQImage->width(); } + +void QtGraphicsBackend::SetLineColor() { m_oLineColor = std::nullopt; } + +void QtGraphicsBackend::SetLineColor(Color nColor) { m_oLineColor = nColor; } + +void QtGraphicsBackend::SetFillColor() { m_oFillColor = std::nullopt; } + +void QtGraphicsBackend::SetFillColor(Color nColor) { m_oFillColor = nColor; } + +void QtGraphicsBackend::SetXORMode(bool bSet, bool) +{ + if (bSet) + m_eCompositionMode = QPainter::CompositionMode_Xor; + else + m_eCompositionMode = QPainter::CompositionMode_SourceOver; +} + +void QtGraphicsBackend::SetROPLineColor(SalROPColor /*nROPColor*/) {} + +void QtGraphicsBackend::SetROPFillColor(SalROPColor /*nROPColor*/) {} + +bool QtGraphicsBackend::supportsOperation(OutDevSupportType eType) const +{ + switch (eType) + { + case OutDevSupportType::TransparentRect: + return true; + default: + return false; + } +} + +void QtGraphics::GetResolution(sal_Int32& rDPIX, sal_Int32& rDPIY) +{ + char* pForceDpi; + if ((pForceDpi = getenv("SAL_FORCEDPI"))) + { + OString sForceDPI(pForceDpi); + rDPIX = rDPIY = sForceDPI.toInt32(); + return; + } + + if (!m_pFrame) + return; + + QScreen* pScreen = m_pFrame->GetQWidget()->screen(); + rDPIX = pScreen->logicalDotsPerInchX() * pScreen->devicePixelRatio() + 0.5; + rDPIY = pScreen->logicalDotsPerInchY() * pScreen->devicePixelRatio() + 0.5; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtGraphics_Text.cxx b/vcl/qt5/QtGraphics_Text.cxx new file mode 100644 index 0000000000..19837d510f --- /dev/null +++ b/vcl/qt5/QtGraphics_Text.cxx @@ -0,0 +1,235 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <QtGraphics.hxx> +#include <QtFontFace.hxx> +#include <QtFont.hxx> +#include <QtPainter.hxx> + +#include <vcl/fontcharmap.hxx> +#include <unx/geninst.h> +#include <unx/fontmanager.hxx> +#include <unx/glyphcache.hxx> + +#include <sallayout.hxx> +#include <font/PhysicalFontCollection.hxx> + +#include <QtGui/QGlyphRun> +#include <QtGui/QFontDatabase> +#include <QtGui/QRawFont> +#include <QtCore/QStringList> + +void QtGraphics::SetTextColor(Color nColor) { m_aTextColor = nColor; } + +void QtGraphics::SetFont(LogicalFontInstance* pReqFont, int nFallbackLevel) +{ + // release the text styles + for (int i = nFallbackLevel; i < MAX_FALLBACK; ++i) + { + if (!m_pTextStyle[i]) + break; + m_pTextStyle[i].clear(); + } + + if (!pReqFont) + return; + + m_pTextStyle[nFallbackLevel] = static_cast<QtFont*>(pReqFont); +} + +void QtGraphics::GetFontMetric(FontMetricDataRef& rFMD, int nFallbackLevel) +{ + QRawFont aRawFont(QRawFont::fromFont(*m_pTextStyle[nFallbackLevel])); + QtFontFace::fillAttributesFromQFont(*m_pTextStyle[nFallbackLevel], *rFMD); + + rFMD->ImplCalcLineSpacing(m_pTextStyle[nFallbackLevel].get()); + rFMD->ImplInitBaselines(m_pTextStyle[nFallbackLevel].get()); + + rFMD->SetSlant(0); + rFMD->SetWidth(aRawFont.averageCharWidth()); + + rFMD->SetMinKashida(m_pTextStyle[nFallbackLevel]->GetKashidaWidth()); +} + +FontCharMapRef QtGraphics::GetFontCharMap() const +{ + if (!m_pTextStyle[0]) + return FontCharMapRef(new FontCharMap()); + return m_pTextStyle[0]->GetFontFace()->GetFontCharMap(); +} + +bool QtGraphics::GetFontCapabilities(vcl::FontCapabilities& rFontCapabilities) const +{ + if (!m_pTextStyle[0]) + return false; + return m_pTextStyle[0]->GetFontFace()->GetFontCapabilities(rFontCapabilities); +} + +void QtGraphics::GetDevFontList(vcl::font::PhysicalFontCollection* pPFC) +{ + static const bool bUseFontconfig = (nullptr == getenv("SAL_VCL_QT5_NO_FONTCONFIG")); + + if (pPFC->Count()) + return; + + FreetypeManager& rFontManager = FreetypeManager::get(); + psp::PrintFontManager& rMgr = psp::PrintFontManager::get(); + ::std::vector<psp::fontID> aList; + + rMgr.getFontList(aList); + for (auto const& nFontId : aList) + { + auto const* pFont = rMgr.getFont(nFontId); + if (!pFont) + continue; + + // normalize face number to the FreetypeManager + int nFaceNum = rMgr.getFontFaceNumber(nFontId); + int nVariantNum = rMgr.getFontFaceVariation(nFontId); + + // inform FreetypeManager about this font provided by the PsPrint subsystem + FontAttributes aFA = pFont->m_aFontAttributes; + aFA.IncreaseQualityBy(4096); + const OString& rFileName = rMgr.getFontFileSysPath(nFontId); + rFontManager.AddFontFile(rFileName, nFaceNum, nVariantNum, nFontId, aFA); + } + + if (bUseFontconfig) + SalGenericInstance::RegisterFontSubstitutors(pPFC); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + for (auto& family : QFontDatabase::families()) + for (auto& style : QFontDatabase::styles(family)) + pPFC->Add(QtFontFace::fromQFontDatabase(family, style)); +#else + QFontDatabase aFDB; + for (auto& family : aFDB.families()) + for (auto& style : aFDB.styles(family)) + pPFC->Add(QtFontFace::fromQFontDatabase(family, style)); +#endif +} + +void QtGraphics::ClearDevFontCache() {} + +bool QtGraphics::AddTempDevFont(vcl::font::PhysicalFontCollection*, const OUString& /*rFileURL*/, + const OUString& /*rFontName*/) +{ + return false; +} + +namespace +{ +class QtCommonSalLayout : public GenericSalLayout +{ +public: + QtCommonSalLayout(LogicalFontInstance& rLFI) + : GenericSalLayout(rLFI) + { + } + + void SetOrientation(Degree10 nOrientation) { mnOrientation = nOrientation; } +}; +} + +std::unique_ptr<GenericSalLayout> QtGraphics::GetTextLayout(int nFallbackLevel) +{ + assert(m_pTextStyle[nFallbackLevel]); + if (!m_pTextStyle[nFallbackLevel]) + return nullptr; + return std::make_unique<QtCommonSalLayout>(*m_pTextStyle[nFallbackLevel]); +} + +static QRawFont GetRawFont(const QFont& rFont, bool bWithoutHintingInTextDirection) +{ + QFont::HintingPreference eHinting = rFont.hintingPreference(); + static bool bAllowDefaultHinting = getenv("SAL_ALLOW_DEFAULT_HINTING") != nullptr; + bool bAllowedHintStyle + = !bWithoutHintingInTextDirection || bAllowDefaultHinting + || (eHinting == QFont::PreferNoHinting || eHinting == QFont::PreferVerticalHinting); + if (bWithoutHintingInTextDirection && !bAllowedHintStyle) + { + QFont aFont(rFont); + aFont.setHintingPreference(QFont::PreferVerticalHinting); + return QRawFont::fromFont(aFont); + } + return QRawFont::fromFont(rFont); +} + +void QtGraphics::DrawTextLayout(const GenericSalLayout& rLayout) +{ + const QtFont* pFont = static_cast<const QtFont*>(&rLayout.GetFont()); + assert(pFont); + QRawFont aRawFont(GetRawFont(*pFont, rLayout.GetSubpixelPositioning())); + + QVector<quint32> glyphIndexes; + QVector<QPointF> positions; + + // prevent glyph rotation inside the SalLayout + // probably better to add a parameter to GetNextGlyphs? + QtCommonSalLayout* pQtLayout + = static_cast<QtCommonSalLayout*>(const_cast<GenericSalLayout*>(&rLayout)); + Degree10 nOrientation = rLayout.GetOrientation(); + if (nOrientation) + pQtLayout->SetOrientation(0_deg10); + + basegfx::B2DPoint aPos; + const GlyphItem* pGlyph; + int nStart = 0; + while (rLayout.GetNextGlyph(&pGlyph, aPos, nStart)) + { + glyphIndexes.push_back(pGlyph->glyphId()); + positions.push_back(QPointF(aPos.getX(), aPos.getY())); + } + + // seems to be common to try to layout an empty string... + if (positions.empty()) + return; + + if (nOrientation) + pQtLayout->SetOrientation(nOrientation); + + QGlyphRun aGlyphRun; + aGlyphRun.setPositions(positions); + aGlyphRun.setGlyphIndexes(glyphIndexes); + aGlyphRun.setRawFont(aRawFont); + + QtPainter aPainter(*m_pBackend); + QColor aColor = toQColor(m_aTextColor); + aPainter.setPen(aColor); + + if (nOrientation) + { + // make text position the center of the rotation + // then rotate and move back + QRect window = aPainter.window(); + window.moveTo(-positions[0].x(), -positions[0].y()); + aPainter.setWindow(window); + + QTransform p; + p.rotate(-static_cast<qreal>(nOrientation.get()) / 10.0); + p.translate(-positions[0].x(), -positions[0].y()); + aPainter.setTransform(p); + } + + aPainter.drawGlyphRun(QPointF(), aGlyphRun); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtInstance.cxx b/vcl/qt5/QtInstance.cxx new file mode 100644 index 0000000000..4880c1bdec --- /dev/null +++ b/vcl/qt5/QtInstance.cxx @@ -0,0 +1,771 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtInstance.hxx> +#include <QtInstance.moc> + +#include <com/sun/star/lang/IllegalArgumentException.hpp> + +#include <QtBitmap.hxx> +#include <QtClipboard.hxx> +#include <QtData.hxx> +#include <QtDragAndDrop.hxx> +#include <QtFilePicker.hxx> +#include <QtFrame.hxx> +#include <QtMenu.hxx> +#include <QtObject.hxx> +#include <QtOpenGLContext.hxx> +#include "QtSvpVirtualDevice.hxx" +#include <QtSystem.hxx> +#include <QtTimer.hxx> +#include <QtVirtualDevice.hxx> + +#include <headless/svpvd.hxx> + +#include <QtCore/QAbstractEventDispatcher> +#include <QtCore/QLibraryInfo> +#include <QtCore/QThread> +#include <QtGui/QScreen> +#include <QtWidgets/QApplication> +#include <QtWidgets/QWidget> + +#include <vclpluginapi.h> +#include <tools/debug.hxx> +#include <comphelper/flagguard.hxx> +#include <dndhelper.hxx> +#include <vcl/sysdata.hxx> +#include <sal/log.hxx> +#include <osl/process.h> +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && ENABLE_GSTREAMER_1_0 && QT5_HAVE_GOBJECT +#include <unx/gstsink.hxx> +#endif +#include <headless/svpbmp.hxx> + +#include <mutex> +#include <condition_variable> + +#ifdef EMSCRIPTEN +#include <QtCore/QtPlugin> +Q_IMPORT_PLUGIN(QWasmIntegrationPlugin) +#endif + +namespace +{ +/// TODO: not much Qt specific here? could be generalised, esp. for OSX... +/// this subclass allows for the transfer of a closure for running on the main +/// thread, to handle all the thread affine stuff in Qt; the SolarMutex is +/// "loaned" to the main thread for the execution of the closure. +/// @note it doesn't work to just use "emit" and signals/slots to move calls to +/// the main thread, because the other thread has the SolarMutex; the other +/// thread (typically) cannot release SolarMutex, because then the main thread +/// will handle all sorts of events and whatnot; this design ensures that the +/// main thread only runs the passed closure (unless the closure releases +/// SolarMutex itself, which should probably be avoided). +class QtYieldMutex : public SalYieldMutex +{ +public: + /// flag only accessed on main thread: + /// main thread has "borrowed" SolarMutex from another thread + bool m_bNoYieldLock = false; + /// members for communication from non-main thread to main thread + std::mutex m_RunInMainMutex; + std::condition_variable m_InMainCondition; + bool m_isWakeUpMain = false; + std::function<void()> m_Closure; ///< code for main thread to run + /// members for communication from main thread to non-main thread + std::condition_variable m_ResultCondition; + bool m_isResultReady = false; + + virtual bool IsCurrentThread() const override; + virtual void doAcquire(sal_uInt32 nLockCount) override; + virtual sal_uInt32 doRelease(bool const bUnlockAll) override; +}; +} + +bool QtYieldMutex::IsCurrentThread() const +{ + auto const* pSalInst(GetQtInstance()); + assert(pSalInst); + if (pSalInst->IsMainThread() && m_bNoYieldLock) + { + return true; // main thread has borrowed SolarMutex + } + return SalYieldMutex::IsCurrentThread(); +} + +void QtYieldMutex::doAcquire(sal_uInt32 nLockCount) +{ + auto const* pSalInst(GetQtInstance()); + assert(pSalInst); + if (!pSalInst->IsMainThread()) + { + SalYieldMutex::doAcquire(nLockCount); + return; + } + if (m_bNoYieldLock) + { + return; // special case for main thread: borrowed from other thread + } + do // main thread acquire... + { + std::function<void()> func; // copy of closure on thread stack + { + std::unique_lock<std::mutex> g(m_RunInMainMutex); + if (m_aMutex.tryToAcquire()) + { + // if there's a closure, the other thread holds m_aMutex + assert(!m_Closure); + m_isWakeUpMain = false; + --nLockCount; // have acquired once! + ++m_nCount; + break; + } + m_InMainCondition.wait(g, [this]() { return m_isWakeUpMain; }); + m_isWakeUpMain = false; + std::swap(func, m_Closure); + } + if (func) + { + assert(!m_bNoYieldLock); + m_bNoYieldLock = true; // execute closure with borrowed SolarMutex + func(); + m_bNoYieldLock = false; + std::scoped_lock<std::mutex> g(m_RunInMainMutex); + assert(!m_isResultReady); + m_isResultReady = true; + m_ResultCondition.notify_all(); // unblock other thread + } + } while (true); + SalYieldMutex::doAcquire(nLockCount); +} + +sal_uInt32 QtYieldMutex::doRelease(bool const bUnlockAll) +{ + auto const* pSalInst(GetQtInstance()); + assert(pSalInst); + if (pSalInst->IsMainThread() && m_bNoYieldLock) + { + return 1; // dummy value + } + + std::scoped_lock<std::mutex> g(m_RunInMainMutex); + // read m_nCount before doRelease (it's guarded by m_aMutex) + bool const isReleased(bUnlockAll || m_nCount == 1); + sal_uInt32 nCount = SalYieldMutex::doRelease(bUnlockAll); + if (isReleased && !pSalInst->IsMainThread()) + { + m_isWakeUpMain = true; + m_InMainCondition.notify_all(); // unblock main thread + } + return nCount; +} + +// this could be abstracted to be independent of Qt by passing in the +// event-trigger as another function parameter... +// it could also be a template of the return type, then it could return the +// result of func... but then how to handle the result in doAcquire? +void QtInstance::RunInMainThread(std::function<void()> func) +{ + DBG_TESTSOLARMUTEX(); + if (IsMainThread()) + { + func(); + return; + } + + QtYieldMutex* const pMutex(static_cast<QtYieldMutex*>(GetYieldMutex())); + { + std::scoped_lock<std::mutex> g(pMutex->m_RunInMainMutex); + assert(!pMutex->m_Closure); + pMutex->m_Closure = func; + // unblock main thread in case it is blocked on condition + pMutex->m_isWakeUpMain = true; + pMutex->m_InMainCondition.notify_all(); + } + + TriggerUserEventProcessing(); + { + std::unique_lock<std::mutex> g(pMutex->m_RunInMainMutex); + pMutex->m_ResultCondition.wait(g, [pMutex]() { return pMutex->m_isResultReady; }); + pMutex->m_isResultReady = false; + } +} + +OUString QtInstance::constructToolkitID(std::u16string_view sTKname) +{ + OUString sID(sTKname + OUString::Concat(u" (")); + if (m_bUseCairo) + sID += "cairo+"; + else + sID += "qfont+"; + sID += toOUString(QGuiApplication::platformName()) + OUString::Concat(u")"); + return sID; +} + +QtInstance::QtInstance(std::unique_ptr<QApplication>& pQApp) + : SalGenericInstance(std::make_unique<QtYieldMutex>()) + , m_bUseCairo(nullptr == getenv("SAL_VCL_QT_USE_QFONT")) + , m_pTimer(nullptr) + , m_bSleeping(false) + , m_pQApplication(std::move(pQApp)) + , m_aUpdateStyleTimer("vcl::qt5 m_aUpdateStyleTimer") + , m_bUpdateFonts(false) + , m_pActivePopup(nullptr) +{ + ImplSVData* pSVData = ImplGetSVData(); + const OUString sToolkit = "qt" + OUString::number(QT_VERSION_MAJOR); + pSVData->maAppData.mxToolkitName = constructToolkitID(sToolkit); + + // this one needs to be blocking, so that the handling in main thread + // is processed before the thread emitting the signal continues + connect(this, SIGNAL(ImplYieldSignal(bool, bool)), this, SLOT(ImplYield(bool, bool)), + Qt::BlockingQueuedConnection); + + // this one needs to be queued non-blocking + // in order to have this event arriving to correct event processing loop + connect(this, &QtInstance::deleteObjectLaterSignal, this, + [](QObject* pObject) { QtInstance::deleteObjectLater(pObject); }, Qt::QueuedConnection); + + m_aUpdateStyleTimer.SetTimeout(50); + m_aUpdateStyleTimer.SetInvokeHandler(LINK(this, QtInstance, updateStyleHdl)); + + QAbstractEventDispatcher* dispatcher = QAbstractEventDispatcher::instance(qApp->thread()); + connect(dispatcher, &QAbstractEventDispatcher::awake, this, [this]() { m_bSleeping = false; }); + connect(dispatcher, &QAbstractEventDispatcher::aboutToBlock, this, + [this]() { m_bSleeping = true; }); + + connect(QGuiApplication::inputMethod(), &QInputMethod::localeChanged, this, + &QtInstance::localeChanged); + + for (const QScreen* pCurScreen : QApplication::screens()) + connectQScreenSignals(pCurScreen); + connect(qApp, &QGuiApplication::primaryScreenChanged, this, &QtInstance::primaryScreenChanged); + connect(qApp, &QGuiApplication::screenAdded, this, &QtInstance::screenAdded); + connect(qApp, &QGuiApplication::screenRemoved, this, &QtInstance::screenRemoved); + +#ifndef EMSCRIPTEN + m_bSupportsOpenGL = true; +#else + ImplGetSVData()->maAppData.m_bUseSystemLoop = true; +#endif +} + +QtInstance::~QtInstance() +{ + // force freeing the QApplication before freeing the arguments, + // as it uses references to the provided arguments! + m_pQApplication.reset(); +} + +void QtInstance::AfterAppInit() +{ + // set the default application icon via desktop file just on Wayland, + // as this otherwise overrides the individual desktop icons on X11. + if (QGuiApplication::platformName() == "wayland") + QGuiApplication::setDesktopFileName(QStringLiteral("libreoffice-startcenter.desktop")); + QGuiApplication::setLayoutDirection(AllSettings::GetLayoutRTL() ? Qt::RightToLeft + : Qt::LeftToRight); +} + +void QtInstance::localeChanged() +{ + SolarMutexGuard aGuard; + const vcl::Window* pFocusWindow = Application::GetFocusWindow(); + SalFrame* const pFocusFrame = pFocusWindow ? pFocusWindow->ImplGetFrame() : nullptr; + if (!pFocusFrame) + return; + + const LanguageTag aTag( + toOUString(QGuiApplication::inputMethod()->locale().name().replace("_", "-"))); + static_cast<QtFrame*>(pFocusFrame)->setInputLanguage(aTag.getLanguageType()); +} + +void QtInstance::deleteObjectLater(QObject* pObject) { pObject->deleteLater(); } + +SalFrame* QtInstance::CreateChildFrame(SystemParentData* /*pParent*/, SalFrameStyleFlags nStyle) +{ + SalFrame* pRet(nullptr); + RunInMainThread([&, this]() { pRet = new QtFrame(nullptr, nStyle, useCairo()); }); + assert(pRet); + return pRet; +} + +SalFrame* QtInstance::CreateFrame(SalFrame* pParent, SalFrameStyleFlags nStyle) +{ + assert(!pParent || dynamic_cast<QtFrame*>(pParent)); + + SalFrame* pRet(nullptr); + RunInMainThread( + [&, this]() { pRet = new QtFrame(static_cast<QtFrame*>(pParent), nStyle, useCairo()); }); + assert(pRet); + return pRet; +} + +void QtInstance::DestroyFrame(SalFrame* pFrame) +{ + if (pFrame) + { + assert(dynamic_cast<QtFrame*>(pFrame)); + Q_EMIT deleteObjectLaterSignal(static_cast<QtFrame*>(pFrame)); + } +} + +SalObject* QtInstance::CreateObject(SalFrame* pParent, SystemWindowData*, bool bShow) +{ + assert(!pParent || dynamic_cast<QtFrame*>(pParent)); + + SalObject* pRet(nullptr); + RunInMainThread([&]() { pRet = new QtObject(static_cast<QtFrame*>(pParent), bShow); }); + assert(pRet); + return pRet; +} + +void QtInstance::DestroyObject(SalObject* pObject) +{ + if (pObject) + { + assert(dynamic_cast<QtObject*>(pObject)); + Q_EMIT deleteObjectLaterSignal(static_cast<QtObject*>(pObject)); + } +} + +std::unique_ptr<SalVirtualDevice> +QtInstance::CreateVirtualDevice(SalGraphics& rGraphics, tools::Long& nDX, tools::Long& nDY, + DeviceFormat /*eFormat*/, const SystemGraphicsData* pGd) +{ + if (m_bUseCairo) + { + SvpSalGraphics* pSvpSalGraphics = dynamic_cast<QtSvpGraphics*>(&rGraphics); + assert(pSvpSalGraphics); + // tdf#127529 see SvpSalInstance::CreateVirtualDevice for the rare case of a non-null pPreExistingTarget + cairo_surface_t* pPreExistingTarget + = pGd ? static_cast<cairo_surface_t*>(pGd->pSurface) : nullptr; + std::unique_ptr<SalVirtualDevice> pVD( + new QtSvpVirtualDevice(pSvpSalGraphics->getSurface(), pPreExistingTarget)); + pVD->SetSize(nDX, nDY); + return pVD; + } + else + { + std::unique_ptr<SalVirtualDevice> pVD(new QtVirtualDevice(/*scale*/ 1)); + pVD->SetSize(nDX, nDY); + return pVD; + } +} + +std::unique_ptr<SalMenu> QtInstance::CreateMenu(bool bMenuBar, Menu* pVCLMenu) +{ + SolarMutexGuard aGuard; + std::unique_ptr<SalMenu> pRet; + RunInMainThread([&pRet, bMenuBar, pVCLMenu]() { + QtMenu* pSalMenu = new QtMenu(bMenuBar); + pRet.reset(pSalMenu); + pSalMenu->SetMenu(pVCLMenu); + }); + assert(pRet); + return pRet; +} + +std::unique_ptr<SalMenuItem> QtInstance::CreateMenuItem(const SalItemParams& rItemData) +{ + return std::unique_ptr<SalMenuItem>(new QtMenuItem(&rItemData)); +} + +SalTimer* QtInstance::CreateSalTimer() +{ + m_pTimer = new QtTimer(); + return m_pTimer; +} + +SalSystem* QtInstance::CreateSalSystem() { return new QtSystem; } + +std::shared_ptr<SalBitmap> QtInstance::CreateSalBitmap() +{ + if (m_bUseCairo) + return std::make_shared<SvpSalBitmap>(); + else + return std::make_shared<QtBitmap>(); +} + +bool QtInstance::ImplYield(bool bWait, bool bHandleAllCurrentEvents) +{ + // Re-acquire the guard for user events when called via Q_EMIT ImplYieldSignal + SolarMutexGuard aGuard; + bool wasEvent = DispatchUserEvents(bHandleAllCurrentEvents); + if (!bHandleAllCurrentEvents && wasEvent) + return true; + + /** + * Quoting the Qt docs: [QAbstractEventDispatcher::processEvents] processes + * pending events that match flags until there are no more events to process. + */ + SolarMutexReleaser aReleaser; + QAbstractEventDispatcher* dispatcher = QAbstractEventDispatcher::instance(qApp->thread()); + if (bWait && !wasEvent) + wasEvent = dispatcher->processEvents(QEventLoop::WaitForMoreEvents); + else + wasEvent = dispatcher->processEvents(QEventLoop::AllEvents) || wasEvent; + return wasEvent; +} + +bool QtInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents) +{ + bool bWasEvent = false; + if (qApp->thread() == QThread::currentThread()) + { + bWasEvent = ImplYield(bWait, bHandleAllCurrentEvents); + if (bWasEvent) + m_aWaitingYieldCond.set(); + } + else + { + { + SolarMutexReleaser aReleaser; + bWasEvent = Q_EMIT ImplYieldSignal(false, bHandleAllCurrentEvents); + } + if (!bWasEvent && bWait) + { + m_aWaitingYieldCond.reset(); + SolarMutexReleaser aReleaser; + m_aWaitingYieldCond.wait(); + bWasEvent = true; + } + } + return bWasEvent; +} + +bool QtInstance::AnyInput(VclInputFlags nType) +{ + bool bResult = false; + if (nType & VclInputFlags::TIMER) + bResult |= (m_pTimer && m_pTimer->remainingTime() == 0); + if (nType & VclInputFlags::OTHER) + bResult |= !m_bSleeping; + return bResult; +} + +OUString QtInstance::GetConnectionIdentifier() { return OUString(); } + +void QtInstance::AddToRecentDocumentList(const OUString&, const OUString&, const OUString&) {} + +#ifndef EMSCRIPTEN +OpenGLContext* QtInstance::CreateOpenGLContext() { return new QtOpenGLContext; } +#endif + +bool QtInstance::IsMainThread() const +{ + return !qApp || (qApp->thread() == QThread::currentThread()); +} + +void QtInstance::TriggerUserEventProcessing() +{ + QAbstractEventDispatcher* dispatcher = QAbstractEventDispatcher::instance(qApp->thread()); + dispatcher->wakeUp(); +} + +void QtInstance::ProcessEvent(SalUserEvent aEvent) +{ + aEvent.m_pFrame->CallCallback(aEvent.m_nEvent, aEvent.m_pData); +} + +rtl::Reference<QtFilePicker> +QtInstance::createPicker(css::uno::Reference<css::uno::XComponentContext> const& context, + QFileDialog::FileMode eMode) +{ + if (!IsMainThread()) + { + SolarMutexGuard g; + rtl::Reference<QtFilePicker> pPicker; + RunInMainThread([&, this]() { pPicker = createPicker(context, eMode); }); + assert(pPicker); + return pPicker; + } + + return new QtFilePicker(context, eMode); +} + +css::uno::Reference<css::ui::dialogs::XFilePicker2> +QtInstance::createFilePicker(const css::uno::Reference<css::uno::XComponentContext>& context) +{ + return css::uno::Reference<css::ui::dialogs::XFilePicker2>( + createPicker(context, QFileDialog::ExistingFile)); +} + +css::uno::Reference<css::ui::dialogs::XFolderPicker2> +QtInstance::createFolderPicker(const css::uno::Reference<css::uno::XComponentContext>& context) +{ + return css::uno::Reference<css::ui::dialogs::XFolderPicker2>( + createPicker(context, QFileDialog::Directory)); +} + +css::uno::Reference<css::uno::XInterface> +QtInstance::CreateClipboard(const css::uno::Sequence<css::uno::Any>& arguments) +{ + OUString sel; + if (arguments.getLength() == 0) + { + sel = "CLIPBOARD"; + } + else if (arguments.getLength() != 1 || !(arguments[0] >>= sel)) + { + throw css::lang::IllegalArgumentException("bad QtInstance::CreateClipboard arguments", + css::uno::Reference<css::uno::XInterface>(), -1); + } + + // This could also use RunInMain, but SolarMutexGuard is enough + // since at this point we're not accessing the clipboard, just get the + // accessor to the clipboard. + SolarMutexGuard aGuard; + + auto it = m_aClipboards.find(sel); + if (it != m_aClipboards.end()) + return it->second; + + css::uno::Reference<css::uno::XInterface> xClipboard = QtClipboard::create(sel); + if (xClipboard.is()) + m_aClipboards[sel] = xClipboard; + + return xClipboard; +} + +css::uno::Reference<css::uno::XInterface> +QtInstance::ImplCreateDragSource(const SystemEnvData* pSysEnv) +{ + return vcl::X11DnDHelper(new QtDragSource(), pSysEnv->aShellWindow); +} + +css::uno::Reference<css::uno::XInterface> +QtInstance::ImplCreateDropTarget(const SystemEnvData* pSysEnv) +{ + return vcl::X11DnDHelper(new QtDropTarget(), pSysEnv->aShellWindow); +} + +IMPL_LINK_NOARG(QtInstance, updateStyleHdl, Timer*, void) +{ + SolarMutexGuard aGuard; + SalFrame* pFrame = anyFrame(); + if (pFrame) + { + pFrame->CallCallback(SalEvent::SettingsChanged, nullptr); + if (m_bUpdateFonts) + { + pFrame->CallCallback(SalEvent::FontChanged, nullptr); + m_bUpdateFonts = false; + } + } +} + +void QtInstance::UpdateStyle(bool bFontsChanged) +{ + if (bFontsChanged) + m_bUpdateFonts = true; + if (!m_aUpdateStyleTimer.IsActive()) + m_aUpdateStyleTimer.Start(); +} + +void* QtInstance::CreateGStreamerSink(const SystemChildWindow* pWindow) +{ +// As of 2021-09, qt-gstreamer is unmaintained and there is no Qt 6 video sink +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && ENABLE_GSTREAMER_1_0 && QT5_HAVE_GOBJECT + auto pSymbol = gstElementFactoryNameSymbol(); + if (!pSymbol) + return nullptr; + + const SystemEnvData* pEnvData = pWindow->GetSystemData(); + if (!pEnvData) + return nullptr; + + if (pEnvData->platform != SystemEnvData::Platform::Wayland) + return nullptr; + + GstElement* pVideosink = pSymbol("qwidget5videosink", "qwidget5videosink"); + if (pVideosink) + { + QWidget* pQWidget = static_cast<QWidget*>(pEnvData->pWidget); + g_object_set(G_OBJECT(pVideosink), "widget", pQWidget, nullptr); + } + else + { + SAL_WARN("vcl.qt", "Couldn't initialize qwidget5videosink." + " Video playback might not work as expected." + " Please install Qt5 packages for QtGStreamer."); + // with no videosink explicitly set, GStreamer will open its own (misplaced) window(s) to display video + } + + return pVideosink; +#else + Q_UNUSED(pWindow); + return nullptr; +#endif +} + +void QtInstance::connectQScreenSignals(const QScreen* pScreen) +{ + connect(pScreen, &QScreen::orientationChanged, this, &QtInstance::orientationChanged); + connect(pScreen, &QScreen::virtualGeometryChanged, this, &QtInstance::virtualGeometryChanged); +} + +void QtInstance::notifyDisplayChanged() +{ + SolarMutexGuard aGuard; + SalFrame* pAnyFrame = anyFrame(); + if (pAnyFrame) + pAnyFrame->CallCallback(SalEvent::DisplayChanged, nullptr); +} + +void QtInstance::orientationChanged(Qt::ScreenOrientation) { notifyDisplayChanged(); } + +void QtInstance::primaryScreenChanged(QScreen*) { notifyDisplayChanged(); } + +void QtInstance::screenAdded(QScreen* pScreen) +{ + connectQScreenSignals(pScreen); + if (QApplication::screens().size() == 1) + notifyDisplayChanged(); +} + +void QtInstance::screenRemoved(QScreen*) { notifyDisplayChanged(); } + +void QtInstance::virtualGeometryChanged(const QRect&) { notifyDisplayChanged(); } + +void QtInstance::AllocFakeCmdlineArgs(std::unique_ptr<char* []>& rFakeArgv, + std::unique_ptr<int>& rFakeArgc, + std::vector<FreeableCStr>& rFakeArgvFreeable) +{ + OString aVersion(qVersion()); + SAL_INFO("vcl.qt", "qt version string is " << aVersion); + + const sal_uInt32 nParams = osl_getCommandArgCount(); + sal_uInt32 nDisplayValueIdx = 0; + OUString aParam, aBin; + + for (sal_uInt32 nIdx = 0; nIdx < nParams; ++nIdx) + { + osl_getCommandArg(nIdx, &aParam.pData); + if (aParam != "-display") + continue; + ++nIdx; + nDisplayValueIdx = nIdx; + } + + osl_getExecutableFile(&aParam.pData); + osl_getSystemPathFromFileURL(aParam.pData, &aBin.pData); + OString aExec = OUStringToOString(aBin, osl_getThreadTextEncoding()); + + std::vector<FreeableCStr> aFakeArgvFreeable; + aFakeArgvFreeable.reserve(4); + aFakeArgvFreeable.emplace_back(strdup(aExec.getStr())); + aFakeArgvFreeable.emplace_back(strdup("--nocrashhandler")); + if (nDisplayValueIdx) + { + aFakeArgvFreeable.emplace_back(strdup("-display")); + osl_getCommandArg(nDisplayValueIdx, &aParam.pData); + OString aDisplay = OUStringToOString(aParam, osl_getThreadTextEncoding()); + aFakeArgvFreeable.emplace_back(strdup(aDisplay.getStr())); + } + rFakeArgvFreeable.swap(aFakeArgvFreeable); + + const int nFakeArgc = rFakeArgvFreeable.size(); + rFakeArgv.reset(new char*[nFakeArgc]); + for (int i = 0; i < nFakeArgc; i++) + rFakeArgv[i] = rFakeArgvFreeable[i].get(); + + rFakeArgc.reset(new int); + *rFakeArgc = nFakeArgc; +} + +void QtInstance::MoveFakeCmdlineArgs(std::unique_ptr<char* []>& rFakeArgv, + std::unique_ptr<int>& rFakeArgc, + std::vector<FreeableCStr>& rFakeArgvFreeable) +{ + m_pFakeArgv = std::move(rFakeArgv); + m_pFakeArgc = std::move(rFakeArgc); + m_pFakeArgvFreeable.swap(rFakeArgvFreeable); +} + +std::unique_ptr<QApplication> QtInstance::CreateQApplication(int& nArgc, char** pArgv) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + // for Qt 6, setting Qt::AA_EnableHighDpiScaling and Qt::AA_UseHighDpiPixmaps + // is deprecated, they're always enabled + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + // for scaled icons in the native menus + QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); +#endif + + FreeableCStr session_manager; + if (getenv("SESSION_MANAGER") != nullptr) + { + session_manager.reset(strdup(getenv("SESSION_MANAGER"))); + unsetenv("SESSION_MANAGER"); + } + + std::unique_ptr<QApplication> pQApp = std::make_unique<QApplication>(nArgc, pArgv); + + if (session_manager != nullptr) + { + // coverity[tainted_string] - trusted source for setenv + setenv("SESSION_MANAGER", session_manager.get(), 1); + } + + QApplication::setQuitOnLastWindowClosed(false); + return pQApp; +} + +bool QtInstance::DoExecute(int& nExitCode) +{ + const bool bIsOnSystemEventLoop = Application::IsOnSystemEventLoop(); + if (bIsOnSystemEventLoop) + nExitCode = QApplication::exec(); + return bIsOnSystemEventLoop; +} + +void QtInstance::DoQuit() +{ + if (Application::IsOnSystemEventLoop()) + QApplication::quit(); +} + +void QtInstance::setActivePopup(QtFrame* pFrame) +{ + assert(!pFrame || pFrame->isPopup()); + m_pActivePopup = pFrame; +} + +extern "C" { +VCLPLUG_QT_PUBLIC SalInstance* create_SalInstance() +{ + std::unique_ptr<char* []> pFakeArgv; + std::unique_ptr<int> pFakeArgc; + std::vector<FreeableCStr> aFakeArgvFreeable; + QtInstance::AllocFakeCmdlineArgs(pFakeArgv, pFakeArgc, aFakeArgvFreeable); + + std::unique_ptr<QApplication> pQApp + = QtInstance::CreateQApplication(*pFakeArgc, pFakeArgv.get()); + + QtInstance* pInstance = new QtInstance(pQApp); + pInstance->MoveFakeCmdlineArgs(pFakeArgv, pFakeArgc, aFakeArgvFreeable); + + new QtData(); + + return pInstance; +} +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtInstance_Print.cxx b/vcl/qt5/QtInstance_Print.cxx new file mode 100644 index 0000000000..e6396099db --- /dev/null +++ b/vcl/qt5/QtInstance_Print.cxx @@ -0,0 +1,139 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <string_view> + +#include <QtInstance.hxx> +#include <QtPrinter.hxx> + +#include <vcl/svapp.hxx> +#include <vcl/timer.hxx> +#include <vcl/QueueInfo.hxx> +#include <printerinfomanager.hxx> + +#include <jobset.h> +#include <print.h> +#include <salptype.hxx> + +#include <unx/genpspgraphics.h> + +using namespace psp; + +/* + * static helpers + */ + +static OUString getPdfDir(const PrinterInfo& rInfo) +{ + OUString aDir; + sal_Int32 nIndex = 0; + while (nIndex != -1) + { + OUString aToken(rInfo.m_aFeatures.getToken(0, ',', nIndex)); + if (aToken.startsWith("pdf=")) + { + sal_Int32 nPos = 0; + aDir = aToken.getToken(1, '=', nPos); + if (aDir.isEmpty()) + if (auto const env = getenv("HOME")) + { + aDir = OStringToOUString(std::string_view(env), osl_getThreadTextEncoding()); + } + break; + } + } + return aDir; +} + +SalInfoPrinter* QtInstance::CreateInfoPrinter(SalPrinterQueueInfo* pQueueInfo, + ImplJobSetup* pJobSetup) +{ + // create and initialize SalInfoPrinter + PspSalInfoPrinter* pPrinter = new PspSalInfoPrinter; + configurePspInfoPrinter(pPrinter, pQueueInfo, pJobSetup); + + return pPrinter; +} + +void QtInstance::DestroyInfoPrinter(SalInfoPrinter* pPrinter) { delete pPrinter; } + +std::unique_ptr<SalPrinter> QtInstance::CreatePrinter(SalInfoPrinter* pInfoPrinter) +{ + // create and initialize SalPrinter + QtPrinter* pPrinter = new QtPrinter(pInfoPrinter); + pPrinter->m_aJobData = static_cast<PspSalInfoPrinter*>(pInfoPrinter)->m_aJobData; + + return std::unique_ptr<SalPrinter>(pPrinter); +} + +void QtInstance::GetPrinterQueueInfo(ImplPrnQueueList* pList) +{ + PrinterInfoManager& rManager(PrinterInfoManager::get()); + static const char* pNoSyncDetection = getenv("SAL_DISABLE_SYNCHRONOUS_PRINTER_DETECTION"); + if (!pNoSyncDetection || !*pNoSyncDetection) + { + // #i62663# synchronize possible asynchronouse printer detection now + rManager.checkPrintersChanged(true); + } + ::std::vector<OUString> aPrinters; + rManager.listPrinters(aPrinters); + + for (const auto& rPrinter : aPrinters) + { + const PrinterInfo& rInfo(rManager.getPrinterInfo(rPrinter)); + // create new entry + std::unique_ptr<SalPrinterQueueInfo> pInfo(new SalPrinterQueueInfo); + pInfo->maPrinterName = rPrinter; + pInfo->maDriver = rInfo.m_aDriverName; + pInfo->maLocation = rInfo.m_aLocation; + pInfo->maComment = rInfo.m_aComment; + + sal_Int32 nIndex = 0; + while (nIndex != -1) + { + OUString aToken(rInfo.m_aFeatures.getToken(0, ',', nIndex)); + if (aToken.startsWith("pdf=")) + { + pInfo->maLocation = getPdfDir(rInfo); + break; + } + } + + pList->Add(std::move(pInfo)); + } +} + +void QtInstance::GetPrinterQueueState(SalPrinterQueueInfo*) {} + +OUString QtInstance::GetDefaultPrinter() +{ + PrinterInfoManager& rManager(PrinterInfoManager::get()); + return rManager.getDefaultPrinter(); +} + +void QtInstance::PostPrintersChanged() {} + +std::unique_ptr<GenPspGraphics> QtInstance::CreatePrintGraphics() +{ + return std::make_unique<GenPspGraphics>(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtMainWindow.cxx b/vcl/qt5/QtMainWindow.cxx new file mode 100644 index 0000000000..5ff9ac9a81 --- /dev/null +++ b/vcl/qt5/QtMainWindow.cxx @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + +#include <QtMainWindow.hxx> +#include <QtMainWindow.moc> +#include <QtAccessibleWidget.hxx> + +#include <QtGui/QAccessible> +#include <QtGui/QCloseEvent> + +QtMainWindow::QtMainWindow(QtFrame& rFrame, Qt::WindowFlags f) + : QMainWindow(nullptr, f) + , m_rFrame(rFrame) +{ +#ifndef EMSCRIPTEN + QAccessible::installFactory(QtAccessibleWidget::customFactory); +#endif +} + +void QtMainWindow::closeEvent(QCloseEvent* pEvent) +{ + bool bRet = false; + bRet = m_rFrame.CallCallback(SalEvent::Close, nullptr); + + if (bRet) + pEvent->accept(); + // SalEvent::Close returning false may mean that user has vetoed + // closing the frame ("you have unsaved changes" dialog for example) + // We shouldn't process the event in such case + else + pEvent->ignore(); +} + +void QtMainWindow::moveEvent(QMoveEvent* pEvent) +{ + const qreal fRatio = m_rFrame.devicePixelRatioF(); + m_rFrame.maGeometry.setPos(toPoint(pEvent->pos() * fRatio)); + m_rFrame.CallCallback(SalEvent::Move, nullptr); +} diff --git a/vcl/qt5/QtMenu.cxx b/vcl/qt5/QtMenu.cxx new file mode 100644 index 0000000000..93f3d6f5a3 --- /dev/null +++ b/vcl/qt5/QtMenu.cxx @@ -0,0 +1,920 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <QtMenu.hxx> +#include <QtMenu.moc> + +#include <QtFrame.hxx> +#include <QtInstance.hxx> +#include <QtMainWindow.hxx> + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include <QtWidgets/QActionGroup> +#else +#include <QtGui/QActionGroup> +#endif + +#include <QtWidgets/QButtonGroup> +#include <QtWidgets/QHBoxLayout> +#include <QtWidgets/QMenuBar> +#include <QtWidgets/QPushButton> +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#include <QtGui/QShortcut> +#else +#include <QtWidgets/QShortcut> +#endif +#include <QtWidgets/QStyle> + +#include <o3tl/safeint.hxx> +#include <vcl/svapp.hxx> +#include <sal/log.hxx> + +#include <strings.hrc> +#include <bitmaps.hlst> + +#include <vcl/toolkit/floatwin.hxx> +#include <window.h> + +// LO SalMenuButtonItem::mnId is sal_uInt16, so we go with -2, as -1 has a special meaning as automatic id +constexpr int CLOSE_BUTTON_ID = -2; +const QString gButtonGroupKey("QtMenu::ButtonGroup"); + +static inline void lcl_force_menubar_layout_update(QMenuBar& rMenuBar) +{ + // just exists as a function to not comment it everywhere: forces reposition of the + // corner widget after its layout changes, which will otherwise just happen on resize. + // it unfortunatly has additional side effects; see QtMenu::GetMenuBarButtonRectPixel. + rMenuBar.adjustSize(); +} + +OUString QtMenu::m_sCurrentHelpId = u""_ustr; + +QtMenu::QtMenu(bool bMenuBar) + : mpVCLMenu(nullptr) + , mpParentSalMenu(nullptr) + , mpFrame(nullptr) + , mbMenuBar(bMenuBar) + , mpQMenuBar(nullptr) + , mpQMenu(nullptr) + , m_pButtonGroup(nullptr) +{ +} + +bool QtMenu::VisibleMenuBar() { return true; } + +void QtMenu::InsertMenuItem(QtMenuItem* pSalMenuItem, unsigned nPos) +{ + sal_uInt16 nId = pSalMenuItem->mnId; + OUString aText = mpVCLMenu->GetItemText(nId); + NativeItemText(aText); + vcl::KeyCode nAccelKey = mpVCLMenu->GetAccelKey(nId); + + pSalMenuItem->mpAction.reset(); + pSalMenuItem->mpMenu.reset(); + + if (mbMenuBar) + { + // top-level menu + if (validateQMenuBar()) + { + QMenu* pQMenu = new QMenu(toQString(aText), nullptr); + connectHelpSignalSlots(pQMenu, pSalMenuItem); + pSalMenuItem->mpMenu.reset(pQMenu); + + if ((nPos != MENU_APPEND) + && (static_cast<size_t>(nPos) < o3tl::make_unsigned(mpQMenuBar->actions().size()))) + { + mpQMenuBar->insertMenu(mpQMenuBar->actions()[nPos], pQMenu); + } + else + { + mpQMenuBar->addMenu(pQMenu); + } + + // correct parent menu for generated menu + if (pSalMenuItem->mpSubMenu) + { + pSalMenuItem->mpSubMenu->mpQMenu = pQMenu; + } + + connect(pQMenu, &QMenu::aboutToShow, this, + [pSalMenuItem] { slotMenuAboutToShow(pSalMenuItem); }); + connect(pQMenu, &QMenu::aboutToHide, this, + [pSalMenuItem] { slotMenuAboutToHide(pSalMenuItem); }); + } + } + else + { + if (!mpQMenu) + { + // no QMenu set, instantiate own one + mpOwnedQMenu.reset(new QMenu); + mpQMenu = mpOwnedQMenu.get(); + connectHelpSignalSlots(mpQMenu, pSalMenuItem); + } + + if (pSalMenuItem->mpSubMenu) + { + // submenu + QMenu* pQMenu = new QMenu(toQString(aText), nullptr); + connectHelpSignalSlots(pQMenu, pSalMenuItem); + pSalMenuItem->mpMenu.reset(pQMenu); + + if ((nPos != MENU_APPEND) + && (static_cast<size_t>(nPos) < o3tl::make_unsigned(mpQMenu->actions().size()))) + { + mpQMenu->insertMenu(mpQMenu->actions()[nPos], pQMenu); + } + else + { + mpQMenu->addMenu(pQMenu); + } + + // correct parent menu for generated menu + pSalMenuItem->mpSubMenu->mpQMenu = pQMenu; + + ReinitializeActionGroup(nPos); + + // clear all action groups since menu is recreated + pSalMenuItem->mpSubMenu->ResetAllActionGroups(); + + connect(pQMenu, &QMenu::aboutToShow, this, + [pSalMenuItem] { slotMenuAboutToShow(pSalMenuItem); }); + connect(pQMenu, &QMenu::aboutToHide, this, + [pSalMenuItem] { slotMenuAboutToHide(pSalMenuItem); }); + } + else + { + if (pSalMenuItem->mnType == MenuItemType::SEPARATOR) + { + QAction* pAction = new QAction(nullptr); + pSalMenuItem->mpAction.reset(pAction); + pAction->setSeparator(true); + + if ((nPos != MENU_APPEND) + && (static_cast<size_t>(nPos) < o3tl::make_unsigned(mpQMenu->actions().size()))) + { + mpQMenu->insertAction(mpQMenu->actions()[nPos], pAction); + } + else + { + mpQMenu->addAction(pAction); + } + + ReinitializeActionGroup(nPos); + } + else + { + // leaf menu + QAction* pAction = new QAction(toQString(aText), nullptr); + pSalMenuItem->mpAction.reset(pAction); + + if ((nPos != MENU_APPEND) + && (static_cast<size_t>(nPos) < o3tl::make_unsigned(mpQMenu->actions().size()))) + { + mpQMenu->insertAction(mpQMenu->actions()[nPos], pAction); + } + else + { + mpQMenu->addAction(pAction); + } + + ReinitializeActionGroup(nPos); + + UpdateActionGroupItem(pSalMenuItem); + + pAction->setShortcut(toQString(nAccelKey.GetName())); + + connect(pAction, &QAction::triggered, this, + [pSalMenuItem] { slotMenuTriggered(pSalMenuItem); }); + connect(pAction, &QAction::hovered, this, + [pSalMenuItem] { slotMenuHovered(pSalMenuItem); }); + } + } + } + + QAction* pAction = pSalMenuItem->getAction(); + if (pAction) + { + pAction->setEnabled(pSalMenuItem->mbEnabled); + pAction->setVisible(pSalMenuItem->mbVisible); + } +} + +void QtMenu::ReinitializeActionGroup(unsigned nPos) +{ + const unsigned nCount = GetItemCount(); + + if (nCount == 0) + { + return; + } + + if (nPos == MENU_APPEND) + { + nPos = nCount - 1; + } + else if (nPos >= nCount) + { + return; + } + + QtMenuItem* pPrevItem = (nPos > 0) ? GetItemAtPos(nPos - 1) : nullptr; + QtMenuItem* pCurrentItem = GetItemAtPos(nPos); + QtMenuItem* pNextItem = (nPos < nCount - 1) ? GetItemAtPos(nPos + 1) : nullptr; + + if (pCurrentItem->mnType == MenuItemType::SEPARATOR) + { + pCurrentItem->mpActionGroup.reset(); + + // if it's inserted into middle of existing group, split it into two groups: + // first goes original group, after separator goes new group + if (pPrevItem && pPrevItem->mpActionGroup && pNextItem && pNextItem->mpActionGroup + && (pPrevItem->mpActionGroup == pNextItem->mpActionGroup)) + { + std::shared_ptr<QActionGroup> pFirstActionGroup = pPrevItem->mpActionGroup; + auto pSecondActionGroup = std::make_shared<QActionGroup>(nullptr); + pSecondActionGroup->setExclusive(true); + + auto actions = pFirstActionGroup->actions(); + + for (unsigned idx = nPos + 1; idx < nCount; ++idx) + { + QtMenuItem* pModifiedItem = GetItemAtPos(idx); + + if ((!pModifiedItem) || (!pModifiedItem->mpActionGroup)) + { + break; + } + + pModifiedItem->mpActionGroup = pSecondActionGroup; + auto action = pModifiedItem->getAction(); + + if (actions.contains(action)) + { + pFirstActionGroup->removeAction(action); + pSecondActionGroup->addAction(action); + } + } + } + } + else + { + if (!pCurrentItem->mpActionGroup) + { + // unless element is inserted between two separators, or a separator and an end of vector, use neighbouring group since it's shared + if (pPrevItem && pPrevItem->mpActionGroup) + { + pCurrentItem->mpActionGroup = pPrevItem->mpActionGroup; + } + else if (pNextItem && pNextItem->mpActionGroup) + { + pCurrentItem->mpActionGroup = pNextItem->mpActionGroup; + } + else + { + pCurrentItem->mpActionGroup = std::make_shared<QActionGroup>(nullptr); + pCurrentItem->mpActionGroup->setExclusive(true); + } + } + + // if there's also a different group after this element, merge it + if (pNextItem && pNextItem->mpActionGroup + && (pCurrentItem->mpActionGroup != pNextItem->mpActionGroup)) + { + auto pFirstCheckedAction = pCurrentItem->mpActionGroup->checkedAction(); + auto pSecondCheckedAction = pNextItem->mpActionGroup->checkedAction(); + auto actions = pNextItem->mpActionGroup->actions(); + + // first move all actions from second group to first one, and if first group already has checked action, + // and second group also has a checked action, uncheck action from second group + for (auto action : actions) + { + pNextItem->mpActionGroup->removeAction(action); + + if (pFirstCheckedAction && pSecondCheckedAction && (action == pSecondCheckedAction)) + { + action->setChecked(false); + } + + pCurrentItem->mpActionGroup->addAction(action); + } + + // now replace all pointers to second group with pointers to first group + for (unsigned idx = nPos + 1; idx < nCount; ++idx) + { + QtMenuItem* pModifiedItem = GetItemAtPos(idx); + + if ((!pModifiedItem) || (!pModifiedItem->mpActionGroup)) + { + break; + } + + pModifiedItem->mpActionGroup = pCurrentItem->mpActionGroup; + } + } + } +} + +void QtMenu::ResetAllActionGroups() +{ + for (unsigned nItem = 0; nItem < GetItemCount(); ++nItem) + { + QtMenuItem* pSalMenuItem = GetItemAtPos(nItem); + pSalMenuItem->mpActionGroup.reset(); + } +} + +void QtMenu::UpdateActionGroupItem(const QtMenuItem* pSalMenuItem) +{ + QAction* pAction = pSalMenuItem->getAction(); + if (!pAction) + return; + + bool bChecked = mpVCLMenu->IsItemChecked(pSalMenuItem->mnId); + MenuItemBits itemBits = mpVCLMenu->GetItemBits(pSalMenuItem->mnId); + + if (itemBits & MenuItemBits::RADIOCHECK) + { + pAction->setCheckable(true); + + if (pSalMenuItem->mpActionGroup) + { + pSalMenuItem->mpActionGroup->addAction(pAction); + } + + pAction->setChecked(bChecked); + } + else + { + pAction->setActionGroup(nullptr); + + if (itemBits & MenuItemBits::CHECKABLE) + { + pAction->setCheckable(true); + pAction->setChecked(bChecked); + } + else + { + pAction->setChecked(false); + pAction->setCheckable(false); + } + } +} + +void QtMenu::InsertItem(SalMenuItem* pSalMenuItem, unsigned nPos) +{ + SolarMutexGuard aGuard; + QtMenuItem* pItem = static_cast<QtMenuItem*>(pSalMenuItem); + + if (nPos == MENU_APPEND) + maItems.push_back(pItem); + else + maItems.insert(maItems.begin() + nPos, pItem); + + pItem->mpParentMenu = this; + + InsertMenuItem(pItem, nPos); +} + +void QtMenu::RemoveItem(unsigned nPos) +{ + SolarMutexGuard aGuard; + + if (nPos >= maItems.size()) + return; + + QtMenuItem* pItem = maItems[nPos]; + pItem->mpAction.reset(); + pItem->mpMenu.reset(); + + maItems.erase(maItems.begin() + nPos); + + // Recalculate action groups if necessary: + // if separator between two QActionGroups was removed, + // it may be needed to merge them + if (nPos > 0) + { + ReinitializeActionGroup(nPos - 1); + } +} + +void QtMenu::SetSubMenu(SalMenuItem* pSalMenuItem, SalMenu* pSubMenu, unsigned nPos) +{ + SolarMutexGuard aGuard; + QtMenuItem* pItem = static_cast<QtMenuItem*>(pSalMenuItem); + QtMenu* pQSubMenu = static_cast<QtMenu*>(pSubMenu); + + pItem->mpSubMenu = pQSubMenu; + // at this point the pointer to parent menu may be outdated, update it too + pItem->mpParentMenu = this; + + if (pQSubMenu != nullptr) + { + pQSubMenu->mpParentSalMenu = this; + pQSubMenu->mpQMenu = pItem->mpMenu.get(); + } + + // if it's not a menu bar item, then convert it to corresponding item if type if necessary. + // If submenu is present and it's an action, convert it to menu. + // If submenu is not present and it's a menu, convert it to action. + // It may be fine to proceed in any case, but by skipping other cases + // amount of unneeded actions taken should be reduced. + if (pItem->mpParentMenu->mbMenuBar || (pQSubMenu && pItem->mpMenu) + || ((!pQSubMenu) && pItem->mpAction)) + { + return; + } + + InsertMenuItem(pItem, nPos); +} + +void QtMenu::SetFrame(const SalFrame* pFrame) +{ + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + if (!pSalInst->IsMainThread()) + { + pSalInst->RunInMainThread([this, pFrame]() { SetFrame(pFrame); }); + return; + } + + SolarMutexGuard aGuard; + assert(mbMenuBar); + mpFrame = const_cast<QtFrame*>(static_cast<const QtFrame*>(pFrame)); + + mpFrame->SetMenu(this); + + QtMainWindow* pMainWindow = mpFrame->GetTopLevelWindow(); + if (!pMainWindow) + return; + + mpQMenuBar = new QMenuBar(); + pMainWindow->setMenuBar(mpQMenuBar); + + QWidget* pWidget = mpQMenuBar->cornerWidget(Qt::TopRightCorner); + if (pWidget) + { + m_pButtonGroup = pWidget->findChild<QButtonGroup*>(gButtonGroupKey); + assert(m_pButtonGroup); + connect(m_pButtonGroup, QOverload<QAbstractButton*>::of(&QButtonGroup::buttonClicked), this, + &QtMenu::slotMenuBarButtonClicked); + QPushButton* pButton = static_cast<QPushButton*>(m_pButtonGroup->button(CLOSE_BUTTON_ID)); + if (pButton) + connect(pButton, &QPushButton::clicked, this, &QtMenu::slotCloseDocument); + } + else + m_pButtonGroup = nullptr; + mpQMenu = nullptr; + + DoFullMenuUpdate(mpVCLMenu); +} + +void QtMenu::DoFullMenuUpdate(Menu* pMenuBar) +{ + // clear action groups since menu is rebuilt + ResetAllActionGroups(); + ShowCloseButton(false); + + for (sal_Int32 nItem = 0; nItem < static_cast<sal_Int32>(GetItemCount()); nItem++) + { + QtMenuItem* pSalMenuItem = GetItemAtPos(nItem); + InsertMenuItem(pSalMenuItem, nItem); + SetItemImage(nItem, pSalMenuItem, pSalMenuItem->maImage); + const bool bShowDisabled + = bool(pMenuBar->GetMenuFlags() & MenuFlags::AlwaysShowDisabledEntries) + || !bool(pMenuBar->GetMenuFlags() & MenuFlags::HideDisabledEntries); + const bool bVisible = pSalMenuItem->mbVisible + && (bShowDisabled || mpVCLMenu->IsItemEnabled(pSalMenuItem->mnId)); + pSalMenuItem->getAction()->setVisible(bVisible); + + if (pSalMenuItem->mpSubMenu != nullptr) + { + pMenuBar->HandleMenuActivateEvent(pSalMenuItem->mpSubMenu->GetMenu()); + pSalMenuItem->mpSubMenu->DoFullMenuUpdate(pMenuBar); + pMenuBar->HandleMenuDeActivateEvent(pSalMenuItem->mpSubMenu->GetMenu()); + } + } +} + +void QtMenu::ShowItem(unsigned nPos, bool bShow) +{ + if (nPos < maItems.size()) + { + QtMenuItem* pSalMenuItem = GetItemAtPos(nPos); + QAction* pAction = pSalMenuItem->getAction(); + if (pAction) + pAction->setVisible(bShow); + pSalMenuItem->mbVisible = bShow; + } +} + +void QtMenu::SetItemBits(unsigned nPos, MenuItemBits) +{ + if (nPos < maItems.size()) + { + QtMenuItem* pSalMenuItem = GetItemAtPos(nPos); + UpdateActionGroupItem(pSalMenuItem); + } +} + +void QtMenu::CheckItem(unsigned nPos, bool bChecked) +{ + if (nPos < maItems.size()) + { + QtMenuItem* pSalMenuItem = GetItemAtPos(nPos); + QAction* pAction = pSalMenuItem->getAction(); + if (pAction) + { + pAction->setCheckable(true); + pAction->setChecked(bChecked); + } + } +} + +void QtMenu::EnableItem(unsigned nPos, bool bEnable) +{ + if (nPos < maItems.size()) + { + QtMenuItem* pSalMenuItem = GetItemAtPos(nPos); + QAction* pAction = pSalMenuItem->getAction(); + if (pAction) + pAction->setEnabled(bEnable); + pSalMenuItem->mbEnabled = bEnable; + } +} + +void QtMenu::SetItemText(unsigned, SalMenuItem* pItem, const OUString& rText) +{ + QtMenuItem* pSalMenuItem = static_cast<QtMenuItem*>(pItem); + QAction* pAction = pSalMenuItem->getAction(); + if (pAction) + { + OUString aText(rText); + NativeItemText(aText); + pAction->setText(toQString(aText)); + } +} + +void QtMenu::SetItemImage(unsigned, SalMenuItem* pItem, const Image& rImage) +{ + QtMenuItem* pSalMenuItem = static_cast<QtMenuItem*>(pItem); + + // Save new image to use it in DoFullMenuUpdate + pSalMenuItem->maImage = rImage; + + QAction* pAction = pSalMenuItem->getAction(); + if (!pAction) + return; + + pAction->setIcon(QPixmap::fromImage(toQImage(rImage))); +} + +void QtMenu::SetAccelerator(unsigned, SalMenuItem* pItem, const vcl::KeyCode&, + const OUString& rText) +{ + QtMenuItem* pSalMenuItem = static_cast<QtMenuItem*>(pItem); + QAction* pAction = pSalMenuItem->getAction(); + if (pAction) + pAction->setShortcut(QKeySequence(toQString(rText), QKeySequence::PortableText)); +} + +void QtMenu::GetSystemMenuData(SystemMenuData*) {} + +QtMenu* QtMenu::GetTopLevel() +{ + QtMenu* pMenu = this; + while (pMenu->mpParentSalMenu) + pMenu = pMenu->mpParentSalMenu; + return pMenu; +} + +bool QtMenu::validateQMenuBar() const +{ + if (!mpQMenuBar) + return false; + assert(mpFrame); + QtMainWindow* pMainWindow = mpFrame->GetTopLevelWindow(); + assert(pMainWindow); + const bool bValid = mpQMenuBar == pMainWindow->menuBar(); + if (!bValid) + { + QtMenu* thisPtr = const_cast<QtMenu*>(this); + thisPtr->mpQMenuBar = nullptr; + } + return bValid; +} + +void QtMenu::ShowMenuBar(bool bVisible) +{ + if (!validateQMenuBar()) + return; + + mpQMenuBar->setVisible(bVisible); + if (bVisible) + lcl_force_menubar_layout_update(*mpQMenuBar); +} + +void QtMenu::slotMenuHovered(QtMenuItem* pItem) +{ + const OUString sHelpId = pItem->mpParentMenu->GetMenu()->GetHelpId(pItem->mnId); + m_sCurrentHelpId = sHelpId; +} + +void QtMenu::slotShowHelp() +{ + SolarMutexGuard aGuard; + Help* pHelp = Application::GetHelp(); + if (pHelp && !m_sCurrentHelpId.isEmpty()) + { + pHelp->Start(m_sCurrentHelpId); + } +} + +void QtMenu::slotMenuTriggered(QtMenuItem* pQItem) +{ + if (!pQItem) + return; + + QtMenu* pSalMenu = pQItem->mpParentMenu; + QtMenu* pTopLevel = pSalMenu->GetTopLevel(); + + Menu* pMenu = pSalMenu->GetMenu(); + auto mnId = pQItem->mnId; + + // HACK to allow HandleMenuCommandEvent to "not-set" the checked button + // LO expects a signal before an item state change, so reset the check item + if (pQItem->mpAction->isCheckable() + && (!pQItem->mpActionGroup || pQItem->mpActionGroup->actions().size() <= 1)) + pQItem->mpAction->setChecked(!pQItem->mpAction->isChecked()); + pTopLevel->GetMenu()->HandleMenuCommandEvent(pMenu, mnId); +} + +void QtMenu::slotMenuAboutToShow(QtMenuItem* pQItem) +{ + if (pQItem) + { + QtMenu* pSalMenu = pQItem->mpSubMenu; + QtMenu* pTopLevel = pSalMenu->GetTopLevel(); + + Menu* pMenu = pSalMenu->GetMenu(); + + // following function may update the menu + pTopLevel->GetMenu()->HandleMenuActivateEvent(pMenu); + } +} + +void QtMenu::slotMenuAboutToHide(QtMenuItem* pQItem) +{ + if (pQItem) + { + QtMenu* pSalMenu = pQItem->mpSubMenu; + QtMenu* pTopLevel = pSalMenu->GetTopLevel(); + + Menu* pMenu = pSalMenu->GetMenu(); + + pTopLevel->GetMenu()->HandleMenuDeActivateEvent(pMenu); + } +} + +void QtMenu::NativeItemText(OUString& rItemText) +{ + // preserve literal '&'s in menu texts + rItemText = rItemText.replaceAll("&", "&&"); + + rItemText = rItemText.replace('~', '&'); +} + +void QtMenu::slotCloseDocument() +{ + MenuBar* pVclMenuBar = static_cast<MenuBar*>(mpVCLMenu.get()); + if (pVclMenuBar) + Application::PostUserEvent(pVclMenuBar->GetCloseButtonClickHdl()); +} + +void QtMenu::slotMenuBarButtonClicked(QAbstractButton* pButton) +{ + MenuBar* pVclMenuBar = static_cast<MenuBar*>(mpVCLMenu.get()); + if (pVclMenuBar) + { + SolarMutexGuard aGuard; + pVclMenuBar->HandleMenuButtonEvent(m_pButtonGroup->id(pButton)); + } +} + +QPushButton* QtMenu::ImplAddMenuBarButton(const QIcon& rIcon, const QString& rToolTip, int nId) +{ + if (!validateQMenuBar()) + return nullptr; + + QWidget* pWidget = mpQMenuBar->cornerWidget(Qt::TopRightCorner); + QHBoxLayout* pLayout; + if (!pWidget) + { + assert(!m_pButtonGroup); + pWidget = new QWidget(mpQMenuBar); + assert(!pWidget->layout()); + pLayout = new QHBoxLayout(); + pLayout->setContentsMargins(QMargins()); + pLayout->setSpacing(0); + pWidget->setLayout(pLayout); + m_pButtonGroup = new QButtonGroup(pLayout); + m_pButtonGroup->setObjectName(gButtonGroupKey); + m_pButtonGroup->setExclusive(false); + connect(m_pButtonGroup, QOverload<QAbstractButton*>::of(&QButtonGroup::buttonClicked), this, + &QtMenu::slotMenuBarButtonClicked); + pWidget->show(); + mpQMenuBar->setCornerWidget(pWidget, Qt::TopRightCorner); + } + else + pLayout = static_cast<QHBoxLayout*>(pWidget->layout()); + assert(m_pButtonGroup); + assert(pLayout); + + QPushButton* pButton = static_cast<QPushButton*>(m_pButtonGroup->button(nId)); + if (pButton) + ImplRemoveMenuBarButton(nId); + + pButton = new QPushButton(); + // we don't want the button to increase the QMenuBar height, so a fixed size square it is + const int nFixedLength + = mpQMenuBar->height() - 2 * mpQMenuBar->style()->pixelMetric(QStyle::PM_MenuBarVMargin); + pButton->setFixedSize(nFixedLength, nFixedLength); + pButton->setIcon(rIcon); + pButton->setFlat(true); + pButton->setFocusPolicy(Qt::NoFocus); + pButton->setToolTip(rToolTip); + + m_pButtonGroup->addButton(pButton, nId); + int nPos = pLayout->count(); + if (m_pButtonGroup->button(CLOSE_BUTTON_ID)) + nPos--; + pLayout->insertWidget(nPos, pButton, 0, Qt::AlignCenter); + // show must happen after adding the button to the layout, otherwise the button is + // shown, but not correct in the layout, if at all! Some times the layout ignores it. + pButton->show(); + + lcl_force_menubar_layout_update(*mpQMenuBar); + + return pButton; +} + +bool QtMenu::AddMenuBarButton(const SalMenuButtonItem& rItem) +{ + if (!validateQMenuBar()) + return false; + return !!ImplAddMenuBarButton(QIcon(QPixmap::fromImage(toQImage(rItem.maImage))), + toQString(rItem.maToolTipText), rItem.mnId); +} + +void QtMenu::ImplRemoveMenuBarButton(int nId) +{ + if (!validateQMenuBar()) + return; + + assert(m_pButtonGroup); + auto* pButton = m_pButtonGroup->button(nId); + assert(pButton); + QWidget* pWidget = mpQMenuBar->cornerWidget(Qt::TopRightCorner); + assert(pWidget); + QLayout* pLayout = pWidget->layout(); + m_pButtonGroup->removeButton(pButton); + pLayout->removeWidget(pButton); + delete pButton; + + lcl_force_menubar_layout_update(*mpQMenuBar); +} + +void QtMenu::connectHelpShortcut(QMenu* pMenu) +{ + assert(pMenu); + QKeySequence sequence(QKeySequence::HelpContents); + QShortcut* pQShortcut = new QShortcut(sequence, pMenu); + connect(pQShortcut, &QShortcut::activated, this, QtMenu::slotShowHelp); + connect(pQShortcut, &QShortcut::activatedAmbiguously, this, QtMenu::slotShowHelp); +} + +void QtMenu::connectHelpSignalSlots(QMenu* pMenu, QtMenuItem* pSalMenuItem) +{ + // connect hovered signal of the menu's own action + QAction* pAction = pMenu->menuAction(); + assert(pAction); + connect(pAction, &QAction::hovered, this, [pSalMenuItem] { slotMenuHovered(pSalMenuItem); }); + + // connect slot to handle Help key (F1) + connectHelpShortcut(pMenu); +} + +void QtMenu::RemoveMenuBarButton(sal_uInt16 nId) { ImplRemoveMenuBarButton(nId); } + +tools::Rectangle QtMenu::GetMenuBarButtonRectPixel(sal_uInt16 nId, SalFrame* pFrame) +{ +#ifdef NDEBUG + Q_UNUSED(pFrame); +#endif + if (!validateQMenuBar()) + return tools::Rectangle(); + + assert(mpFrame == static_cast<QtFrame*>(pFrame)); + assert(m_pButtonGroup); + auto* pButton = static_cast<QPushButton*>(m_pButtonGroup->button(nId)); + assert(pButton); + + // unfortunatly, calling lcl_force_menubar_layout_update results in a temporary wrong menubar size, + // but it's the correct minimal size AFAIK and the layout seems correct, so just adjust the width. + QPoint aPos = pButton->mapTo(mpFrame->asChild(), QPoint()); + aPos.rx() += (mpFrame->asChild()->width() - mpQMenuBar->width()); + return tools::Rectangle(toPoint(aPos), toSize(pButton->size())); +} + +void QtMenu::ShowCloseButton(bool bShow) +{ + if (!validateQMenuBar()) + return; + + if (!bShow && !m_pButtonGroup) + return; + + QPushButton* pButton = nullptr; + if (m_pButtonGroup) + pButton = static_cast<QPushButton*>(m_pButtonGroup->button(CLOSE_BUTTON_ID)); + if (!bShow && !pButton) + return; + + if (!pButton) + { + QIcon aIcon; + if (QIcon::hasThemeIcon("window-close-symbolic")) + aIcon = QIcon::fromTheme("window-close-symbolic"); + else + aIcon = QIcon( + QPixmap::fromImage(toQImage(Image(StockImage::Yes, SV_RESID_BITMAP_CLOSEDOC)))); + pButton = ImplAddMenuBarButton(aIcon, toQString(VclResId(SV_HELPTEXT_CLOSEDOCUMENT)), + CLOSE_BUTTON_ID); + connect(pButton, &QPushButton::clicked, this, &QtMenu::slotCloseDocument); + } + + if (bShow) + pButton->show(); + else + pButton->hide(); + + lcl_force_menubar_layout_update(*mpQMenuBar); +} + +bool QtMenu::ShowNativePopupMenu(FloatingWindow* pWin, const tools::Rectangle& rRect, + FloatWinPopupFlags nFlags) +{ + assert(mpQMenu); + DoFullMenuUpdate(mpVCLMenu); + mpQMenu->setTearOffEnabled(bool(nFlags & FloatWinPopupFlags::AllowTearOff)); + + const VclPtr<vcl::Window> xParent = pWin->ImplGetWindowImpl()->mpRealParent; + AbsoluteScreenPixelRectangle aFloatRect = FloatingWindow::ImplConvertToAbsPos(xParent, rRect); + + // tdf#154447 Menu bar height has to be added + QtFrame* pFrame = static_cast<QtFrame*>(pWin->ImplGetFrame()); + assert(pFrame); + aFloatRect.SetPosY(aFloatRect.getY() + pFrame->menuBarOffset()); + + const QRect aRect = toQRect(aFloatRect, 1 / pFrame->devicePixelRatioF()); + mpQMenu->exec(aRect.bottomLeft()); + + return true; +} + +int QtMenu::GetMenuBarHeight() const +{ + if (!validateQMenuBar() || mpQMenuBar->isHidden()) + return 0; + + return mpQMenuBar->height(); +} + +QtMenuItem::QtMenuItem(const SalItemParams* pItemData) + : mpParentMenu(nullptr) + , mpSubMenu(nullptr) + , mnId(pItemData->nId) + , mnType(pItemData->eType) + , mbVisible(true) + , mbEnabled(true) + , maImage(pItemData->aImage) +{ +} + +QAction* QtMenuItem::getAction() const +{ + if (mpMenu) + return mpMenu->menuAction(); + if (mpAction) + return mpAction.get(); + return nullptr; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtObject.cxx b/vcl/qt5/QtObject.cxx new file mode 100644 index 0000000000..fbdc8e9b62 --- /dev/null +++ b/vcl/qt5/QtObject.cxx @@ -0,0 +1,155 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtObject.hxx> +#include <QtObject.moc> + +#include <QtFrame.hxx> +#include <QtWidget.hxx> + +#include <QtGui/QGuiApplication> +#include <QtGui/QKeyEvent> +#include <QtGui/QMouseEvent> + +QtObject::QtObject(QtFrame* pParent, bool bShow) + : m_pParent(pParent) + , m_pQWidget(nullptr) + , m_bForwardKey(false) +{ + if (!m_pParent || !pParent->GetQWidget()) + return; + + m_pQWidget = new QtObjectWidget(*this); + if (bShow) + m_pQWidget->show(); + + QtFrame::FillSystemEnvData(m_aSystemData, reinterpret_cast<sal_IntPtr>(this), m_pQWidget); +} + +QtObject::~QtObject() +{ + if (m_pQWidget) + { + m_pQWidget->setParent(nullptr); + delete m_pQWidget; + } +} + +QWindow* QtObject::windowHandle() const +{ + return m_pQWidget ? m_pQWidget->windowHandle() : nullptr; +} + +void QtObject::ResetClipRegion() +{ + if (m_pQWidget) + m_pRegion = QRegion(m_pQWidget->geometry()); + else + m_pRegion = QRegion(); +} + +void QtObject::BeginSetClipRegion(sal_uInt32) { m_pRegion = QRegion(); } + +void QtObject::UnionClipRegion(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight) +{ + m_pRegion += QRect(nX, nY, nWidth, nHeight); +} + +void QtObject::EndSetClipRegion() +{ + if (m_pQWidget) + m_pRegion = m_pRegion.intersected(m_pQWidget->geometry()); +} + +void QtObject::SetPosSize(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight) +{ + if (m_pQWidget) + { + m_pQWidget->move(nX, nY); + m_pQWidget->setFixedSize(nWidth, nHeight); + } +} + +void QtObject::Show(bool bVisible) +{ + if (m_pQWidget) + m_pQWidget->setVisible(bVisible); +} + +void QtObject::SetForwardKey(bool bEnable) { m_bForwardKey = bEnable; } + +void QtObject::Reparent(SalFrame* pFrame) +{ + QtFrame* pNewParent = static_cast<QtFrame*>(pFrame); + if (m_pParent == pNewParent) + return; + m_pParent = pNewParent; + m_pQWidget->setParent(m_pParent->GetQWidget()); +} + +QtObjectWidget::QtObjectWidget(QtObject& rParent) + : QWidget(rParent.frame()->GetQWidget()) + , m_rParent(rParent) +{ + assert(m_rParent.frame() && m_rParent.frame()->GetQWidget()); + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_OpaquePaintEvent); +} + +void QtObjectWidget::focusInEvent(QFocusEvent*) +{ + SolarMutexGuard aGuard; + m_rParent.CallCallback(SalObjEvent::GetFocus); +} + +void QtObjectWidget::focusOutEvent(QFocusEvent*) +{ + SolarMutexGuard aGuard; + m_rParent.CallCallback(SalObjEvent::LoseFocus); +} + +void QtObjectWidget::mousePressEvent(QMouseEvent* pEvent) +{ + SolarMutexGuard aGuard; + m_rParent.CallCallback(SalObjEvent::ToTop); + + if (m_rParent.forwardKey()) + pEvent->ignore(); +} + +void QtObjectWidget::mouseReleaseEvent(QMouseEvent* pEvent) +{ + if (m_rParent.forwardKey()) + pEvent->ignore(); +} + +void QtObjectWidget::keyReleaseEvent(QKeyEvent* pEvent) +{ + if (m_rParent.forwardKey()) + pEvent->ignore(); +} + +void QtObjectWidget::keyPressEvent(QKeyEvent* pEvent) +{ + if (m_rParent.forwardKey()) + pEvent->ignore(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtOpenGLContext.cxx b/vcl/qt5/QtOpenGLContext.cxx new file mode 100644 index 0000000000..9dd75b69a1 --- /dev/null +++ b/vcl/qt5/QtOpenGLContext.cxx @@ -0,0 +1,154 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtOpenGLContext.hxx> + +#include <epoxy/gl.h> + +#include <vcl/sysdata.hxx> +#include <opengl/zone.hxx> +#include <sal/log.hxx> + +#include <window.h> + +#include <QtObject.hxx> + +#include <QtGui/QOpenGLContext> +#include <QtGui/QWindow> + +bool QtOpenGLContext::g_bAnyCurrent = false; + +void QtOpenGLContext::swapBuffers() +{ + OpenGLZone aZone; + + if (m_pContext && m_pWindow && m_pWindow->isExposed()) + { + m_pContext->swapBuffers(m_pWindow); + } + + BuffersSwapped(); +} + +void QtOpenGLContext::resetCurrent() +{ + clearCurrent(); + + OpenGLZone aZone; + + if (m_pContext) + { + m_pContext->doneCurrent(); + g_bAnyCurrent = false; + } +} + +bool QtOpenGLContext::isCurrent() +{ + OpenGLZone aZone; + return g_bAnyCurrent && (QOpenGLContext::currentContext() == m_pContext); +} + +bool QtOpenGLContext::isAnyCurrent() +{ + OpenGLZone aZone; + return g_bAnyCurrent && (QOpenGLContext::currentContext() != nullptr); +} + +bool QtOpenGLContext::ImplInit() +{ + if (!m_pWindow) + { + SAL_WARN("vcl.opengl.qt", "failed to create window"); + return false; + } + + m_pWindow->setSurfaceType(QSurface::OpenGLSurface); + m_pWindow->create(); + + m_pContext = new QOpenGLContext(m_pWindow); + if (!m_pContext->create()) + { + SAL_WARN("vcl.opengl.qt", "failed to create context"); + return false; + } + + m_pContext->makeCurrent(m_pWindow); + g_bAnyCurrent = true; + + bool bRet = InitGL(); + InitGLDebugging(); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + + registerAsCurrent(); + + return bRet; +} + +void QtOpenGLContext::makeCurrent() +{ + if (isCurrent()) + return; + + OpenGLZone aZone; + + clearCurrent(); + + if (m_pContext && m_pWindow) + { + m_pContext->makeCurrent(m_pWindow); + g_bAnyCurrent = true; + } + + registerAsCurrent(); +} + +void QtOpenGLContext::destroyCurrentContext() +{ + OpenGLZone aZone; + + if (m_pContext) + { + m_pContext->doneCurrent(); + g_bAnyCurrent = false; + } + + if (glGetError() != GL_NO_ERROR) + { + SAL_WARN("vcl.opengl.qt", "glError: " << glGetError()); + } +} + +void QtOpenGLContext::initWindow() +{ + if (!m_pChildWindow) + { + SystemWindowData winData = generateWinData(mpWindow, mbRequestLegacyContext); + m_pChildWindow = VclPtr<SystemChildWindow>::Create(mpWindow, 0, &winData, false); + } + + if (m_pChildWindow) + { + InitChildWindow(m_pChildWindow.get()); + } + + m_pWindow + = static_cast<QtObject*>(m_pChildWindow->ImplGetWindowImpl()->mpSysObj)->windowHandle(); +} diff --git a/vcl/qt5/QtPainter.cxx b/vcl/qt5/QtPainter.cxx new file mode 100644 index 0000000000..115b4a82c6 --- /dev/null +++ b/vcl/qt5/QtPainter.cxx @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtPainter.hxx> + +#include <QtGui/QColor> + +QtPainter::QtPainter(QtGraphicsBackend& rGraphics, bool bPrepareBrush, sal_uInt8 nTransparency) + : m_rGraphics(rGraphics) +{ + if (rGraphics.m_pQImage) + { + if (!begin(rGraphics.m_pQImage)) + std::abort(); + } + else + { + assert(rGraphics.m_pFrame); + if (!begin(rGraphics.m_pFrame->GetQWidget())) + std::abort(); + } + if (!rGraphics.m_aClipPath.isEmpty()) + setClipPath(rGraphics.m_aClipPath); + else + setClipRegion(rGraphics.m_aClipRegion); + if (rGraphics.m_oLineColor) + { + QColor aColor = toQColor(*rGraphics.m_oLineColor); + aColor.setAlpha(nTransparency); + setPen(aColor); + } + else + setPen(Qt::NoPen); + if (bPrepareBrush && rGraphics.m_oFillColor) + { + QColor aColor = toQColor(*rGraphics.m_oFillColor); + aColor.setAlpha(nTransparency); + setBrush(aColor); + } + setCompositionMode(rGraphics.m_eCompositionMode); + setRenderHint(QPainter::Antialiasing, m_rGraphics.getAntiAlias()); +} diff --git a/vcl/qt5/QtPrinter.cxx b/vcl/qt5/QtPrinter.cxx new file mode 100644 index 0000000000..346dd3f26e --- /dev/null +++ b/vcl/qt5/QtPrinter.cxx @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtPrinter.hxx> + +QtPrinter::QtPrinter(SalInfoPrinter* pInfoPrinter) + : PspSalPrinter(pInfoPrinter) +{ +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtSvpGraphics.cxx b/vcl/qt5/QtSvpGraphics.cxx new file mode 100644 index 0000000000..903fee1f56 --- /dev/null +++ b/vcl/qt5/QtSvpGraphics.cxx @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <sal/config.h> +#include <sal/log.hxx> +#include <salbmp.hxx> + +#include <config_cairo_canvas.h> + +#include <QtData.hxx> +#include <QtFrame.hxx> +#include <QtGraphics_Controls.hxx> +#include <QtSvpGraphics.hxx> +#include <QtSvpSurface.hxx> +#include <QtTools.hxx> + +#include <QtGui/QScreen> +#include <QtGui/QWindow> +#include <QtWidgets/QWidget> + +QtSvpGraphics::QtSvpGraphics(QtFrame* pFrame) + : m_pFrame(pFrame) +{ + if (!QtData::noNativeControls()) + m_pWidgetDraw.reset(new QtGraphics_Controls(*this)); + if (m_pFrame) + setDevicePixelRatioF(m_pFrame->devicePixelRatioF()); +} + +QtSvpGraphics::~QtSvpGraphics() {} + +void QtSvpGraphics::updateQWidget() const +{ + if (!m_pFrame) + return; + QWidget* pQWidget = m_pFrame->GetQWidget(); + if (pQWidget) + pQWidget->update(pQWidget->rect()); +} + +#if ENABLE_CAIRO_CANVAS + +bool QtSvpGraphics::SupportsCairo() const { return true; } + +cairo::SurfaceSharedPtr +QtSvpGraphics::CreateSurface(const cairo::CairoSurfaceSharedPtr& rSurface) const +{ + return std::make_shared<cairo::QtSvpSurface>(rSurface); +} + +cairo::SurfaceSharedPtr QtSvpGraphics::CreateSurface(const OutputDevice& /*rRefDevice*/, int x, + int y, int width, int height) const +{ + return std::make_shared<cairo::QtSvpSurface>(this, x, y, width, height); +} + +#endif + +static void QImage2BitmapBuffer(QImage& rImg, BitmapBuffer& rBuf) +{ + assert(rImg.width()); + assert(rImg.height()); + + rBuf.mnWidth = rImg.width(); + rBuf.mnHeight = rImg.height(); + rBuf.mnBitCount = getFormatBits(rImg.format()); + rBuf.mpBits = rImg.bits(); + rBuf.mnScanlineSize = rImg.bytesPerLine(); +} + +void QtSvpGraphics::handleDamage(const tools::Rectangle& rDamagedRegion) +{ + assert(m_pWidgetDraw); + assert(dynamic_cast<QtGraphics_Controls*>(m_pWidgetDraw.get())); + assert(!rDamagedRegion.IsEmpty()); + + QImage* pImage = static_cast<QtGraphics_Controls*>(m_pWidgetDraw.get())->getImage(); + assert(pImage); + if (pImage->width() == 0 || pImage->height() == 0) + return; + + BitmapBuffer aBuffer; + QImage2BitmapBuffer(*pImage, aBuffer); + SalTwoRect aTR(0, 0, pImage->width(), pImage->height(), rDamagedRegion.Left(), + rDamagedRegion.Top(), rDamagedRegion.GetWidth(), rDamagedRegion.GetHeight()); + + getSvpBackend()->drawBitmapBuffer(aTR, &aBuffer, CAIRO_OPERATOR_OVER); +} + +void QtSvpGraphics::GetResolution(sal_Int32& rDPIX, sal_Int32& rDPIY) +{ + char* pForceDpi; + if ((pForceDpi = getenv("SAL_FORCEDPI"))) + { + OString sForceDPI(pForceDpi); + rDPIX = rDPIY = sForceDPI.toInt32(); + return; + } + + if (!m_pFrame) + return; + + QScreen* pScreen = m_pFrame->GetQWidget()->screen(); + rDPIX = pScreen->logicalDotsPerInchX() * pScreen->devicePixelRatio() + 0.5; + rDPIY = pScreen->logicalDotsPerInchY() * pScreen->devicePixelRatio() + 0.5; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtSvpSurface.cxx b/vcl/qt5/QtSvpSurface.cxx new file mode 100644 index 0000000000..760419bd7f --- /dev/null +++ b/vcl/qt5/QtSvpSurface.cxx @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <utility> + +#include <QtSvpSurface.hxx> + +#include <QtSvpGraphics.hxx> + +#include <vcl/sysdata.hxx> +#include <vcl/bitmap.hxx> +#include <vcl/virdev.hxx> +#include <vcl/window.hxx> +#include <basegfx/vector/b2isize.hxx> + +namespace +{ +Size get_surface_size(cairo_surface_t* surface) +{ + cairo_t* cr = cairo_create(surface); + double x1, x2, y1, y2; + cairo_clip_extents(cr, &x1, &y1, &x2, &y2); + cairo_destroy(cr); + return Size(x2 - x1, y2 - y1); +} +} + +namespace cairo +{ +QtSvpSurface::QtSvpSurface(CairoSurfaceSharedPtr pSurface) + : m_pGraphics(nullptr) + , m_pCairoContext(nullptr) + , m_pSurface(std::move(pSurface)) +{ +} + +QtSvpSurface::QtSvpSurface(const QtSvpGraphics* pGraphics, int x, int y, int width, int height) + : m_pGraphics(pGraphics) + , m_pCairoContext(pGraphics->getCairoContext()) +{ + cairo_surface_t* surface = cairo_get_target(m_pCairoContext); + m_pSurface.reset(cairo_surface_create_for_rectangle(surface, x, y, width, height), + &cairo_surface_destroy); +} + +QtSvpSurface::~QtSvpSurface() +{ + if (m_pCairoContext) + cairo_destroy(m_pCairoContext); +} + +CairoSharedPtr QtSvpSurface::getCairo() const +{ + return CairoSharedPtr(cairo_create(m_pSurface.get()), &cairo_destroy); +} + +SurfaceSharedPtr QtSvpSurface::getSimilar(int cairo_content_type, int width, int height) const +{ + return std::make_shared<QtSvpSurface>(CairoSurfaceSharedPtr( + cairo_surface_create_similar( + m_pSurface.get(), static_cast<cairo_content_t>(cairo_content_type), width, height), + &cairo_surface_destroy)); +} + +void QtSvpSurface::flush() const +{ + cairo_surface_flush(m_pSurface.get()); + if (m_pGraphics) + m_pGraphics->updateQWidget(); +} + +VclPtr<VirtualDevice> QtSvpSurface::createVirtualDevice() const +{ + SystemGraphicsData aSystemGraphicsData; + + aSystemGraphicsData.nSize = sizeof(SystemGraphicsData); + aSystemGraphicsData.pSurface = m_pSurface.get(); + + return VclPtr<VirtualDevice>::Create(aSystemGraphicsData, get_surface_size(m_pSurface.get()), + DeviceFormat::WITHOUT_ALPHA); +} + +} // namespace cairo + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtSvpVirtualDevice.hxx b/vcl/qt5/QtSvpVirtualDevice.hxx new file mode 100644 index 0000000000..26247a6e1d --- /dev/null +++ b/vcl/qt5/QtSvpVirtualDevice.hxx @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <headless/svpvd.hxx> +#include <QtSvpGraphics.hxx> + +class VCL_DLLPUBLIC QtSvpVirtualDevice : public SvpSalVirtualDevice +{ +public: + QtSvpVirtualDevice(cairo_surface_t* pRefSurface, cairo_surface_t* pPreExistingTarget) + : SvpSalVirtualDevice(pRefSurface, pPreExistingTarget) + { + } + + SalGraphics* AcquireGraphics() override { return AddGraphics(new QtSvpGraphics(nullptr)); } +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtSystem.cxx b/vcl/qt5/QtSystem.cxx new file mode 100644 index 0000000000..d43e47832f --- /dev/null +++ b/vcl/qt5/QtSystem.cxx @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <QtGui/QGuiApplication> +#include <QtGui/QScreen> + +#include <tools/gen.hxx> +#include <QtSystem.hxx> +#include <QtTools.hxx> + +unsigned int QtSystem::GetDisplayScreenCount() { return QGuiApplication::screens().size(); } + +AbsoluteScreenPixelRectangle QtSystem::GetDisplayScreenPosSizePixel(unsigned int nScreen) +{ + QRect qRect = QGuiApplication::screens().at(nScreen)->geometry(); + return AbsoluteScreenPixelRectangle(toRectangle(scaledQRect(qRect, qApp->devicePixelRatio()))); +} + +int QtSystem::ShowNativeDialog(const OUString&, const OUString&, const std::vector<OUString>&) +{ + return 0; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtTimer.cxx b/vcl/qt5/QtTimer.cxx new file mode 100644 index 0000000000..1a34213977 --- /dev/null +++ b/vcl/qt5/QtTimer.cxx @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtTimer.hxx> +#include <QtTimer.moc> + +#include <QtInstance.hxx> + +#include <QtWidgets/QApplication> +#include <QtCore/QThread> + +#include <vcl/svapp.hxx> +#include <sal/log.hxx> + +#include <svdata.hxx> + +QtTimer::QtTimer() +{ + m_aTimer.setSingleShot(true); + m_aTimer.setTimerType(Qt::PreciseTimer); + connect(&m_aTimer, SIGNAL(timeout()), this, SLOT(timeoutActivated())); + connect(this, SIGNAL(startTimerSignal(int)), this, SLOT(startTimer(int))); + connect(this, SIGNAL(stopTimerSignal()), this, SLOT(stopTimer())); +} + +void QtTimer::timeoutActivated() +{ + SolarMutexGuard aGuard; + if (Application::IsOnSystemEventLoop()) + { + const ImplSVData* pSVData = ImplGetSVData(); + assert(pSVData && pSVData->mpDefInst); + static_cast<QtInstance*>(pSVData->mpDefInst)->DispatchUserEvents(true); + } + CallCallback(); +} + +void QtTimer::startTimer(int nMS) { m_aTimer.start(nMS); } + +void QtTimer::Start(sal_uInt64 nMS) { Q_EMIT startTimerSignal(nMS); } + +void QtTimer::stopTimer() { m_aTimer.stop(); } + +void QtTimer::Stop() { Q_EMIT stopTimerSignal(); } + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtTools.cxx b/vcl/qt5/QtTools.cxx new file mode 100644 index 0000000000..030b3af2b5 --- /dev/null +++ b/vcl/qt5/QtTools.cxx @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtTools.hxx> + +#include <cairo.h> + +#include <tools/stream.hxx> +#include <vcl/event.hxx> +#include <vcl/image.hxx> +#include <vcl/filter/PngImageWriter.hxx> + +#include <QtGui/QImage> + +void CairoDeleter::operator()(cairo_surface_t* pSurface) const { cairo_surface_destroy(pSurface); } + +sal_uInt16 GetKeyModCode(Qt::KeyboardModifiers eKeyModifiers) +{ + sal_uInt16 nCode = 0; + if (eKeyModifiers & Qt::ShiftModifier) + nCode |= KEY_SHIFT; + if (eKeyModifiers & Qt::ControlModifier) + nCode |= KEY_MOD1; + if (eKeyModifiers & Qt::AltModifier) + nCode |= KEY_MOD2; + if (eKeyModifiers & Qt::MetaModifier) + nCode |= KEY_MOD3; + return nCode; +} + +sal_uInt16 GetMouseModCode(Qt::MouseButtons eButtons) +{ + sal_uInt16 nCode = 0; + if (eButtons & Qt::LeftButton) + nCode |= MOUSE_LEFT; + if (eButtons & Qt::MiddleButton) + nCode |= MOUSE_MIDDLE; + if (eButtons & Qt::RightButton) + nCode |= MOUSE_RIGHT; + return nCode; +} + +Qt::DropActions toQtDropActions(sal_Int8 dragOperation) +{ + Qt::DropActions eRet = Qt::IgnoreAction; + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_COPY) + eRet |= Qt::CopyAction; + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_MOVE) + eRet |= Qt::MoveAction; + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_LINK) + eRet |= Qt::LinkAction; + return eRet; +} + +sal_Int8 toVclDropActions(Qt::DropActions dragOperation) +{ + sal_Int8 nRet(0); + if (dragOperation & Qt::CopyAction) + nRet |= css::datatransfer::dnd::DNDConstants::ACTION_COPY; + if (dragOperation & Qt::MoveAction) + nRet |= css::datatransfer::dnd::DNDConstants::ACTION_MOVE; + if (dragOperation & Qt::LinkAction) + nRet |= css::datatransfer::dnd::DNDConstants::ACTION_LINK; + return nRet; +} + +sal_Int8 toVclDropAction(Qt::DropAction dragOperation) +{ + sal_Int8 nRet(0); + if (dragOperation == Qt::CopyAction) + nRet = css::datatransfer::dnd::DNDConstants::ACTION_COPY; + else if (dragOperation == Qt::MoveAction) + nRet = css::datatransfer::dnd::DNDConstants::ACTION_MOVE; + else if (dragOperation == Qt::LinkAction) + nRet = css::datatransfer::dnd::DNDConstants::ACTION_LINK; + return nRet; +} + +Qt::DropAction getPreferredDropAction(sal_Int8 dragOperation) +{ + Qt::DropAction eAct = Qt::IgnoreAction; + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_MOVE) + eAct = Qt::MoveAction; + else if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_COPY) + eAct = Qt::CopyAction; + else if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_LINK) + eAct = Qt::LinkAction; + return eAct; +} + +QImage toQImage(const Image& rImage) +{ + QImage aImage; + + if (!!rImage) + { + SvMemoryStream aMemStm; + auto rBitmapEx = rImage.GetBitmapEx(); + vcl::PngImageWriter aWriter(aMemStm); + aWriter.write(rBitmapEx); + aImage.loadFromData(static_cast<const uchar*>(aMemStm.GetData()), aMemStm.TellEnd()); + } + + return aImage; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtTransferable.cxx b/vcl/qt5/QtTransferable.cxx new file mode 100644 index 0000000000..d9e0beaa71 --- /dev/null +++ b/vcl/qt5/QtTransferable.cxx @@ -0,0 +1,361 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + +#include <QtTransferable.hxx> + +#include <comphelper/sequence.hxx> +#include <sal/log.hxx> +#include <o3tl/string_view.hxx> + +#include <QtWidgets/QApplication> + +#include <QtInstance.hxx> +#include <QtTools.hxx> + +#include <cassert> + +static bool lcl_textMimeInfo(std::u16string_view rMimeString, bool& bHaveNoCharset, + bool& bHaveUTF16, bool& bHaveUTF8) +{ + sal_Int32 nIndex = 0; + if (o3tl::getToken(rMimeString, 0, ';', nIndex) == u"text/plain") + { + std::u16string_view aToken(o3tl::getToken(rMimeString, 0, ';', nIndex)); + if (aToken == u"charset=utf-16") + bHaveUTF16 = true; + else if (aToken == u"charset=utf-8") + bHaveUTF8 = true; + else if (aToken.empty()) + bHaveNoCharset = true; + else // we just handle UTF-16 and UTF-8, everything else is "bytes" + return false; + return true; + } + return false; +} + +QtTransferable::QtTransferable(const QMimeData* pMimeData) + : m_pMimeData(pMimeData) + , m_bProvideUTF16FromOtherEncoding(false) +{ + assert(pMimeData); +} + +css::uno::Sequence<css::datatransfer::DataFlavor> SAL_CALL QtTransferable::getTransferDataFlavors() +{ + // it's just filled once, ever, so just try to get it without locking first + if (m_aMimeTypeSeq.hasElements()) + return m_aMimeTypeSeq; + + // better safe then sorry; preventing broken usage + // DnD should not be shared and Clipboard access runs in the GUI thread + osl::MutexGuard aGuard(m_aMutex); + if (m_aMimeTypeSeq.hasElements()) + return m_aMimeTypeSeq; + + QStringList aFormatList(m_pMimeData->formats()); + // we might add the UTF-16 mime text variant later + const int nMimeTypeSeqSize = aFormatList.size() + 1; + bool bHaveNoCharset = false, bHaveUTF16 = false, bHaveUTF8 = false; + css::uno::Sequence<css::datatransfer::DataFlavor> aMimeTypeSeq(nMimeTypeSeqSize); + auto pMimeTypeSeq = aMimeTypeSeq.getArray(); + + css::datatransfer::DataFlavor aFlavor; + int nMimeTypeCount = 0; + + for (const QString& rMimeType : aFormatList) + { + // filter out non-MIME types such as TARGETS, MULTIPLE, TIMESTAMP + if (rMimeType.indexOf('/') == -1) + continue; + + // gtk3 thinks it is not well defined - skip too + if (rMimeType == QStringLiteral("text/plain;charset=unicode")) + continue; + + // LO doesn't like 'text/plain', so we have to provide UTF-16 + bool bIsNoCharset = false, bIsUTF16 = false, bIsUTF8 = false; + if (lcl_textMimeInfo(toOUString(rMimeType), bIsNoCharset, bIsUTF16, bIsUTF8)) + { + bHaveNoCharset |= bIsNoCharset; + bHaveUTF16 |= bIsUTF16; + bHaveUTF8 |= bIsUTF8; + if (bIsUTF16) + aFlavor.DataType = cppu::UnoType<OUString>::get(); + else + aFlavor.DataType = cppu::UnoType<css::uno::Sequence<sal_Int8>>::get(); + } + else + aFlavor.DataType = cppu::UnoType<css::uno::Sequence<sal_Int8>>::get(); + + aFlavor.MimeType = toOUString(rMimeType); + assert(nMimeTypeCount < nMimeTypeSeqSize); + pMimeTypeSeq[nMimeTypeCount] = aFlavor; + nMimeTypeCount++; + } + + m_bProvideUTF16FromOtherEncoding = (bHaveNoCharset || bHaveUTF8) && !bHaveUTF16; + if (m_bProvideUTF16FromOtherEncoding) + { + aFlavor.MimeType = "text/plain;charset=utf-16"; + aFlavor.DataType = cppu::UnoType<OUString>::get(); + assert(nMimeTypeCount < nMimeTypeSeqSize); + pMimeTypeSeq[nMimeTypeCount] = aFlavor; + nMimeTypeCount++; + } + + aMimeTypeSeq.realloc(nMimeTypeCount); + + m_aMimeTypeSeq = aMimeTypeSeq; + return m_aMimeTypeSeq; +} + +sal_Bool SAL_CALL +QtTransferable::isDataFlavorSupported(const css::datatransfer::DataFlavor& rFlavor) +{ + const auto aSeq = getTransferDataFlavors(); + return std::any_of(aSeq.begin(), aSeq.end(), [&](const css::datatransfer::DataFlavor& aFlavor) { + return rFlavor.MimeType == aFlavor.MimeType; + }); +} + +css::uno::Any SAL_CALL QtTransferable::getTransferData(const css::datatransfer::DataFlavor& rFlavor) +{ + css::uno::Any aAny; + if (!isDataFlavorSupported(rFlavor)) + return aAny; + + if (rFlavor.MimeType == "text/plain;charset=utf-16") + { + OUString aString; + if (m_bProvideUTF16FromOtherEncoding) + { + if (m_pMimeData->hasFormat("text/plain;charset=utf-8")) + { + QByteArray aByteData(m_pMimeData->data(QStringLiteral("text/plain;charset=utf-8"))); + aString = OUString::fromUtf8(reinterpret_cast<const char*>(aByteData.data())); + } + else + { + QByteArray aByteData(m_pMimeData->data(QStringLiteral("text/plain"))); + aString = OUString(reinterpret_cast<const char*>(aByteData.data()), + aByteData.size(), osl_getThreadTextEncoding()); + } + } + else + { + QByteArray aByteData(m_pMimeData->data(toQString(rFlavor.MimeType))); + aString = OUString(reinterpret_cast<const sal_Unicode*>(aByteData.data()), + aByteData.size() / 2); + } + aAny <<= aString; + } + else + { + QByteArray aByteData(m_pMimeData->data(toQString(rFlavor.MimeType))); + css::uno::Sequence<sal_Int8> aSeq(reinterpret_cast<const sal_Int8*>(aByteData.data()), + aByteData.size()); + aAny <<= aSeq; + } + + return aAny; +} + +QtClipboardTransferable::QtClipboardTransferable(const QClipboard::Mode aMode, + const QMimeData* pMimeData) + : QtTransferable(pMimeData) + , m_aMode(aMode) +{ +} + +bool QtClipboardTransferable::hasInFlightChanged() const +{ + const bool bChanged(mimeData() != QApplication::clipboard()->mimeData(m_aMode)); + SAL_WARN_IF(bChanged, "vcl.qt", "In flight clipboard change detected - broken clipboard read!"); + return bChanged; +} + +css::uno::Any SAL_CALL +QtClipboardTransferable::getTransferData(const css::datatransfer::DataFlavor& rFlavor) +{ + css::uno::Any aAny; + auto* pSalInst(GetQtInstance()); + SolarMutexGuard g; + pSalInst->RunInMainThread([&, this]() { + if (!hasInFlightChanged()) + aAny = QtTransferable::getTransferData(rFlavor); + }); + return aAny; +} + +css::uno::Sequence<css::datatransfer::DataFlavor> + SAL_CALL QtClipboardTransferable::getTransferDataFlavors() +{ + css::uno::Sequence<css::datatransfer::DataFlavor> aSeq; + auto* pSalInst(GetQtInstance()); + SolarMutexGuard g; + pSalInst->RunInMainThread([&, this]() { + if (!hasInFlightChanged()) + aSeq = QtTransferable::getTransferDataFlavors(); + }); + return aSeq; +} + +sal_Bool SAL_CALL +QtClipboardTransferable::isDataFlavorSupported(const css::datatransfer::DataFlavor& rFlavor) +{ + bool bIsSupported = false; + auto* pSalInst(GetQtInstance()); + SolarMutexGuard g; + pSalInst->RunInMainThread([&, this]() { + if (!hasInFlightChanged()) + bIsSupported = QtTransferable::isDataFlavorSupported(rFlavor); + }); + return bIsSupported; +} + +QtMimeData::QtMimeData(const css::uno::Reference<css::datatransfer::XTransferable>& xTrans) + : m_aContents(xTrans) + , m_bHaveNoCharset(false) + , m_bHaveUTF8(false) +{ + assert(xTrans.is()); +} + +bool QtMimeData::deepCopy(QMimeData** const pMimeCopy) const +{ + if (!pMimeCopy) + return false; + + QMimeData* pMimeData = new QMimeData(); + for (QString& format : formats()) + { + QByteArray aData = data(format); + // Checking for custom MIME types + if (format.startsWith("application/x-qt")) + { + // Retrieving true format name + int indexBegin = format.indexOf('"') + 1; + int indexEnd = format.indexOf('"', indexBegin); + format = format.mid(indexBegin, indexEnd - indexBegin); + } + pMimeData->setData(format, aData); + } + + *pMimeCopy = pMimeData; + return true; +} + +QStringList QtMimeData::formats() const +{ + if (!m_aMimeTypeList.isEmpty()) + return m_aMimeTypeList; + + const css::uno::Sequence<css::datatransfer::DataFlavor> aFormats + = m_aContents->getTransferDataFlavors(); + QStringList aList; + bool bHaveUTF16 = false; + + for (const auto& rFlavor : aFormats) + { + aList << toQString(rFlavor.MimeType); + lcl_textMimeInfo(rFlavor.MimeType, m_bHaveNoCharset, bHaveUTF16, m_bHaveUTF8); + } + + // we provide a locale encoded and a UTF-8 variant, if missing + if (m_bHaveNoCharset || bHaveUTF16 || m_bHaveUTF8) + { + // if there is a text representation from LO point of view, it'll be UTF-16 + assert(bHaveUTF16); + if (!m_bHaveUTF8) + aList << QStringLiteral("text/plain;charset=utf-8"); + if (!m_bHaveNoCharset) + aList << QStringLiteral("text/plain"); + } + + m_aMimeTypeList = aList; + return m_aMimeTypeList; +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +QVariant QtMimeData::retrieveData(const QString& mimeType, QVariant::Type) const +#else +QVariant QtMimeData::retrieveData(const QString& mimeType, QMetaType) const +#endif +{ + if (!hasFormat(mimeType)) + return QVariant(); + + css::datatransfer::DataFlavor aFlavor; + aFlavor.MimeType = toOUString(mimeType); + aFlavor.DataType = cppu::UnoType<css::uno::Sequence<sal_Int8>>::get(); + + bool bWantNoCharset = false, bWantUTF16 = false, bWantUTF8 = false; + if (lcl_textMimeInfo(aFlavor.MimeType, bWantNoCharset, bWantUTF16, bWantUTF8)) + { + if ((bWantNoCharset && !m_bHaveNoCharset) || (bWantUTF8 && !m_bHaveUTF8)) + { + aFlavor.MimeType = "text/plain;charset=utf-16"; + aFlavor.DataType = cppu::UnoType<OUString>::get(); + } + else if (bWantUTF16) + aFlavor.DataType = cppu::UnoType<OUString>::get(); + } + + css::uno::Any aValue; + + try + { + // tdf#129809 take a reference in case m_aContents is replaced during this call + css::uno::Reference<com::sun::star::datatransfer::XTransferable> xCurrentContents( + m_aContents); + aValue = xCurrentContents->getTransferData(aFlavor); + } + catch (...) + { + } + + QByteArray aByteArray; + if (aValue.getValueTypeClass() == css::uno::TypeClass_STRING) + { + OUString aString; + aValue >>= aString; + + if (bWantUTF8) + { + OString aUTF8String(OUStringToOString(aString, RTL_TEXTENCODING_UTF8)); + aByteArray = QByteArray(aUTF8String.getStr(), aUTF8String.getLength()); + } + else if (bWantNoCharset) + { + OString aLocaleString(OUStringToOString(aString, osl_getThreadTextEncoding())); + aByteArray = QByteArray(aLocaleString.getStr(), aLocaleString.getLength()); + } + else if (bWantUTF16) + { + aByteArray = QByteArray(reinterpret_cast<const char*>(aString.getStr()), + aString.getLength() * 2); + } + else + return QVariant(toQString(aString)); + } + else + { + css::uno::Sequence<sal_Int8> aData; + aValue >>= aData; + aByteArray + = QByteArray(reinterpret_cast<const char*>(aData.getConstArray()), aData.getLength()); + } + return QVariant::fromValue(aByteArray); +} + +bool QtMimeData::hasFormat(const QString& mimeType) const { return formats().contains(mimeType); } + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtVirtualDevice.cxx b/vcl/qt5/QtVirtualDevice.cxx new file mode 100644 index 0000000000..22844f1df6 --- /dev/null +++ b/vcl/qt5/QtVirtualDevice.cxx @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtVirtualDevice.hxx> + +#include <QtGraphics.hxx> +#include <QtTools.hxx> + +#include <QtGui/QImage> + +QtVirtualDevice::QtVirtualDevice(double fScale) + : m_fScale(fScale) +{ +} + +SalGraphics* QtVirtualDevice::AcquireGraphics() +{ + assert(m_pImage); + QtGraphics* pGraphics = new QtGraphics(m_pImage.get()); + m_aGraphics.push_back(pGraphics); + return pGraphics; +} + +void QtVirtualDevice::ReleaseGraphics(SalGraphics* pGraphics) +{ + std::erase(m_aGraphics, dynamic_cast<QtGraphics*>(pGraphics)); + delete pGraphics; +} + +bool QtVirtualDevice::SetSize(tools::Long nNewDX, tools::Long nNewDY) +{ + return SetSizeUsingBuffer(nNewDX, nNewDY, nullptr); +} + +bool QtVirtualDevice::SetSizeUsingBuffer(tools::Long nNewDX, tools::Long nNewDY, sal_uInt8* pBuffer) +{ + if (nNewDX == 0) + nNewDX = 1; + if (nNewDY == 0) + nNewDY = 1; + + if (m_pImage && m_aFrameSize.width() == nNewDX && m_aFrameSize.height() == nNewDY) + return true; + + m_aFrameSize = QSize(nNewDX, nNewDY); + + nNewDX *= m_fScale; + nNewDY *= m_fScale; + + if (pBuffer) + m_pImage.reset(new QImage(pBuffer, nNewDX, nNewDY, Qt_DefaultFormat32)); + else + m_pImage.reset(new QImage(nNewDX, nNewDY, Qt_DefaultFormat32)); + + m_pImage->fill(Qt::transparent); + m_pImage->setDevicePixelRatio(m_fScale); + + // update device in existing graphics + for (auto pQtGraph : m_aGraphics) + pQtGraph->ChangeQImage(m_pImage.get()); + + return true; +} + +tools::Long QtVirtualDevice::GetWidth() const { return m_pImage ? m_aFrameSize.width() : 0; } + +tools::Long QtVirtualDevice::GetHeight() const { return m_pImage ? m_aFrameSize.height() : 0; } + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtWidget.cxx b/vcl/qt5/QtWidget.cxx new file mode 100644 index 0000000000..a7c4f32e92 --- /dev/null +++ b/vcl/qt5/QtWidget.cxx @@ -0,0 +1,1024 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <QtWidget.hxx> +#include <QtWidget.moc> + +#include <QtFrame.hxx> +#include <QtGraphics.hxx> +#include <QtInstance.hxx> +#include <QtMainWindow.hxx> +#include <QtSvpGraphics.hxx> +#include <QtTransferable.hxx> +#include <QtTools.hxx> + +#include <QtCore/QMimeData> +#include <QtGui/QDrag> +#include <QtGui/QFocusEvent> +#include <QtGui/QGuiApplication> +#include <QtGui/QImage> +#include <QtGui/QKeyEvent> +#include <QtGui/QMouseEvent> +#include <QtGui/QPainter> +#include <QtGui/QPaintEvent> +#include <QtGui/QResizeEvent> +#include <QtGui/QShowEvent> +#include <QtGui/QTextCharFormat> +#include <QtGui/QWheelEvent> +#include <QtWidgets/QMainWindow> +#include <QtWidgets/QToolTip> +#include <QtWidgets/QWidget> + +#include <cairo.h> +#include <vcl/commandevent.hxx> +#include <vcl/event.hxx> +#include <vcl/toolkit/floatwin.hxx> +#include <window.h> +#include <comphelper/diagnose_ex.hxx> + +#include <com/sun/star/accessibility/XAccessibleContext.hpp> +#include <com/sun/star/accessibility/XAccessibleEditableText.hpp> + +#if CHECK_ANY_QT_USING_X11 +#define XK_MISCELLANY +#include <X11/keysymdef.h> +#endif + +using namespace com::sun::star; + +void QtWidget::paintEvent(QPaintEvent* pEvent) +{ + QPainter p(this); + if (!m_rFrame.m_bNullRegion) + p.setClipRegion(m_rFrame.m_aRegion); + + QImage aImage; + if (m_rFrame.m_bUseCairo) + { + cairo_surface_t* pSurface = m_rFrame.m_pSurface.get(); + cairo_surface_flush(pSurface); + + aImage = QImage(cairo_image_surface_get_data(pSurface), + cairo_image_surface_get_width(pSurface), + cairo_image_surface_get_height(pSurface), Qt_DefaultFormat32); + } + else + aImage = *m_rFrame.m_pQImage; + + const qreal fRatio = m_rFrame.devicePixelRatioF(); + aImage.setDevicePixelRatio(fRatio); + QRectF source(pEvent->rect().topLeft() * fRatio, pEvent->rect().size() * fRatio); + p.drawImage(pEvent->rect(), aImage, source); +} + +void QtWidget::resizeEvent(QResizeEvent* pEvent) +{ + const qreal fRatio = m_rFrame.devicePixelRatioF(); + const int nWidth = ceil(pEvent->size().width() * fRatio); + const int nHeight = ceil(pEvent->size().height() * fRatio); + + m_rFrame.maGeometry.setSize({ nWidth, nHeight }); + + if (m_rFrame.m_bUseCairo) + { + if (m_rFrame.m_pSurface) + { + const int nOldWidth = cairo_image_surface_get_width(m_rFrame.m_pSurface.get()); + const int nOldHeight = cairo_image_surface_get_height(m_rFrame.m_pSurface.get()); + if (nOldWidth != nWidth || nOldHeight != nHeight) + { + cairo_surface_t* pSurface + = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, nWidth, nHeight); + cairo_surface_set_user_data(pSurface, SvpSalGraphics::getDamageKey(), + &m_rFrame.m_aDamageHandler, nullptr); + m_rFrame.m_pSvpGraphics->setSurface(pSurface, basegfx::B2IVector(nWidth, nHeight)); + UniqueCairoSurface old_surface(m_rFrame.m_pSurface.release()); + m_rFrame.m_pSurface.reset(pSurface); + + const int nMinWidth = qMin(nOldWidth, nWidth); + const int nMinHeight = qMin(nOldHeight, nHeight); + SalTwoRect rect(0, 0, nMinWidth, nMinHeight, 0, 0, nMinWidth, nMinHeight); + m_rFrame.m_pSvpGraphics->copySource(rect, old_surface.get()); + } + } + } + else + { + if (m_rFrame.m_pQImage && m_rFrame.m_pQImage->size() != QSize(nWidth, nHeight)) + { + QImage* pImage = new QImage(m_rFrame.m_pQImage->copy(0, 0, nWidth, nHeight)); + m_rFrame.m_pQtGraphics->ChangeQImage(pImage); + m_rFrame.m_pQImage.reset(pImage); + } + } + + m_rFrame.CallCallback(SalEvent::Resize, nullptr); +} + +void QtWidget::fakeResize() +{ + QResizeEvent aEvent(size(), QSize()); + resizeEvent(&aEvent); +} + +void QtWidget::fillSalAbstractMouseEvent(const QtFrame& rFrame, const QInputEvent* pQEvent, + const QPoint& rPos, Qt::MouseButtons eButtons, int nWidth, + SalAbstractMouseEvent& aSalEvent) +{ + const qreal fRatio = rFrame.devicePixelRatioF(); + const Point aPos = toPoint(rPos * fRatio); + + aSalEvent.mnX = QGuiApplication::isLeftToRight() ? aPos.X() : round(nWidth * fRatio) - aPos.X(); + aSalEvent.mnY = aPos.Y(); + aSalEvent.mnTime = pQEvent->timestamp(); + aSalEvent.mnCode = GetKeyModCode(pQEvent->modifiers()) | GetMouseModCode(eButtons); +} + +#define FILL_SAME(rFrame, nWidth) \ + fillSalAbstractMouseEvent(rFrame, pEvent, pEvent->pos(), pEvent->buttons(), nWidth, aEvent) + +void QtWidget::handleMouseButtonEvent(const QtFrame& rFrame, const QMouseEvent* pEvent) +{ + SalMouseEvent aEvent; + FILL_SAME(rFrame, rFrame.GetQWidget()->width()); + + switch (pEvent->button()) + { + case Qt::LeftButton: + aEvent.mnButton = MOUSE_LEFT; + break; + case Qt::MiddleButton: + aEvent.mnButton = MOUSE_MIDDLE; + break; + case Qt::RightButton: + aEvent.mnButton = MOUSE_RIGHT; + break; + default: + return; + } + + SalEvent nEventType; + if (pEvent->type() == QEvent::MouseButtonPress || pEvent->type() == QEvent::MouseButtonDblClick) + nEventType = SalEvent::MouseButtonDown; + else + nEventType = SalEvent::MouseButtonUp; + rFrame.CallCallback(nEventType, &aEvent); +} + +void QtWidget::mousePressEvent(QMouseEvent* pEvent) +{ + handleMouseButtonEvent(m_rFrame, pEvent); + if (m_rFrame.isPopup() + && !geometry().translated(geometry().topLeft() * -1).contains(pEvent->pos())) + closePopup(); +} + +void QtWidget::mouseReleaseEvent(QMouseEvent* pEvent) { handleMouseButtonEvent(m_rFrame, pEvent); } + +void QtWidget::mouseMoveEvent(QMouseEvent* pEvent) +{ + SalMouseEvent aEvent; + FILL_SAME(m_rFrame, width()); + + aEvent.mnButton = 0; + + m_rFrame.CallCallback(SalEvent::MouseMove, &aEvent); + pEvent->accept(); +} + +void QtWidget::handleMouseEnterLeaveEvents(const QtFrame& rFrame, QEvent* pQEvent) +{ + const qreal fRatio = rFrame.devicePixelRatioF(); + const QWidget* pWidget = rFrame.GetQWidget(); + const Point aPos = toPoint(pWidget->mapFromGlobal(QCursor::pos()) * fRatio); + + SalMouseEvent aEvent; + aEvent.mnX + = QGuiApplication::isLeftToRight() ? aPos.X() : round(pWidget->width() * fRatio) - aPos.X(); + aEvent.mnY = aPos.Y(); + aEvent.mnTime = 0; + aEvent.mnButton = 0; + aEvent.mnCode = GetKeyModCode(QGuiApplication::keyboardModifiers()) + | GetMouseModCode(QGuiApplication::mouseButtons()); + + SalEvent nEventType; + if (pQEvent->type() == QEvent::Enter) + nEventType = SalEvent::MouseMove; + else + nEventType = SalEvent::MouseLeave; + rFrame.CallCallback(nEventType, &aEvent); + pQEvent->accept(); +} + +void QtWidget::leaveEvent(QEvent* pEvent) { handleMouseEnterLeaveEvents(m_rFrame, pEvent); } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void QtWidget::enterEvent(QEnterEvent* pEvent) +#else +void QtWidget::enterEvent(QEvent* pEvent) +#endif +{ + handleMouseEnterLeaveEvents(m_rFrame, pEvent); +} + +void QtWidget::wheelEvent(QWheelEvent* pEvent) +{ + SalWheelMouseEvent aEvent; + fillSalAbstractMouseEvent(m_rFrame, pEvent, pEvent->position().toPoint(), pEvent->buttons(), + width(), aEvent); + + // mouse wheel ticks are 120, which we map to 3 lines. + // we have to accumulate for touch scroll to keep track of the absolute delta. + + int nDelta = pEvent->angleDelta().y(), lines; + aEvent.mbHorz = nDelta == 0; + if (aEvent.mbHorz) + { + nDelta = (QGuiApplication::isLeftToRight() ? 1 : -1) * pEvent->angleDelta().x(); + if (!nDelta) + return; + + m_nDeltaX += nDelta; + lines = m_nDeltaX / 40; + m_nDeltaX = m_nDeltaX % 40; + } + else + { + m_nDeltaY += nDelta; + lines = m_nDeltaY / 40; + m_nDeltaY = m_nDeltaY % 40; + } + + aEvent.mnDelta = nDelta; + aEvent.mnNotchDelta = nDelta < 0 ? -1 : 1; + aEvent.mnScrollLines = std::abs(lines); + + m_rFrame.CallCallback(SalEvent::WheelMouse, &aEvent); + pEvent->accept(); +} + +void QtWidget::dragEnterEvent(QDragEnterEvent* event) +{ + if (dynamic_cast<const QtMimeData*>(event->mimeData())) + event->accept(); + else + event->acceptProposedAction(); +} + +// also called when a drop is rejected +void QtWidget::dragLeaveEvent(QDragLeaveEvent*) { m_rFrame.handleDragLeave(); } + +void QtWidget::dragMoveEvent(QDragMoveEvent* pEvent) { m_rFrame.handleDragMove(pEvent); } + +void QtWidget::dropEvent(QDropEvent* pEvent) { m_rFrame.handleDrop(pEvent); } + +void QtWidget::moveEvent(QMoveEvent* pEvent) +{ + // already handled by QtMainWindow::moveEvent + if (m_rFrame.m_pTopLevel) + return; + + m_rFrame.maGeometry.setPos(toPoint(pEvent->pos() * m_rFrame.devicePixelRatioF())); + m_rFrame.CallCallback(SalEvent::Move, nullptr); +} + +void QtWidget::showEvent(QShowEvent*) +{ + QSize aSize(m_rFrame.GetQWidget()->size() * m_rFrame.devicePixelRatioF()); + // forcing an immediate update somehow interferes with the hide + show + // sequence from QtFrame::SetModal, if the frame was already set visible, + // resulting in a hidden / unmapped window + SalPaintEvent aPaintEvt(0, 0, aSize.width(), aSize.height()); + if (m_rFrame.isPopup()) + GetQtInstance()->setActivePopup(&m_rFrame); + m_rFrame.CallCallback(SalEvent::Paint, &aPaintEvt); +} + +void QtWidget::hideEvent(QHideEvent*) +{ + if (m_rFrame.isPopup() && GetQtInstance()->activePopup() == &m_rFrame) + GetQtInstance()->setActivePopup(nullptr); +} + +void QtWidget::closeEvent(QCloseEvent* /*pEvent*/) +{ + m_rFrame.CallCallback(SalEvent::Close, nullptr); +} + +static sal_uInt16 GetKeyCode(int keyval, Qt::KeyboardModifiers modifiers) +{ + sal_uInt16 nCode = 0; + if (keyval >= Qt::Key_0 && keyval <= Qt::Key_9) + nCode = KEY_0 + (keyval - Qt::Key_0); + else if (keyval >= Qt::Key_A && keyval <= Qt::Key_Z) + nCode = KEY_A + (keyval - Qt::Key_A); + else if (keyval >= Qt::Key_F1 && keyval <= Qt::Key_F26) + nCode = KEY_F1 + (keyval - Qt::Key_F1); + else if (modifiers.testFlag(Qt::KeypadModifier) + && (keyval == Qt::Key_Period || keyval == Qt::Key_Comma)) + // Qt doesn't use a special keyval for decimal separator ("," or ".") + // on numerical keypad, but sets Qt::KeypadModifier in addition + nCode = KEY_DECIMAL; + else + { + switch (keyval) + { + case Qt::Key_Down: + nCode = KEY_DOWN; + break; + case Qt::Key_Up: + nCode = KEY_UP; + break; + case Qt::Key_Left: + nCode = KEY_LEFT; + break; + case Qt::Key_Right: + nCode = KEY_RIGHT; + break; + case Qt::Key_Home: + nCode = KEY_HOME; + break; + case Qt::Key_End: + nCode = KEY_END; + break; + case Qt::Key_PageUp: + nCode = KEY_PAGEUP; + break; + case Qt::Key_PageDown: + nCode = KEY_PAGEDOWN; + break; + case Qt::Key_Return: + case Qt::Key_Enter: + nCode = KEY_RETURN; + break; + case Qt::Key_Escape: + nCode = KEY_ESCAPE; + break; + case Qt::Key_Tab: + // oddly enough, Qt doesn't send Shift-Tab event as 'Tab key pressed with Shift + // modifier' but as 'Backtab key pressed' (while its modifier bits are still + // set to Shift) -- so let's map both Key_Tab and Key_Backtab to VCL's KEY_TAB + case Qt::Key_Backtab: + nCode = KEY_TAB; + break; + case Qt::Key_Backspace: + nCode = KEY_BACKSPACE; + break; + case Qt::Key_Space: + nCode = KEY_SPACE; + break; + case Qt::Key_Insert: + nCode = KEY_INSERT; + break; + case Qt::Key_Delete: + nCode = KEY_DELETE; + break; + case Qt::Key_Plus: + nCode = KEY_ADD; + break; + case Qt::Key_Minus: + nCode = KEY_SUBTRACT; + break; + case Qt::Key_Asterisk: + nCode = KEY_MULTIPLY; + break; + case Qt::Key_Slash: + nCode = KEY_DIVIDE; + break; + case Qt::Key_Period: + nCode = KEY_POINT; + break; + case Qt::Key_Comma: + nCode = KEY_COMMA; + break; + case Qt::Key_Less: + nCode = KEY_LESS; + break; + case Qt::Key_Greater: + nCode = KEY_GREATER; + break; + case Qt::Key_Equal: + nCode = KEY_EQUAL; + break; + case Qt::Key_Find: + nCode = KEY_FIND; + break; + case Qt::Key_Menu: + nCode = KEY_CONTEXTMENU; + break; + case Qt::Key_Help: + nCode = KEY_HELP; + break; + case Qt::Key_Undo: + nCode = KEY_UNDO; + break; + case Qt::Key_Redo: + nCode = KEY_REPEAT; + break; + case Qt::Key_Cancel: + nCode = KEY_F11; + break; + case Qt::Key_AsciiTilde: + nCode = KEY_TILDE; + break; + case Qt::Key_QuoteLeft: + nCode = KEY_QUOTELEFT; + break; + case Qt::Key_BracketLeft: + nCode = KEY_BRACKETLEFT; + break; + case Qt::Key_BracketRight: + nCode = KEY_BRACKETRIGHT; + break; + case Qt::Key_NumberSign: + nCode = KEY_NUMBERSIGN; + break; + case Qt::Key_Forward: + nCode = KEY_XF86FORWARD; + break; + case Qt::Key_Back: + nCode = KEY_XF86BACK; + break; + case Qt::Key_Colon: + nCode = KEY_COLON; + break; + case Qt::Key_Semicolon: + nCode = KEY_SEMICOLON; + break; + case Qt::Key_Copy: + nCode = KEY_COPY; + break; + case Qt::Key_Cut: + nCode = KEY_CUT; + break; + case Qt::Key_Open: + nCode = KEY_OPEN; + break; + case Qt::Key_Paste: + nCode = KEY_PASTE; + break; + } + } + + return nCode; +} + +void QtWidget::commitText(QtFrame& rFrame, const QString& aText) +{ + SalExtTextInputEvent aInputEvent; + aInputEvent.mpTextAttr = nullptr; + aInputEvent.mnCursorFlags = 0; + aInputEvent.maText = toOUString(aText); + aInputEvent.mnCursorPos = aInputEvent.maText.getLength(); + + SolarMutexGuard aGuard; + vcl::DeletionListener aDel(&rFrame); + rFrame.CallCallback(SalEvent::ExtTextInput, &aInputEvent); + if (!aDel.isDeleted()) + rFrame.CallCallback(SalEvent::EndExtTextInput, nullptr); +} + +void QtWidget::deleteReplacementText(QtFrame& rFrame, int nReplacementStart, int nReplacementLength) +{ + // get the surrounding text + SolarMutexGuard aGuard; + SalSurroundingTextRequestEvent aSurroundingTextEvt; + aSurroundingTextEvt.maText.clear(); + aSurroundingTextEvt.mnStart = aSurroundingTextEvt.mnEnd = 0; + rFrame.CallCallback(SalEvent::SurroundingTextRequest, &aSurroundingTextEvt); + + // Turn nReplacementStart, nReplacementLength into a UTF-16 selection + const Selection aSelection = SalFrame::CalcDeleteSurroundingSelection( + aSurroundingTextEvt.maText, aSurroundingTextEvt.mnStart, nReplacementStart, + nReplacementLength); + + const Selection aInvalid(SAL_MAX_UINT32, SAL_MAX_UINT32); + if (aSelection == aInvalid) + { + SAL_WARN("vcl.qt", "Invalid selection when deleting IM replacement text"); + return; + } + + SalSurroundingTextSelectionChangeEvent aEvt; + aEvt.mnStart = aSelection.Min(); + aEvt.mnEnd = aSelection.Max(); + rFrame.CallCallback(SalEvent::DeleteSurroundingTextRequest, &aEvt); +} + +bool QtWidget::handleGestureEvent(QtFrame& rFrame, QGestureEvent* pGestureEvent) +{ + if (QGesture* pGesture = pGestureEvent->gesture(Qt::PinchGesture)) + { + if (!pGesture->hasHotSpot()) + { + pGestureEvent->ignore(); + return false; + } + + GestureEventZoomType eType = GestureEventZoomType::Begin; + switch (pGesture->state()) + { + case Qt::GestureStarted: + eType = GestureEventZoomType::Begin; + break; + case Qt::GestureUpdated: + eType = GestureEventZoomType::Update; + break; + case Qt::GestureFinished: + eType = GestureEventZoomType::End; + break; + case Qt::NoGesture: + case Qt::GestureCanceled: + default: + SAL_WARN("vcl.qt", "Unhandled pinch gesture state: " << pGesture->state()); + pGestureEvent->ignore(); + return false; + } + + QPinchGesture* pPinchGesture = static_cast<QPinchGesture*>(pGesture); + const QPointF aHotspot = pGesture->hotSpot(); + SalGestureZoomEvent aEvent; + aEvent.meEventType = eType; + aEvent.mnX = aHotspot.x(); + aEvent.mnY = aHotspot.y(); + aEvent.mfScaleDelta = 1 + pPinchGesture->totalScaleFactor(); + rFrame.CallCallback(SalEvent::GestureZoom, &aEvent); + pGestureEvent->accept(); + return true; + } + + pGestureEvent->ignore(); + return false; +} + +bool QtWidget::handleKeyEvent(QtFrame& rFrame, const QWidget& rWidget, QKeyEvent* pEvent) +{ + const bool bIsKeyPressed + = pEvent->type() == QEvent::KeyPress || pEvent->type() == QEvent::ShortcutOverride; + sal_uInt16 nCode = GetKeyCode(pEvent->key(), pEvent->modifiers()); + if (bIsKeyPressed && nCode == 0 && pEvent->text().length() > 1 + && rWidget.testAttribute(Qt::WA_InputMethodEnabled)) + { + commitText(rFrame, pEvent->text()); + pEvent->accept(); + return true; + } + + if (nCode == 0 && pEvent->text().isEmpty()) + { + sal_uInt16 nModCode = GetKeyModCode(pEvent->modifiers()); + SalKeyModEvent aModEvt; + aModEvt.mbDown = bIsKeyPressed; + aModEvt.mnModKeyCode = ModKeyFlags::NONE; + +#if CHECK_ANY_QT_USING_X11 + if (QGuiApplication::platformName() == "xcb") + { + // pressing just the ctrl key leads to a keysym of XK_Control but + // the event state does not contain ControlMask. In the release + // event it's the other way round: it does contain the Control mask. + // The modifier mode therefore has to be adapted manually. + ModKeyFlags nExtModMask = ModKeyFlags::NONE; + sal_uInt16 nModMask = 0; + switch (pEvent->nativeVirtualKey()) + { + case XK_Control_L: + nExtModMask = ModKeyFlags::LeftMod1; + nModMask = KEY_MOD1; + break; + case XK_Control_R: + nExtModMask = ModKeyFlags::RightMod1; + nModMask = KEY_MOD1; + break; + case XK_Alt_L: + nExtModMask = ModKeyFlags::LeftMod2; + nModMask = KEY_MOD2; + break; + case XK_Alt_R: + nExtModMask = ModKeyFlags::RightMod2; + nModMask = KEY_MOD2; + break; + case XK_Shift_L: + nExtModMask = ModKeyFlags::LeftShift; + nModMask = KEY_SHIFT; + break; + case XK_Shift_R: + nExtModMask = ModKeyFlags::RightShift; + nModMask = KEY_SHIFT; + break; + // Map Meta/Super keys to MOD3 modifier on all Unix systems + // except macOS + case XK_Meta_L: + case XK_Super_L: + nExtModMask = ModKeyFlags::LeftMod3; + nModMask = KEY_MOD3; + break; + case XK_Meta_R: + case XK_Super_R: + nExtModMask = ModKeyFlags::RightMod3; + nModMask = KEY_MOD3; + break; + } + + if (!bIsKeyPressed) + { + // sending the old mnModKeyCode mask on release is needed to + // implement the writing direction switch with Ctrl + L/R-Shift + aModEvt.mnModKeyCode = rFrame.m_nKeyModifiers; + nModCode &= ~nModMask; + rFrame.m_nKeyModifiers &= ~nExtModMask; + } + else + { + nModCode |= nModMask; + rFrame.m_nKeyModifiers |= nExtModMask; + aModEvt.mnModKeyCode = rFrame.m_nKeyModifiers; + } + } +#endif + aModEvt.mnCode = nModCode; + + rFrame.CallCallback(SalEvent::KeyModChange, &aModEvt); + return false; + } + +#if CHECK_ANY_QT_USING_X11 + // prevent interference of writing direction switch (Ctrl + L/R-Shift) with "normal" shortcuts + rFrame.m_nKeyModifiers = ModKeyFlags::NONE; +#endif + + SalKeyEvent aEvent; + aEvent.mnCharCode = (pEvent->text().isEmpty() ? 0 : pEvent->text().at(0).unicode()); + aEvent.mnRepeat = 0; + aEvent.mnCode = nCode; + aEvent.mnCode |= GetKeyModCode(pEvent->modifiers()); + + QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle); + + bool bStopProcessingKey; + if (bIsKeyPressed) + bStopProcessingKey = rFrame.CallCallback(SalEvent::KeyInput, &aEvent); + else + bStopProcessingKey = rFrame.CallCallback(SalEvent::KeyUp, &aEvent); + if (bStopProcessingKey) + pEvent->accept(); + return bStopProcessingKey; +} + +bool QtWidget::handleEvent(QtFrame& rFrame, QWidget& rWidget, QEvent* pEvent) +{ + if (pEvent->type() == QEvent::Gesture) + { + QGestureEvent* pGestureEvent = static_cast<QGestureEvent*>(pEvent); + return handleGestureEvent(rFrame, pGestureEvent); + } + else if (pEvent->type() == QEvent::ShortcutOverride) + { + // ignore non-spontaneous QEvent::ShortcutOverride events, + // since such an extra event is sent e.g. with Orca screen reader enabled, + // so that two events of that kind (the "real one" and a non-spontaneous one) + // would otherwise be processed, resulting in duplicate input as 'handleKeyEvent' + // is called below (s. tdf#122053) + if (!pEvent->spontaneous()) + { + return false; + } + + // Accepted event disables shortcut activation, + // but enables keypress event. + // If event is not accepted and shortcut is successfully activated, + // KeyPress event is omitted. + // + // Instead of processing keyPressEvent, handle ShortcutOverride event, + // and if it's handled - disable the shortcut, it should have been activated. + // Don't process keyPressEvent generated after disabling shortcut since it was handled here. + // If event is not handled, don't accept it and let Qt activate related shortcut. + if (handleKeyEvent(rFrame, rWidget, static_cast<QKeyEvent*>(pEvent))) + return true; + } + else if (pEvent->type() == QEvent::ToolTip) + { + // Qt's POV on the active popup is wrong due to our fake popup, so check LO's state. + // Otherwise Qt will continue handling ToolTip events from the "parent" window. + const QtFrame* pPopupFrame = GetQtInstance()->activePopup(); + if (!rFrame.m_aTooltipText.isEmpty() && (!pPopupFrame || pPopupFrame == &rFrame)) + QToolTip::showText(QCursor::pos(), toQString(rFrame.m_aTooltipText), &rWidget, + rFrame.m_aTooltipArea); + else + { + QToolTip::hideText(); + pEvent->ignore(); + } + return true; + } + return false; +} + +bool QtWidget::event(QEvent* pEvent) +{ + return handleEvent(m_rFrame, *this, pEvent) || QWidget::event(pEvent); +} + +void QtWidget::keyReleaseEvent(QKeyEvent* pEvent) +{ + if (!handleKeyReleaseEvent(m_rFrame, *this, pEvent)) + QWidget::keyReleaseEvent(pEvent); +} + +void QtWidget::focusInEvent(QFocusEvent*) { m_rFrame.CallCallback(SalEvent::GetFocus, nullptr); } + +void QtWidget::closePopup() +{ + VclPtr<FloatingWindow> pFirstFloat = ImplGetSVData()->mpWinData->mpFirstFloat; + if (pFirstFloat && !(pFirstFloat->GetPopupModeFlags() & FloatWinPopupFlags::NoAppFocusClose)) + { + SolarMutexGuard aGuard; + pFirstFloat->EndPopupMode(FloatWinPopupEndFlags::Cancel | FloatWinPopupEndFlags::CloseAll); + } +} + +void QtWidget::focusOutEvent(QFocusEvent*) +{ +#if CHECK_ANY_QT_USING_X11 + m_rFrame.m_nKeyModifiers = ModKeyFlags::NONE; +#endif + endExtTextInput(); + m_rFrame.CallCallback(SalEvent::LoseFocus, nullptr); + closePopup(); +} + +QtWidget::QtWidget(QtFrame& rFrame, Qt::WindowFlags f) + // if you try to set the QWidget parent via the QtFrame, instead of using the Q_NULLPTR, at + // least test Wayland popups; these horribly broke last time doing this (read commits)! + : QWidget(Q_NULLPTR, f) + , m_rFrame(rFrame) + , m_bNonEmptyIMPreeditSeen(false) + , m_bInInputMethodQueryCursorRectangle(false) + , m_nDeltaX(0) + , m_nDeltaY(0) +{ + setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_OpaquePaintEvent); + setAttribute(Qt::WA_NoSystemBackground); + setMouseTracking(true); + if (!rFrame.isPopup()) + setFocusPolicy(Qt::StrongFocus); + else + setFocusPolicy(Qt::ClickFocus); + + grabGesture(Qt::PinchGesture); +} + +static ExtTextInputAttr lcl_MapUnderlineStyle(QTextCharFormat::UnderlineStyle us) +{ + switch (us) + { + case QTextCharFormat::NoUnderline: + return ExtTextInputAttr::NONE; + case QTextCharFormat::DotLine: + return ExtTextInputAttr::DottedUnderline; + case QTextCharFormat::DashDotDotLine: + case QTextCharFormat::DashDotLine: + return ExtTextInputAttr::DashDotUnderline; + case QTextCharFormat::WaveUnderline: + return ExtTextInputAttr::GrayWaveline; + default: + return ExtTextInputAttr::Underline; + } +} + +void QtWidget::inputMethodEvent(QInputMethodEvent* pEvent) +{ + const bool bHasCommitText = !pEvent->commitString().isEmpty(); + const int nReplacementLength = pEvent->replacementLength(); + + if (nReplacementLength > 0 || bHasCommitText) + { + if (nReplacementLength > 0) + deleteReplacementText(m_rFrame, pEvent->replacementStart(), nReplacementLength); + if (bHasCommitText) + commitText(m_rFrame, pEvent->commitString()); + } + else + { + SalExtTextInputEvent aInputEvent; + aInputEvent.mpTextAttr = nullptr; + aInputEvent.mnCursorFlags = 0; + aInputEvent.maText = toOUString(pEvent->preeditString()); + aInputEvent.mnCursorPos = 0; + + const sal_Int32 nLength = aInputEvent.maText.getLength(); + const QList<QInputMethodEvent::Attribute>& rAttrList = pEvent->attributes(); + std::vector<ExtTextInputAttr> aTextAttrs(std::max(sal_Int32(1), nLength), + ExtTextInputAttr::NONE); + aInputEvent.mpTextAttr = aTextAttrs.data(); + + for (const QInputMethodEvent::Attribute& rAttr : rAttrList) + { + switch (rAttr.type) + { + case QInputMethodEvent::TextFormat: + { + QTextCharFormat aCharFormat + = qvariant_cast<QTextFormat>(rAttr.value).toCharFormat(); + if (aCharFormat.isValid()) + { + ExtTextInputAttr aETIP + = lcl_MapUnderlineStyle(aCharFormat.underlineStyle()); + if (aCharFormat.hasProperty(QTextFormat::BackgroundBrush)) + aETIP |= ExtTextInputAttr::Highlight; + if (aCharFormat.fontStrikeOut()) + aETIP |= ExtTextInputAttr::RedText; + for (int j = rAttr.start; j < rAttr.start + rAttr.length; j++) + { + SAL_WARN_IF(j >= static_cast<int>(aTextAttrs.size()), "vcl.qt", + "QInputMethodEvent::Attribute out of range. Broken range: " + << rAttr.start << "," << rAttr.start + rAttr.length + << " Legal range: 0," << aTextAttrs.size()); + if (j >= static_cast<int>(aTextAttrs.size())) + break; + aTextAttrs[j] = aETIP; + } + } + break; + } + case QInputMethodEvent::Cursor: + { + aInputEvent.mnCursorPos = rAttr.start; + if (rAttr.length == 0) + aInputEvent.mnCursorFlags |= EXTTEXTINPUT_CURSOR_INVISIBLE; + break; + } + default: + SAL_WARN("vcl.qt", "Unhandled QInputMethodEvent attribute: " + << static_cast<int>(rAttr.type)); + break; + } + } + + const bool bIsEmpty = aInputEvent.maText.isEmpty(); + if (m_bNonEmptyIMPreeditSeen || !bIsEmpty) + { + SolarMutexGuard aGuard; + vcl::DeletionListener aDel(&m_rFrame); + m_rFrame.CallCallback(SalEvent::ExtTextInput, &aInputEvent); + if (!aDel.isDeleted() && bIsEmpty) + m_rFrame.CallCallback(SalEvent::EndExtTextInput, nullptr); + m_bNonEmptyIMPreeditSeen = !bIsEmpty; + } + } + + pEvent->accept(); +} + +static bool lcl_retrieveSurrounding(sal_Int32& rPosition, sal_Int32& rAnchor, QString* pText, + QString* pSelection) +{ + SolarMutexGuard aGuard; + vcl::Window* pFocusWin = Application::GetFocusWindow(); + if (!pFocusWin) + return false; + + uno::Reference<accessibility::XAccessibleEditableText> xText; + try + { + uno::Reference<accessibility::XAccessible> xAccessible(pFocusWin->GetAccessible()); + if (xAccessible.is()) + xText = FindFocusedEditableText(xAccessible->getAccessibleContext()); + } + catch (const uno::Exception&) + { + TOOLS_WARN_EXCEPTION("vcl.qt", "Exception in getting input method surrounding text"); + } + + if (xText.is()) + { + rPosition = xText->getCaretPosition(); + if (rPosition != -1) + { + if (pText) + *pText = toQString(xText->getText()); + + sal_Int32 nSelStart = xText->getSelectionStart(); + sal_Int32 nSelEnd = xText->getSelectionEnd(); + if (nSelStart == nSelEnd) + { + rAnchor = rPosition; + } + else + { + if (rPosition == nSelStart) + rAnchor = nSelEnd; + else + rAnchor = nSelStart; + if (pSelection) + *pSelection = toQString(xText->getSelectedText()); + } + return true; + } + } + + return false; +} + +QVariant QtWidget::inputMethodQuery(Qt::InputMethodQuery property) const +{ + switch (property) + { + case Qt::ImSurroundingText: + { + QString aText; + sal_Int32 nCursorPos, nAnchor; + if (lcl_retrieveSurrounding(nCursorPos, nAnchor, &aText, nullptr)) + return QVariant(aText); + return QVariant(); + } + case Qt::ImCursorPosition: + { + sal_Int32 nCursorPos, nAnchor; + if (lcl_retrieveSurrounding(nCursorPos, nAnchor, nullptr, nullptr)) + return QVariant(static_cast<int>(nCursorPos)); + return QVariant(); + } + case Qt::ImCursorRectangle: + { + if (!m_bInInputMethodQueryCursorRectangle) + { + m_bInInputMethodQueryCursorRectangle = true; + SalExtTextInputPosEvent aPosEvent; + m_rFrame.CallCallback(SalEvent::ExtTextInputPos, &aPosEvent); + const qreal fRatio = m_rFrame.devicePixelRatioF(); + m_aImCursorRectangle.setRect(aPosEvent.mnX / fRatio, aPosEvent.mnY / fRatio, + aPosEvent.mnWidth / fRatio, + aPosEvent.mnHeight / fRatio); + m_bInInputMethodQueryCursorRectangle = false; + } + return QVariant(m_aImCursorRectangle); + } + case Qt::ImAnchorPosition: + { + sal_Int32 nCursorPos, nAnchor; + if (lcl_retrieveSurrounding(nCursorPos, nAnchor, nullptr, nullptr)) + return QVariant(static_cast<int>(nAnchor)); + return QVariant(); + } + case Qt::ImCurrentSelection: + { + QString aSelection; + sal_Int32 nCursorPos, nAnchor; + if (lcl_retrieveSurrounding(nCursorPos, nAnchor, nullptr, &aSelection)) + return QVariant(aSelection); + return QVariant(); + } + default: + return QWidget::inputMethodQuery(property); + } +} + +void QtWidget::endExtTextInput() +{ + if (m_bNonEmptyIMPreeditSeen) + { + m_rFrame.CallCallback(SalEvent::EndExtTextInput, nullptr); + m_bNonEmptyIMPreeditSeen = false; + } +} + +void QtWidget::changeEvent(QEvent* pEvent) +{ + switch (pEvent->type()) + { + case QEvent::FontChange: + [[fallthrough]]; + case QEvent::PaletteChange: + [[fallthrough]]; + case QEvent::StyleChange: + { + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + pSalInst->UpdateStyle(QEvent::FontChange == pEvent->type()); + break; + } + default: + break; + } + QWidget::changeEvent(pEvent); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtX11Support.cxx b/vcl/qt5/QtX11Support.cxx new file mode 100644 index 0000000000..84036fb8a5 --- /dev/null +++ b/vcl/qt5/QtX11Support.cxx @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <QtX11Support.hxx> + +#include <config_vclplug.h> + +#include <QtCore/QVersionNumber> + +#include <QtInstance.hxx> +#include <QtTools.hxx> + +#if CHECK_QT5_USING_X11 +#include <QtX11Extras/QX11Info> +#endif + +#include <unx/gensys.h> + +void QtX11Support::setApplicationID(const xcb_window_t nWinId, std::u16string_view rWMClass) +{ +#if CHECK_QT5_USING_X11 + OString aResClass = OUStringToOString(rWMClass, RTL_TEXTENCODING_ASCII_US); + const char* pResClass + = !aResClass.isEmpty() ? aResClass.getStr() : SalGenericSystem::getFrameClassName(); + OString aResName = SalGenericSystem::getFrameResName(); + + // the WM_CLASS data consists of two concatenated cstrings, including the terminating '\0' chars + const uint32_t data_len = aResName.getLength() + 1 + strlen(pResClass) + 1; + char* data = new char[data_len]; + memcpy(data, aResName.getStr(), aResName.getLength() + 1); + memcpy(data + aResName.getLength() + 1, pResClass, strlen(pResClass) + 1); + + xcb_change_property(QX11Info::connection(), XCB_PROP_MODE_REPLACE, nWinId, XCB_ATOM_WM_CLASS, + XCB_ATOM_STRING, 8, data_len, data); + delete[] data; +#else + Q_UNUSED(nWinId); + Q_UNUSED(rWMClass); +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/qt5/QtXAccessible.cxx b/vcl/qt5/QtXAccessible.cxx new file mode 100644 index 0000000000..25c0c5e9e1 --- /dev/null +++ b/vcl/qt5/QtXAccessible.cxx @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <QtXAccessible.hxx> +#include <QtXAccessible.moc> + +#include <QtFrame.hxx> +#include <QtTools.hxx> +#include <QtWidget.hxx> + +#include <com/sun/star/accessibility/XAccessible.hpp> + +#include <sal/log.hxx> +#include <utility> + +using namespace css::accessibility; +using namespace css::uno; + +QtXAccessible::QtXAccessible(Reference<XAccessible> xAccessible) + : m_xAccessible(std::move(xAccessible)) +{ +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |