diff options
Diffstat (limited to 'svtools/source/table')
-rw-r--r-- | svtools/source/table/cellvalueconversion.cxx | 376 | ||||
-rw-r--r-- | svtools/source/table/cellvalueconversion.hxx | 72 | ||||
-rw-r--r-- | svtools/source/table/defaultinputhandler.cxx | 186 | ||||
-rw-r--r-- | svtools/source/table/gridtablerenderer.cxx | 598 | ||||
-rw-r--r-- | svtools/source/table/mousefunction.cxx | 274 | ||||
-rw-r--r-- | svtools/source/table/tablecontrol.cxx | 639 | ||||
-rw-r--r-- | svtools/source/table/tablecontrol_impl.cxx | 2552 | ||||
-rw-r--r-- | svtools/source/table/tablecontrol_impl.hxx | 479 | ||||
-rw-r--r-- | svtools/source/table/tabledatawindow.cxx | 200 | ||||
-rw-r--r-- | svtools/source/table/tabledatawindow.hxx | 66 | ||||
-rw-r--r-- | svtools/source/table/tablegeometry.cxx | 154 | ||||
-rw-r--r-- | svtools/source/table/tablegeometry.hxx | 158 |
12 files changed, 5754 insertions, 0 deletions
diff --git a/svtools/source/table/cellvalueconversion.cxx b/svtools/source/table/cellvalueconversion.cxx new file mode 100644 index 000000000..53177af0d --- /dev/null +++ b/svtools/source/table/cellvalueconversion.cxx @@ -0,0 +1,376 @@ +/* -*- 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 "cellvalueconversion.hxx" + +#include <com/sun/star/util/NumberFormatsSupplier.hpp> +#include <com/sun/star/util/NumberFormatter.hpp> +#include <com/sun/star/beans/XPropertySet.hpp> +#include <com/sun/star/util/Date.hpp> +#include <com/sun/star/util/Time.hpp> +#include <com/sun/star/util/DateTime.hpp> +#include <com/sun/star/util/XNumberFormatTypes.hpp> +#include <com/sun/star/util/NumberFormat.hpp> +#include <sal/log.hxx> +#include <tools/date.hxx> +#include <tools/time.hxx> +#include <tools/diagnose_ex.h> +#include <tools/long.hxx> +#include <unotools/syslocale.hxx> +#include <i18nlangtag/languagetag.hxx> +#include <comphelper/processfactory.hxx> + +#include <limits> +#include <memory> + +namespace svt +{ +using namespace ::com::sun::star::uno; +using ::com::sun::star::util::XNumberFormatter; +using ::com::sun::star::util::NumberFormatter; +using ::com::sun::star::util::XNumberFormatsSupplier; +using ::com::sun::star::util::NumberFormatsSupplier; +using ::com::sun::star::beans::XPropertySet; +using ::com::sun::star::lang::Locale; +using ::com::sun::star::util::DateTime; +using ::com::sun::star::util::XNumberFormatTypes; + +namespace NumberFormat = ::com::sun::star::util::NumberFormat; + +//= helper + +namespace +{ +double lcl_convertDateToDays(sal_uInt16 const i_day, sal_uInt16 const i_month, + sal_Int16 const i_year) +{ + tools::Long const nNullDateDays = ::Date::DateToDays(1, 1, 1900); + tools::Long const nValueDateDays = ::Date::DateToDays(i_day, i_month, i_year); + + return nValueDateDays - nNullDateDays; +} + +double lcl_convertTimeToDays(tools::Long const i_hours, tools::Long const i_minutes, + tools::Long const i_seconds, tools::Long const i_100thSeconds) +{ + return tools::Time(i_hours, i_minutes, i_seconds, i_100thSeconds).GetTimeInDays(); +} +} + +//= StandardFormatNormalizer + +StandardFormatNormalizer::StandardFormatNormalizer(Reference<XNumberFormatter> const& i_formatter, + ::sal_Int32 const i_numberFormatType) + : m_nFormatKey(0) +{ + try + { + ENSURE_OR_THROW(i_formatter.is(), "StandardFormatNormalizer: no formatter!"); + Reference<XNumberFormatsSupplier> const xSupplier(i_formatter->getNumberFormatsSupplier(), + UNO_SET_THROW); + Reference<XNumberFormatTypes> const xTypes(xSupplier->getNumberFormats(), UNO_QUERY_THROW); + m_nFormatKey = xTypes->getStandardFormat(i_numberFormatType, + SvtSysLocale().GetLanguageTag().getLocale()); + } + catch (const Exception&) + { + DBG_UNHANDLED_EXCEPTION("svtools.table"); + } +} + +//= DoubleNormalization + +namespace +{ +class DoubleNormalization : public StandardFormatNormalizer +{ +public: + explicit DoubleNormalization(Reference<XNumberFormatter> const& i_formatter) + : StandardFormatNormalizer(i_formatter, NumberFormat::NUMBER) + { + } + + virtual double convertToDouble(Any const& i_value) const override + { + double returnValue = std::numeric_limits<double>::quiet_NaN(); + OSL_VERIFY(i_value >>= returnValue); + return returnValue; + } +}; + +//= IntegerNormalization + +class IntegerNormalization : public StandardFormatNormalizer +{ +public: + explicit IntegerNormalization(Reference<XNumberFormatter> const& i_formatter) + : StandardFormatNormalizer(i_formatter, NumberFormat::NUMBER) + { + } + + virtual double convertToDouble(Any const& i_value) const override + { + sal_Int64 value(0); + OSL_VERIFY(i_value >>= value); + return value; + } +}; + +//= BooleanNormalization + +class BooleanNormalization : public StandardFormatNormalizer +{ +public: + explicit BooleanNormalization(Reference<XNumberFormatter> const& i_formatter) + : StandardFormatNormalizer(i_formatter, NumberFormat::LOGICAL) + { + } + + virtual double convertToDouble(Any const& i_value) const override + { + bool value(false); + OSL_VERIFY(i_value >>= value); + return value ? 1 : 0; + } +}; + +//= DateTimeNormalization + +class DateTimeNormalization : public StandardFormatNormalizer +{ +public: + explicit DateTimeNormalization(Reference<XNumberFormatter> const& i_formatter) + : StandardFormatNormalizer(i_formatter, NumberFormat::DATETIME) + { + } + + virtual double convertToDouble(Any const& i_value) const override + { + double returnValue = std::numeric_limits<double>::quiet_NaN(); + + // extract actual UNO value + DateTime aDateTimeValue; + ENSURE_OR_RETURN(i_value >>= aDateTimeValue, "allowed for DateTime values only", + returnValue); + + // date part + returnValue + = lcl_convertDateToDays(aDateTimeValue.Day, aDateTimeValue.Month, aDateTimeValue.Year); + + // time part + returnValue += lcl_convertTimeToDays(aDateTimeValue.Hours, aDateTimeValue.Minutes, + aDateTimeValue.Seconds, aDateTimeValue.NanoSeconds); + + // done + return returnValue; + } +}; + +//= DateNormalization + +class DateNormalization : public StandardFormatNormalizer +{ +public: + explicit DateNormalization(Reference<XNumberFormatter> const& i_formatter) + : StandardFormatNormalizer(i_formatter, NumberFormat::DATE) + { + } + + virtual double convertToDouble(Any const& i_value) const override + { + double returnValue = std::numeric_limits<double>::quiet_NaN(); + + // extract + css::util::Date aDateValue; + ENSURE_OR_RETURN(i_value >>= aDateValue, "allowed for Date values only", returnValue); + + // convert + returnValue = lcl_convertDateToDays(aDateValue.Day, aDateValue.Month, aDateValue.Year); + + // done + return returnValue; + } +}; + +//= TimeNormalization + +class TimeNormalization : public StandardFormatNormalizer +{ +public: + explicit TimeNormalization(Reference<XNumberFormatter> const& i_formatter) + : StandardFormatNormalizer(i_formatter, NumberFormat::TIME) + { + } + + virtual double convertToDouble(Any const& i_value) const override + { + double returnValue = std::numeric_limits<double>::quiet_NaN(); + + // extract + css::util::Time aTimeValue; + ENSURE_OR_RETURN(i_value >>= aTimeValue, "allowed for tools::Time values only", + returnValue); + + // convert + returnValue += lcl_convertTimeToDays(aTimeValue.Hours, aTimeValue.Minutes, + aTimeValue.Seconds, aTimeValue.NanoSeconds); + + // done + return returnValue; + } +}; +} + +//= operations + +bool CellValueConversion::ensureNumberFormatter() +{ + if (bAttemptedFormatterCreation) + return xNumberFormatter.is(); + bAttemptedFormatterCreation = true; + + try + { + Reference<XComponentContext> xContext = ::comphelper::getProcessComponentContext(); + // a number formatter + Reference<XNumberFormatter> const xFormatter(NumberFormatter::create(xContext), + UNO_QUERY_THROW); + + // a supplier of number formats + Locale aLocale = SvtSysLocale().GetLanguageTag().getLocale(); + + Reference<XNumberFormatsSupplier> const xSupplier + = NumberFormatsSupplier::createWithLocale(xContext, aLocale); + + // ensure a NullDate we will assume later on + css::util::Date const aNullDate(1, 1, 1900); + Reference<XPropertySet> const xFormatSettings(xSupplier->getNumberFormatSettings(), + UNO_SET_THROW); + xFormatSettings->setPropertyValue("NullDate", Any(aNullDate)); + + // knit + xFormatter->attachNumberFormatsSupplier(xSupplier); + + // done + xNumberFormatter = xFormatter; + } + catch (const Exception&) + { + DBG_UNHANDLED_EXCEPTION("svtools.table"); + } + + return xNumberFormatter.is(); +} + +bool CellValueConversion::getValueNormalizer(Type const& i_valueType, + std::shared_ptr<StandardFormatNormalizer>& o_formatter) +{ + auto pos = aNormalizers.find(i_valueType.getTypeName()); + if (pos == aNormalizers.end()) + { + // never encountered this type before + o_formatter.reset(); + + OUString const sTypeName(i_valueType.getTypeName()); + TypeClass const eTypeClass = i_valueType.getTypeClass(); + + if (sTypeName == ::cppu::UnoType<DateTime>::get().getTypeName()) + { + o_formatter = std::make_shared<DateTimeNormalization>(xNumberFormatter); + } + else if (sTypeName == ::cppu::UnoType<css::util::Date>::get().getTypeName()) + { + o_formatter = std::make_shared<DateNormalization>(xNumberFormatter); + } + else if (sTypeName == ::cppu::UnoType<css::util::Time>::get().getTypeName()) + { + o_formatter = std::make_shared<TimeNormalization>(xNumberFormatter); + } + else if (sTypeName == ::cppu::UnoType<sal_Bool>::get().getTypeName()) + { + o_formatter = std::make_shared<BooleanNormalization>(xNumberFormatter); + } + else if (sTypeName == ::cppu::UnoType<double>::get().getTypeName() + || sTypeName == ::cppu::UnoType<float>::get().getTypeName()) + { + o_formatter = std::make_shared<DoubleNormalization>(xNumberFormatter); + } + else if ((eTypeClass == TypeClass_BYTE) || (eTypeClass == TypeClass_SHORT) + || (eTypeClass == TypeClass_UNSIGNED_SHORT) || (eTypeClass == TypeClass_LONG) + || (eTypeClass == TypeClass_UNSIGNED_LONG) || (eTypeClass == TypeClass_HYPER)) + { + o_formatter = std::make_shared<IntegerNormalization>(xNumberFormatter); + } + else + { + SAL_WARN("svtools.table", "unsupported type '" << sTypeName << "'!"); + } + aNormalizers[sTypeName] = o_formatter; + } + else + o_formatter = pos->second; + + return bool(o_formatter); +} + +//= CellValueConversion + +CellValueConversion::CellValueConversion() + : xNumberFormatter() + , bAttemptedFormatterCreation(false) + , aNormalizers() +{ +} + +CellValueConversion::~CellValueConversion() {} + +OUString CellValueConversion::convertToString(const Any& i_value) +{ + OUString sStringValue; + if (!i_value.hasValue()) + return sStringValue; + + if (!(i_value >>= sStringValue)) + { + if (ensureNumberFormatter()) + { + std::shared_ptr<StandardFormatNormalizer> pNormalizer; + if (getValueNormalizer(i_value.getValueType(), pNormalizer)) + { + try + { + double const formatterCompliantValue = pNormalizer->convertToDouble(i_value); + sal_Int32 const formatKey = pNormalizer->getFormatKey(); + sStringValue = xNumberFormatter->convertNumberToString(formatKey, + formatterCompliantValue); + } + catch (const Exception&) + { + DBG_UNHANDLED_EXCEPTION("svtools.table"); + } + } + } + } + + return sStringValue; +} + +} // namespace svt + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svtools/source/table/cellvalueconversion.hxx b/svtools/source/table/cellvalueconversion.hxx new file mode 100644 index 000000000..2e05707e5 --- /dev/null +++ b/svtools/source/table/cellvalueconversion.hxx @@ -0,0 +1,72 @@ +/* -*- 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 <com/sun/star/util/XNumberFormatter.hpp> +#include <com/sun/star/uno/Any.hxx> +#include <unordered_map> +#include <memory> + +namespace svt +{ +class StandardFormatNormalizer +{ +public: + /** converts the given <code>Any</code> into a <code>double</code> value to be fed into a number formatter + */ + virtual double convertToDouble(css::uno::Any const& i_value) const = 0; + + /** returns the format key to be used for formatting values + */ + sal_Int32 getFormatKey() const { return m_nFormatKey; } + +protected: + StandardFormatNormalizer(css::uno::Reference<css::util::XNumberFormatter> const& i_formatter, + ::sal_Int32 const i_numberFormatType); + + virtual ~StandardFormatNormalizer() {} + +private: + ::sal_Int32 m_nFormatKey; +}; + +class CellValueConversion +{ +public: + CellValueConversion(); + ~CellValueConversion(); + + OUString convertToString(const css::uno::Any& i_cellValue); + +private: + bool ensureNumberFormatter(); + bool getValueNormalizer(css::uno::Type const& i_valueType, + std::shared_ptr<StandardFormatNormalizer>& o_formatter); + + typedef std::unordered_map<OUString, std::shared_ptr<StandardFormatNormalizer>> NormalizerCache; + + css::uno::Reference<css::util::XNumberFormatter> xNumberFormatter; + bool bAttemptedFormatterCreation; + NormalizerCache aNormalizers; +}; + +} // namespace svt + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svtools/source/table/defaultinputhandler.cxx b/svtools/source/table/defaultinputhandler.cxx new file mode 100644 index 000000000..0263b2968 --- /dev/null +++ b/svtools/source/table/defaultinputhandler.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 <table/defaultinputhandler.hxx> +#include <table/tablecontrolinterface.hxx> + +#include <vcl/event.hxx> +#include <osl/diagnose.h> + + +namespace svt::table +{ + + + //= DefaultInputHandler + + + DefaultInputHandler::DefaultInputHandler() + { + aMouseFunctions.push_back( new ColumnResize ); + aMouseFunctions.push_back( new RowSelection ); + aMouseFunctions.push_back( new ColumnSortHandler ); + } + + + DefaultInputHandler::~DefaultInputHandler() + { + } + + + bool DefaultInputHandler::delegateMouseEvent( ITableControl& i_control, const MouseEvent& i_event, + FunctionResult ( MouseFunction::*i_handlerMethod )( ITableControl&, const MouseEvent& ) ) + { + if ( pActiveFunction.is() ) + { + bool furtherHandler = false; + switch ( (pActiveFunction.get()->*i_handlerMethod)( i_control, i_event ) ) + { + case ActivateFunction: + OSL_ENSURE( false, "lcl_delegateMouseEvent: unexpected - function already *is* active!" ); + break; + case ContinueFunction: + break; + case DeactivateFunction: + pActiveFunction.clear(); + break; + case SkipFunction: + furtherHandler = true; + break; + } + if ( !furtherHandler ) + // handled the event + return true; + } + + // ask all other handlers + bool handled = false; + for (auto const& mouseFunction : aMouseFunctions) + { + if (handled) + break; + if (mouseFunction == pActiveFunction) + // we already invoked this function + continue; + + switch ( (mouseFunction.get()->*i_handlerMethod)( i_control, i_event ) ) + { + case ActivateFunction: + pActiveFunction = mouseFunction; + handled = true; + break; + case ContinueFunction: + case DeactivateFunction: + OSL_ENSURE( false, "lcl_delegateMouseEvent: unexpected: inactive handler cannot be continued or deactivated!" ); + break; + case SkipFunction: + handled = false; + break; + } + } + return handled; + } + + + bool DefaultInputHandler::MouseMove( ITableControl& i_tableControl, const MouseEvent& i_event ) + { + return delegateMouseEvent( i_tableControl, i_event, &MouseFunction::handleMouseMove ); + } + + + bool DefaultInputHandler::MouseButtonDown( ITableControl& i_tableControl, const MouseEvent& i_event ) + { + return delegateMouseEvent( i_tableControl, i_event, &MouseFunction::handleMouseDown ); + } + + + bool DefaultInputHandler::MouseButtonUp( ITableControl& i_tableControl, const MouseEvent& i_event ) + { + return delegateMouseEvent( i_tableControl, i_event, &MouseFunction::handleMouseUp ); + } + + + bool DefaultInputHandler::KeyInput( ITableControl& _rControl, const KeyEvent& rKEvt ) + { + bool bHandled = false; + + const vcl::KeyCode& rKeyCode = rKEvt.GetKeyCode(); + sal_uInt16 nKeyCode = rKeyCode.GetCode(); + + struct ActionMapEntry + { + sal_uInt16 nKeyCode; + sal_uInt16 nKeyModifier; + TableControlAction eAction; + } + static const aKnownActions[] = { + { KEY_DOWN, 0, cursorDown }, + { KEY_UP, 0, cursorUp }, + { KEY_LEFT, 0, cursorLeft }, + { KEY_RIGHT, 0, cursorRight }, + { KEY_HOME, 0, cursorToLineStart }, + { KEY_END, 0, cursorToLineEnd }, + { KEY_PAGEUP, 0, cursorPageUp }, + { KEY_PAGEDOWN, 0, cursorPageDown }, + { KEY_PAGEUP, KEY_MOD1, cursorToFirstLine }, + { KEY_PAGEDOWN, KEY_MOD1, cursorToLastLine }, + { KEY_HOME, KEY_MOD1, cursorTopLeft }, + { KEY_END, KEY_MOD1, cursorBottomRight }, + { KEY_SPACE, KEY_MOD1, cursorSelectRow }, + { KEY_UP, KEY_SHIFT, cursorSelectRowUp }, + { KEY_DOWN, KEY_SHIFT, cursorSelectRowDown }, + { KEY_END, KEY_SHIFT, cursorSelectRowAreaBottom }, + { KEY_HOME, KEY_SHIFT, cursorSelectRowAreaTop }, + + { 0, 0, invalidTableControlAction } + }; + + const ActionMapEntry* pActions = aKnownActions; + for ( ; pActions->eAction != invalidTableControlAction; ++pActions ) + { + if ( ( pActions->nKeyCode == nKeyCode ) && ( pActions->nKeyModifier == rKeyCode.GetModifier() ) ) + { + bHandled = _rControl.dispatchAction( pActions->eAction ); + break; + } + } + + return bHandled; + } + + + bool DefaultInputHandler::GetFocus( ITableControl& _rControl ) + { + _rControl.showCursor(); + return false; // continue processing + } + + + bool DefaultInputHandler::LoseFocus( ITableControl& _rControl ) + { + _rControl.hideCursor(); + return false; // continue processing + } + + +} // namespace svt::table + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svtools/source/table/gridtablerenderer.cxx b/svtools/source/table/gridtablerenderer.cxx new file mode 100644 index 000000000..f814fc64d --- /dev/null +++ b/svtools/source/table/gridtablerenderer.cxx @@ -0,0 +1,598 @@ +/* -*- 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 "cellvalueconversion.hxx" +#include <table/gridtablerenderer.hxx> +#include <table/tablesort.hxx> + +#include <com/sun/star/graphic/XGraphic.hpp> + +#include <tools/debug.hxx> +#include <tools/diagnose_ex.h> +#include <vcl/window.hxx> +#include <vcl/image.hxx> +#include <vcl/virdev.hxx> +#include <vcl/decoview.hxx> +#include <vcl/settings.hxx> + + +namespace svt::table +{ + using ::css::uno::Any; + using ::css::uno::Reference; + using ::css::uno::UNO_QUERY; + using ::css::uno::XInterface; + using ::css::uno::TypeClass_INTERFACE; + using ::css::graphic::XGraphic; + using ::css::style::HorizontalAlignment; + using ::css::style::HorizontalAlignment_CENTER; + using ::css::style::HorizontalAlignment_RIGHT; + using ::css::style::VerticalAlignment; + using ::css::style::VerticalAlignment_MIDDLE; + using ::css::style::VerticalAlignment_BOTTOM; + + + //= CachedSortIndicator + + namespace { + + class CachedSortIndicator + { + public: + CachedSortIndicator() + : m_lastHeaderHeight( 0 ) + , m_lastArrowColor( COL_TRANSPARENT ) + { + } + + BitmapEx const & getBitmapFor(vcl::RenderContext const & i_device, tools::Long const i_headerHeight, + StyleSettings const & i_style, bool const i_sortAscending); + + private: + tools::Long m_lastHeaderHeight; + Color m_lastArrowColor; + BitmapEx m_sortAscending; + BitmapEx m_sortDescending; + }; + + } + + BitmapEx const & CachedSortIndicator::getBitmapFor(vcl::RenderContext const& i_device, tools::Long const i_headerHeight, + StyleSettings const & i_style, bool const i_sortAscending ) + { + BitmapEx& rBitmap(i_sortAscending ? m_sortAscending : m_sortDescending); + if (rBitmap.IsEmpty() || (i_headerHeight != m_lastHeaderHeight) || (i_style.GetActiveColor() != m_lastArrowColor)) + { + tools::Long const nSortIndicatorWidth = 2 * i_headerHeight / 3; + tools::Long const nSortIndicatorHeight = 2 * nSortIndicatorWidth / 3; + + Point const aBitmapPos( 0, 0 ); + Size const aBitmapSize( nSortIndicatorWidth, nSortIndicatorHeight ); + ScopedVclPtrInstance< VirtualDevice > aDevice(i_device, DeviceFormat::DEFAULT, + DeviceFormat::DEFAULT); + aDevice->SetOutputSizePixel( aBitmapSize ); + + DecorationView aDecoView(aDevice.get()); + aDecoView.DrawSymbol(tools::Rectangle(aBitmapPos, aBitmapSize), + i_sortAscending ? SymbolType::SPIN_UP : SymbolType::SPIN_DOWN, + i_style.GetActiveColor()); + + rBitmap = aDevice->GetBitmapEx(aBitmapPos, aBitmapSize); + m_lastHeaderHeight = i_headerHeight; + m_lastArrowColor = i_style.GetActiveColor(); + } + return rBitmap; + } + + + //= GridTableRenderer_Impl + + struct GridTableRenderer_Impl + { + ITableModel& rModel; + RowPos nCurrentRow; + bool bUseGridLines; + CachedSortIndicator aSortIndicator; + CellValueConversion aStringConverter; + + explicit GridTableRenderer_Impl( ITableModel& _rModel ) + : rModel( _rModel ) + , nCurrentRow( ROW_INVALID ) + , bUseGridLines( true ) + , aSortIndicator( ) + , aStringConverter() + { + } + }; + + + //= helper + + namespace + { + tools::Rectangle lcl_getContentArea( GridTableRenderer_Impl const & i_impl, tools::Rectangle const & i_cellArea ) + { + tools::Rectangle aContentArea( i_cellArea ); + if ( i_impl.bUseGridLines ) + { + aContentArea.AdjustRight( -1 ); + aContentArea.AdjustBottom( -1 ); + } + return aContentArea; + } + tools::Rectangle lcl_getTextRenderingArea( tools::Rectangle const & i_contentArea ) + { + tools::Rectangle aTextArea( i_contentArea ); + aTextArea.AdjustLeft(2 ); aTextArea.AdjustRight( -2 ); + aTextArea.AdjustTop( 1 ); aTextArea.AdjustBottom( -1 ); + return aTextArea; + } + + DrawTextFlags lcl_getAlignmentTextDrawFlags( GridTableRenderer_Impl const & i_impl, ColPos const i_columnPos ) + { + DrawTextFlags nVertFlag = DrawTextFlags::Top; + VerticalAlignment const eVertAlign = i_impl.rModel.getVerticalAlign(); + switch ( eVertAlign ) + { + case VerticalAlignment_MIDDLE: nVertFlag = DrawTextFlags::VCenter; break; + case VerticalAlignment_BOTTOM: nVertFlag = DrawTextFlags::Bottom; break; + default: + break; + } + + DrawTextFlags nHorzFlag = DrawTextFlags::Left; + HorizontalAlignment const eHorzAlign = i_impl.rModel.getColumnCount() > 0 + ? i_impl.rModel.getColumnModel( i_columnPos )->getHorizontalAlign() + : HorizontalAlignment_CENTER; + switch ( eHorzAlign ) + { + case HorizontalAlignment_CENTER: nHorzFlag = DrawTextFlags::Center; break; + case HorizontalAlignment_RIGHT: nHorzFlag = DrawTextFlags::Right; break; + default: + break; + } + + return nVertFlag | nHorzFlag; + } + + } + + + //= GridTableRenderer + + + GridTableRenderer::GridTableRenderer( ITableModel& _rModel ) + :m_pImpl( new GridTableRenderer_Impl( _rModel ) ) + { + } + + + GridTableRenderer::~GridTableRenderer() + { + } + + + bool GridTableRenderer::useGridLines() const + { + return m_pImpl->bUseGridLines; + } + + + void GridTableRenderer::useGridLines( bool const i_use ) + { + m_pImpl->bUseGridLines = i_use; + } + + + namespace + { + Color lcl_getEffectiveColor(std::optional<Color> const& i_modelColor, + StyleSettings const& i_styleSettings, + Color const& (StyleSettings::*i_getDefaultColor) () const) + { + if (!!i_modelColor) + return *i_modelColor; + return (i_styleSettings.*i_getDefaultColor)(); + } + } + + + void GridTableRenderer::PaintHeaderArea(vcl::RenderContext& rRenderContext, const tools::Rectangle& _rArea, + bool _bIsColHeaderArea, bool _bIsRowHeaderArea, const StyleSettings& _rStyle) + { + OSL_PRECOND(_bIsColHeaderArea || _bIsRowHeaderArea, "GridTableRenderer::PaintHeaderArea: invalid area flags!"); + + rRenderContext.Push(vcl::PushFlags::FILLCOLOR | vcl::PushFlags::LINECOLOR); + + Color const background = lcl_getEffectiveColor(m_pImpl->rModel.getHeaderBackgroundColor(), + _rStyle, &StyleSettings::GetDialogColor); + rRenderContext.SetFillColor(background); + + rRenderContext.SetLineColor(); + rRenderContext.DrawRect(_rArea); + + // delimiter lines at bottom/right + std::optional<Color> aLineColor(m_pImpl->rModel.getLineColor()); + Color const lineColor = !aLineColor ? _rStyle.GetSeparatorColor() : *aLineColor; + rRenderContext.SetLineColor(lineColor); + rRenderContext.DrawLine(_rArea.BottomLeft(), _rArea.BottomRight()); + rRenderContext.DrawLine(_rArea.BottomRight(), _rArea.TopRight()); + + rRenderContext.Pop(); + } + + + void GridTableRenderer::PaintColumnHeader( + ColPos _nCol, + vcl::RenderContext& rRenderContext, + const tools::Rectangle& _rArea, const StyleSettings& _rStyle) + { + rRenderContext.Push(vcl::PushFlags::LINECOLOR); + + OUString sHeaderText; + PColumnModel const pColumn = m_pImpl->rModel.getColumnModel( _nCol ); + DBG_ASSERT( pColumn, "GridTableRenderer::PaintColumnHeader: invalid column model object!" ); + if ( pColumn ) + sHeaderText = pColumn->getName(); + + Color const textColor = lcl_getEffectiveColor( m_pImpl->rModel.getTextColor(), _rStyle, &StyleSettings::GetFieldTextColor ); + rRenderContext.SetTextColor(textColor); + + tools::Rectangle const aTextRect( lcl_getTextRenderingArea( lcl_getContentArea( *m_pImpl, _rArea ) ) ); + DrawTextFlags nDrawTextFlags = lcl_getAlignmentTextDrawFlags( *m_pImpl, _nCol ) | DrawTextFlags::Clip; + if (!m_pImpl->rModel.isEnabled()) + nDrawTextFlags |= DrawTextFlags::Disable; + rRenderContext.DrawText( aTextRect, sHeaderText, nDrawTextFlags ); + + std::optional<Color> const aLineColor( m_pImpl->rModel.getLineColor() ); + Color const lineColor = !aLineColor ? _rStyle.GetSeparatorColor() : *aLineColor; + rRenderContext.SetLineColor( lineColor ); + rRenderContext.DrawLine( _rArea.BottomRight(), _rArea.TopRight()); + rRenderContext.DrawLine( _rArea.BottomLeft(), _rArea.BottomRight() ); + + // draw sort indicator if the model data is sorted by the given column + ITableDataSort const * pSortAdapter = m_pImpl->rModel.getSortAdapter(); + ColumnSort aCurrentSortOrder; + if ( pSortAdapter != nullptr ) + aCurrentSortOrder = pSortAdapter->getCurrentSortOrder(); + if ( aCurrentSortOrder.nColumnPos == _nCol ) + { + tools::Long const nHeaderHeight( _rArea.GetHeight() ); + BitmapEx const aIndicatorBitmap = m_pImpl->aSortIndicator.getBitmapFor(rRenderContext, nHeaderHeight, _rStyle, + aCurrentSortOrder.eSortDirection == ColumnSortAscending); + Size const aBitmapSize( aIndicatorBitmap.GetSizePixel() ); + tools::Long const nSortIndicatorPaddingX = 2; + tools::Long const nSortIndicatorPaddingY = ( nHeaderHeight - aBitmapSize.Height() ) / 2; + + if ( nDrawTextFlags & DrawTextFlags::Right ) + { + // text is right aligned => draw the sort indicator at the left hand side + rRenderContext.DrawBitmapEx(Point(_rArea.Left() + nSortIndicatorPaddingX, _rArea.Top() + nSortIndicatorPaddingY), + aIndicatorBitmap); + } + else + { + // text is left-aligned or centered => draw the sort indicator at the right hand side + rRenderContext.DrawBitmapEx(Point(_rArea.Right() - nSortIndicatorPaddingX - aBitmapSize.Width(), nSortIndicatorPaddingY), + aIndicatorBitmap); + } + } + + rRenderContext.Pop(); + } + + + void GridTableRenderer::PrepareRow(RowPos _nRow, bool i_hasControlFocus, bool _bSelected, vcl::RenderContext& rRenderContext, + const tools::Rectangle& _rRowArea, const StyleSettings& _rStyle) + { + // remember the row for subsequent calls to the other ->ITableRenderer methods + m_pImpl->nCurrentRow = _nRow; + + rRenderContext.Push(vcl::PushFlags::FILLCOLOR | vcl::PushFlags::LINECOLOR); + + Color backgroundColor = _rStyle.GetFieldColor(); + + Color const activeSelectionBackColor = lcl_getEffectiveColor(m_pImpl->rModel.getActiveSelectionBackColor(), + _rStyle, &StyleSettings::GetHighlightColor); + if (_bSelected) + { + // selected rows use the background color from the style + backgroundColor = i_hasControlFocus + ? activeSelectionBackColor + : lcl_getEffectiveColor(m_pImpl->rModel.getInactiveSelectionBackColor(), _rStyle, &StyleSettings::GetDeactiveColor); + } + else + { + std::optional< std::vector<Color> > aRowColors = m_pImpl->rModel.getRowBackgroundColors(); + if (!aRowColors) + { + // use alternating default colors + Color const fieldColor = _rStyle.GetFieldColor(); + if (_rStyle.GetHighContrastMode() || ((m_pImpl->nCurrentRow % 2) == 0)) + { + backgroundColor = fieldColor; + } + else + { + Color hilightColor = activeSelectionBackColor; + hilightColor.SetRed( 9 * ( fieldColor.GetRed() - hilightColor.GetRed() ) / 10 + hilightColor.GetRed() ); + hilightColor.SetGreen( 9 * ( fieldColor.GetGreen() - hilightColor.GetGreen() ) / 10 + hilightColor.GetGreen() ); + hilightColor.SetBlue( 9 * ( fieldColor.GetBlue() - hilightColor.GetBlue() ) / 10 + hilightColor.GetBlue() ); + backgroundColor = hilightColor; + } + } + else + { + if (aRowColors->empty()) + { + // all colors have the same background color + backgroundColor = _rStyle.GetFieldColor(); + } + else + { + backgroundColor = aRowColors->at(m_pImpl->nCurrentRow % aRowColors->size()); + } + } + } + + rRenderContext.SetLineColor(); + rRenderContext.SetFillColor(backgroundColor); + rRenderContext.DrawRect(_rRowArea); + + rRenderContext.Pop(); + } + + + void GridTableRenderer::PaintRowHeader(vcl::RenderContext& rRenderContext, + const tools::Rectangle& _rArea, const StyleSettings& _rStyle) + { + rRenderContext.Push( vcl::PushFlags::LINECOLOR | vcl::PushFlags::TEXTCOLOR ); + + std::optional<Color> const aLineColor( m_pImpl->rModel.getLineColor() ); + Color const lineColor = !aLineColor ? _rStyle.GetSeparatorColor() : *aLineColor; + rRenderContext.SetLineColor(lineColor); + rRenderContext.DrawLine(_rArea.BottomLeft(), _rArea.BottomRight()); + + Any const rowHeading( m_pImpl->rModel.getRowHeading( m_pImpl->nCurrentRow ) ); + OUString const rowTitle( m_pImpl->aStringConverter.convertToString( rowHeading ) ); + if (!rowTitle.isEmpty()) + { + Color const textColor = lcl_getEffectiveColor(m_pImpl->rModel.getHeaderTextColor(), + _rStyle, &StyleSettings::GetFieldTextColor); + rRenderContext.SetTextColor(textColor); + + tools::Rectangle const aTextRect(lcl_getTextRenderingArea(lcl_getContentArea(*m_pImpl, _rArea))); + DrawTextFlags nDrawTextFlags = lcl_getAlignmentTextDrawFlags(*m_pImpl, 0) | DrawTextFlags::Clip; + if (!m_pImpl->rModel.isEnabled()) + nDrawTextFlags |= DrawTextFlags::Disable; + // TODO: is using the horizontal alignment of the 0'th column a good idea here? This is pretty ... arbitrary .. + rRenderContext.DrawText(aTextRect, rowTitle, nDrawTextFlags); + } + + rRenderContext.Pop(); + } + + + struct GridTableRenderer::CellRenderContext + { + OutputDevice& rDevice; + tools::Rectangle const aContentArea; + StyleSettings const & rStyle; + ColPos const nColumn; + bool const bSelected; + bool const bHasControlFocus; + + CellRenderContext( OutputDevice& i_device, tools::Rectangle const & i_contentArea, + StyleSettings const & i_style, ColPos const i_column, bool const i_selected, bool const i_hasControlFocus ) + :rDevice( i_device ) + ,aContentArea( i_contentArea ) + ,rStyle( i_style ) + ,nColumn( i_column ) + ,bSelected( i_selected ) + ,bHasControlFocus( i_hasControlFocus ) + { + } + }; + + + void GridTableRenderer::PaintCell(ColPos const i_column, bool _bSelected, bool i_hasControlFocus, + vcl::RenderContext& rRenderContext, const tools::Rectangle& _rArea, const StyleSettings& _rStyle) + { + rRenderContext.Push(vcl::PushFlags::LINECOLOR | vcl::PushFlags::FILLCOLOR); + + tools::Rectangle const aContentArea(lcl_getContentArea(*m_pImpl, _rArea)); + CellRenderContext const aCellRenderContext(rRenderContext, aContentArea, _rStyle, i_column, _bSelected, i_hasControlFocus); + impl_paintCellContent(aCellRenderContext); + + if ( m_pImpl->bUseGridLines ) + { + ::std::optional< ::Color > aLineColor( m_pImpl->rModel.getLineColor() ); + ::Color lineColor = !aLineColor ? _rStyle.GetSeparatorColor() : *aLineColor; + + if ( _bSelected && !aLineColor ) + { + // if no line color is specified by the model, use the usual selection color for lines in selected cells + lineColor = i_hasControlFocus + ? lcl_getEffectiveColor( m_pImpl->rModel.getActiveSelectionBackColor(), _rStyle, &StyleSettings::GetHighlightColor ) + : lcl_getEffectiveColor( m_pImpl->rModel.getInactiveSelectionBackColor(), _rStyle, &StyleSettings::GetDeactiveColor ); + } + + rRenderContext.SetLineColor( lineColor ); + rRenderContext.DrawLine( _rArea.BottomLeft(), _rArea.BottomRight() ); + rRenderContext.DrawLine( _rArea.BottomRight(), _rArea.TopRight() ); + } + + rRenderContext.Pop(); + } + + + void GridTableRenderer::impl_paintCellImage( CellRenderContext const & i_context, Image const & i_image ) + { + Point imagePos( Point( i_context.aContentArea.Left(), i_context.aContentArea.Top() ) ); + Size imageSize = i_image.GetSizePixel(); + if ( i_context.aContentArea.GetWidth() > imageSize.Width() ) + { + const HorizontalAlignment eHorzAlign = m_pImpl->rModel.getColumnModel( i_context.nColumn )->getHorizontalAlign(); + switch ( eHorzAlign ) + { + case HorizontalAlignment_CENTER: + imagePos.AdjustX(( i_context.aContentArea.GetWidth() - imageSize.Width() ) / 2 ); + break; + case HorizontalAlignment_RIGHT: + imagePos.setX( i_context.aContentArea.Right() - imageSize.Width() ); + break; + default: + break; + } + + } + else + imageSize.setWidth( i_context.aContentArea.GetWidth() ); + + if ( i_context.aContentArea.GetHeight() > imageSize.Height() ) + { + const VerticalAlignment eVertAlign = m_pImpl->rModel.getVerticalAlign(); + switch ( eVertAlign ) + { + case VerticalAlignment_MIDDLE: + imagePos.AdjustY(( i_context.aContentArea.GetHeight() - imageSize.Height() ) / 2 ); + break; + case VerticalAlignment_BOTTOM: + imagePos.setY( i_context.aContentArea.Bottom() - imageSize.Height() ); + break; + default: + break; + } + } + else + imageSize.setHeight( i_context.aContentArea.GetHeight() - 1 ); + DrawImageFlags const nStyle = m_pImpl->rModel.isEnabled() ? DrawImageFlags::NONE : DrawImageFlags::Disable; + i_context.rDevice.DrawImage( imagePos, imageSize, i_image, nStyle ); + } + + + void GridTableRenderer::impl_paintCellContent( CellRenderContext const & i_context ) + { + Any aCellContent; + m_pImpl->rModel.getCellContent( i_context.nColumn, m_pImpl->nCurrentRow, aCellContent ); + + if ( aCellContent.getValueTypeClass() == TypeClass_INTERFACE ) + { + Reference< XInterface > const xContentInterface( aCellContent, UNO_QUERY ); + if ( !xContentInterface.is() ) + // allowed. kind of. + return; + + Reference< XGraphic > const xGraphic( aCellContent, UNO_QUERY ); + ENSURE_OR_RETURN_VOID( xGraphic.is(), "GridTableRenderer::impl_paintCellContent: only XGraphic interfaces (or NULL) are supported for painting." ); + + const Image aImage( xGraphic ); + impl_paintCellImage( i_context, aImage ); + return; + } + + const OUString sText( m_pImpl->aStringConverter.convertToString( aCellContent ) ); + impl_paintCellText( i_context, sText ); + } + + + void GridTableRenderer::impl_paintCellText( CellRenderContext const & i_context, OUString const & i_text ) + { + if ( i_context.bSelected ) + { + ::Color const textColor = i_context.bHasControlFocus + ? lcl_getEffectiveColor( m_pImpl->rModel.getActiveSelectionTextColor(), i_context.rStyle, &StyleSettings::GetHighlightTextColor ) + : lcl_getEffectiveColor( m_pImpl->rModel.getInactiveSelectionTextColor(), i_context.rStyle, &StyleSettings::GetDeactiveTextColor ); + i_context.rDevice.SetTextColor( textColor ); + } + else + { + ::Color const textColor = lcl_getEffectiveColor( m_pImpl->rModel.getTextColor(), i_context.rStyle, &StyleSettings::GetFieldTextColor ); + i_context.rDevice.SetTextColor( textColor ); + } + + tools::Rectangle const textRect( lcl_getTextRenderingArea( i_context.aContentArea ) ); + DrawTextFlags nDrawTextFlags = lcl_getAlignmentTextDrawFlags( *m_pImpl, i_context.nColumn ) | DrawTextFlags::Clip; + if ( !m_pImpl->rModel.isEnabled() ) + nDrawTextFlags |= DrawTextFlags::Disable; + i_context.rDevice.DrawText( textRect, i_text, nDrawTextFlags ); + } + + + void GridTableRenderer::ShowCellCursor( vcl::Window& _rView, const tools::Rectangle& _rCursorRect) + { + _rView.ShowFocus( _rCursorRect ); + } + + + void GridTableRenderer::HideCellCursor( vcl::Window& _rView ) + { + _rView.HideFocus(); + } + + + bool GridTableRenderer::FitsIntoCell( Any const & i_cellContent, + OutputDevice& i_targetDevice, tools::Rectangle const & i_targetArea ) const + { + if ( !i_cellContent.hasValue() ) + return true; + + if ( i_cellContent.getValueTypeClass() == TypeClass_INTERFACE ) + { + Reference< XInterface > const xContentInterface( i_cellContent, UNO_QUERY ); + if ( !xContentInterface.is() ) + return true; + + Reference< XGraphic > const xGraphic( i_cellContent, UNO_QUERY ); + if ( xGraphic.is() ) + // for the moment, assume it fits. We can always scale it down during painting ... + return true; + + OSL_ENSURE( false, "GridTableRenderer::FitsIntoCell: only XGraphic interfaces (or NULL) are supported for painting." ); + return true; + } + + OUString const sText( m_pImpl->aStringConverter.convertToString( i_cellContent ) ); + if ( sText.isEmpty() ) + return true; + + tools::Rectangle const aTargetArea( lcl_getTextRenderingArea( lcl_getContentArea( *m_pImpl, i_targetArea ) ) ); + + tools::Long const nTextHeight = i_targetDevice.GetTextHeight(); + if ( nTextHeight > aTargetArea.GetHeight() ) + return false; + + tools::Long const nTextWidth = i_targetDevice.GetTextWidth( sText ); + return nTextWidth <= aTargetArea.GetWidth(); + } + + + bool GridTableRenderer::GetFormattedCellString( Any const & i_cellValue, OUString & o_cellString ) const + { + o_cellString = m_pImpl->aStringConverter.convertToString( i_cellValue ); + + return true; + } + + +} // namespace svt::table + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svtools/source/table/mousefunction.cxx b/svtools/source/table/mousefunction.cxx new file mode 100644 index 000000000..8bb30390a --- /dev/null +++ b/svtools/source/table/mousefunction.cxx @@ -0,0 +1,274 @@ +/* -*- 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 <mousefunction.hxx> +#include <table/tablecontrolinterface.hxx> +#include <table/tablesort.hxx> + +#include <tools/diagnose_ex.h> +#include <vcl/ptrstyle.hxx> + +namespace svt::table +{ + + + //= ColumnResize + + + FunctionResult ColumnResize::handleMouseMove( ITableControl& i_tableControl, MouseEvent const & i_event ) + { + Point const aPoint = i_event.GetPosPixel(); + + if ( m_nResizingColumn == COL_INVALID ) + { + // if we hit a column divider, change the mouse pointer accordingly + PointerStyle aNewPointer( PointerStyle::Arrow ); + TableCell const tableCell = i_tableControl.hitTest( aPoint ); + if ( ( tableCell.nRow == ROW_COL_HEADERS ) && ( tableCell.eArea == ColumnDivider ) ) + { + aNewPointer = PointerStyle::HSplit; + } + i_tableControl.setPointer( aNewPointer ); + + return SkipFunction; // TODO: is this correct? + } + + ::Size const tableSize = i_tableControl.getTableSizePixel(); + + // set proper pointer + PointerStyle aNewPointer( PointerStyle::Arrow ); + ColumnMetrics const & columnMetrics( i_tableControl.getColumnMetrics( m_nResizingColumn ) ); + if ( ( aPoint.X() > tableSize.Width() ) + || ( aPoint.X() < columnMetrics.nStartPixel ) + ) + { + aNewPointer = PointerStyle::NotAllowed; + } + else + { + aNewPointer = PointerStyle::HSplit; + } + i_tableControl.setPointer( aNewPointer ); + + // show tracking line + i_tableControl.hideTracking(); + i_tableControl.showTracking( + tools::Rectangle( + Point( aPoint.X(), 0 ), + Size( 1, tableSize.Height() ) + ), + ShowTrackFlags::Split | ShowTrackFlags::TrackWindow + ); + + return ContinueFunction; + } + + + FunctionResult ColumnResize::handleMouseDown( ITableControl& i_tableControl, MouseEvent const & i_event ) + { + if ( m_nResizingColumn != COL_INVALID ) + { + OSL_ENSURE( false, "ColumnResize::handleMouseDown: suspicious: MouseButtonDown while still tracking?" ); + return ContinueFunction; + } + + TableCell const tableCell( i_tableControl.hitTest( i_event.GetPosPixel() ) ); + if ( tableCell.nRow == ROW_COL_HEADERS ) + { + if ( ( tableCell.nColumn != COL_INVALID ) + && ( tableCell.eArea == ColumnDivider ) + ) + { + m_nResizingColumn = tableCell.nColumn; + i_tableControl.captureMouse(); + return ActivateFunction; + } + } + + return SkipFunction; + } + + + FunctionResult ColumnResize::handleMouseUp( ITableControl& i_tableControl, MouseEvent const & i_event ) + { + if ( m_nResizingColumn == COL_INVALID ) + return SkipFunction; + + Point const aPoint = i_event.GetPosPixel(); + + i_tableControl.hideTracking(); + PColumnModel const pColumn = i_tableControl.getModel()->getColumnModel( m_nResizingColumn ); + tools::Long const maxWidthLogical = pColumn->getMaxWidth(); + tools::Long const minWidthLogical = pColumn->getMinWidth(); + + // new position of mouse + tools::Long const requestedEnd = aPoint.X(); + + // old position of right border + tools::Long const oldEnd = i_tableControl.getColumnMetrics( m_nResizingColumn ).nEndPixel; + + // position of left border if cursor in the to-be-resized column + tools::Long const columnStart = i_tableControl.getColumnMetrics( m_nResizingColumn ).nStartPixel; + tools::Long const requestedWidth = requestedEnd - columnStart; + // TODO: this is not correct, strictly: It assumes that the mouse was pressed exactly on the "end" pos, + // but for a while now, we have relaxed this, and allow clicking a few pixels aside, too + + if ( requestedEnd >= columnStart ) + { + tools::Long requestedWidthLogical = i_tableControl.pixelWidthToAppFont( requestedWidth ); + // respect column width limits + if ( oldEnd > requestedEnd ) + { + // column has become smaller, check against minimum width + if ( ( minWidthLogical != 0 ) && ( requestedWidthLogical < minWidthLogical ) ) + requestedWidthLogical = minWidthLogical; + } + else if ( oldEnd < requestedEnd ) + { + // column has become larger, check against max width + if ( ( maxWidthLogical != 0 ) && ( requestedWidthLogical >= maxWidthLogical ) ) + requestedWidthLogical = maxWidthLogical; + } + pColumn->setWidth( requestedWidthLogical ); + i_tableControl.invalidate( TableArea::All ); + } + + i_tableControl.setPointer( PointerStyle::Arrow ); + i_tableControl.releaseMouse(); + + m_nResizingColumn = COL_INVALID; + return DeactivateFunction; + } + + + //= RowSelection + + + FunctionResult RowSelection::handleMouseMove( ITableControl&, MouseEvent const & ) + { + return SkipFunction; + } + + + FunctionResult RowSelection::handleMouseDown( ITableControl& i_tableControl, MouseEvent const & i_event ) + { + bool handled = false; + + TableCell const tableCell( i_tableControl.hitTest( i_event.GetPosPixel() ) ); + if ( tableCell.nRow >= 0 ) + { + if ( i_tableControl.getSelEngine()->GetSelectionMode() == SelectionMode::NONE ) + { + i_tableControl.activateCell( tableCell.nColumn, tableCell.nRow ); + handled = true; + } + else + { + handled = i_tableControl.getSelEngine()->SelMouseButtonDown( i_event ); + } + } + + if ( handled ) + m_bActive = true; + return handled ? ActivateFunction : SkipFunction; + } + + + FunctionResult RowSelection::handleMouseUp( ITableControl& i_tableControl, MouseEvent const & i_event ) + { + TableCell const tableCell = i_tableControl.hitTest( i_event.GetPosPixel() ); + if ( tableCell.nRow >= 0 ) + { + if ( i_tableControl.getSelEngine()->GetSelectionMode() != SelectionMode::NONE ) + { + i_tableControl.getSelEngine()->SelMouseButtonUp( i_event ); + } + } + if ( m_bActive ) + { + m_bActive = false; + return DeactivateFunction; + } + return SkipFunction; + } + + + //= ColumnSortHandler + + + FunctionResult ColumnSortHandler::handleMouseMove( ITableControl&, MouseEvent const & ) + { + return SkipFunction; + } + + + FunctionResult ColumnSortHandler::handleMouseDown( ITableControl& i_tableControl, MouseEvent const & i_event ) + { + if ( m_nActiveColumn != COL_INVALID ) + { + OSL_ENSURE( false, "ColumnSortHandler::handleMouseDown: called while already active - suspicious!" ); + return ContinueFunction; + } + + if ( i_tableControl.getModel()->getSortAdapter() == nullptr ) + // no sorting support at the model + return SkipFunction; + + TableCell const tableCell( i_tableControl.hitTest( i_event.GetPosPixel() ) ); + if ( ( tableCell.nRow != ROW_COL_HEADERS ) || ( tableCell.nColumn < 0 ) ) + return SkipFunction; + + // TODO: ensure the column header is rendered in some special way, indicating its current state + + m_nActiveColumn = tableCell.nColumn; + return ActivateFunction; + } + + + FunctionResult ColumnSortHandler::handleMouseUp( ITableControl& i_tableControl, MouseEvent const & i_event ) + { + if ( m_nActiveColumn == COL_INVALID ) + return SkipFunction; + + TableCell const tableCell( i_tableControl.hitTest( i_event.GetPosPixel() ) ); + if ( ( tableCell.nRow == ROW_COL_HEADERS ) && ( tableCell.nColumn == m_nActiveColumn ) ) + { + ITableDataSort* pSort = i_tableControl.getModel()->getSortAdapter(); + ENSURE_OR_RETURN( pSort != nullptr, "ColumnSortHandler::handleMouseUp: somebody is mocking with us!", DeactivateFunction ); + // in handleMousButtonDown, the model claimed to have sort support ... + + ColumnSortDirection eSortDirection = ColumnSortAscending; + ColumnSort const aCurrentSort = pSort->getCurrentSortOrder(); + if ( aCurrentSort.nColumnPos == m_nActiveColumn ) + // invert existing sort order + eSortDirection = ( aCurrentSort.eSortDirection == ColumnSortAscending ) ? ColumnSortDescending : ColumnSortAscending; + + pSort->sortByColumn( m_nActiveColumn, eSortDirection ); + } + + m_nActiveColumn = COL_INVALID; + return DeactivateFunction; + } + + +} // namespace svt::table + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svtools/source/table/tablecontrol.cxx b/svtools/source/table/tablecontrol.cxx new file mode 100644 index 000000000..e341b1ea4 --- /dev/null +++ b/svtools/source/table/tablecontrol.cxx @@ -0,0 +1,639 @@ +/* -*- 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 <table/tablecontrol.hxx> + +#include "tablecontrol_impl.hxx" +#include "tabledatawindow.hxx" + +#include <com/sun/star/accessibility/AccessibleStateType.hpp> +#include <com/sun/star/accessibility/AccessibleEventId.hpp> + +#include <sal/log.hxx> +#include <tools/diagnose_ex.h> +#include <unotools/accessiblestatesethelper.hxx> +#include <vcl/settings.hxx> +#include <vcl/vclevent.hxx> + +using namespace ::com::sun::star::uno; +using ::com::sun::star::accessibility::XAccessible; +using namespace ::com::sun::star::accessibility; +using namespace ::com::sun::star::lang; +using namespace utl; + +namespace svt::table +{ + + + namespace AccessibleEventId = ::com::sun::star::accessibility::AccessibleEventId; + + + //= TableControl + + + TableControl::TableControl( vcl::Window* _pParent, WinBits _nStyle ) + :Control( _pParent, _nStyle ) + ,m_pImpl( std::make_shared<TableControl_Impl>( *this ) ) + { + TableDataWindow& rDataWindow = m_pImpl->getDataWindow(); + rDataWindow.SetSelectHdl( LINK( this, TableControl, ImplSelectHdl ) ); + + // by default, use the background as determined by the style settings + const Color aWindowColor( GetSettings().GetStyleSettings().GetFieldColor() ); + SetBackground( Wallpaper( aWindowColor ) ); + GetOutDev()->SetFillColor( aWindowColor ); + + SetCompoundControl( true ); + } + + + TableControl::~TableControl() + { + disposeOnce(); + } + + void TableControl::dispose() + { + CallEventListeners( VclEventId::ObjectDying ); + + m_pImpl->setModel( PTableModel() ); + m_pImpl->disposeAccessible(); + m_pImpl.reset(); + Control::dispose(); + } + + + void TableControl::GetFocus() + { + if ( !m_pImpl || !m_pImpl->getInputHandler()->GetFocus( *m_pImpl ) ) + Control::GetFocus(); + } + + + void TableControl::LoseFocus() + { + if ( !m_pImpl || !m_pImpl->getInputHandler()->LoseFocus( *m_pImpl ) ) + Control::LoseFocus(); + } + + + void TableControl::KeyInput( const KeyEvent& rKEvt ) + { + if ( !m_pImpl->getInputHandler()->KeyInput( *m_pImpl, rKEvt ) ) + Control::KeyInput( rKEvt ); + else + { + if ( m_pImpl->isAccessibleAlive() ) + { + m_pImpl->commitCellEvent( AccessibleEventId::STATE_CHANGED, + Any( AccessibleStateType::FOCUSED ), + Any() + ); + // Huh? What the heck? Why do we unconditionally notify a STATE_CHANGE/FOCUSED after each and every + // (handled) key stroke? + + m_pImpl->commitTableEvent( AccessibleEventId::ACTIVE_DESCENDANT_CHANGED, + Any(), + Any() + ); + // ditto: Why do we notify this unconditionally? We should find the right place to notify the + // ACTIVE_DESCENDANT_CHANGED event. + // Also, we should check if STATE_CHANGED/FOCUSED is really necessary: finally, the children are + // transient, aren't they? + } + } + } + + + void TableControl::StateChanged( StateChangedType i_nStateChange ) + { + Control::StateChanged( i_nStateChange ); + + // forward certain settings to the data window + switch ( i_nStateChange ) + { + case StateChangedType::ControlFocus: + m_pImpl->invalidateSelectedRows(); + break; + + case StateChangedType::ControlBackground: + if ( IsControlBackground() ) + getDataWindow().SetControlBackground( GetControlBackground() ); + else + getDataWindow().SetControlBackground(); + break; + + case StateChangedType::ControlForeground: + if ( IsControlForeground() ) + getDataWindow().SetControlForeground( GetControlForeground() ); + else + getDataWindow().SetControlForeground(); + break; + + case StateChangedType::ControlFont: + if ( IsControlFont() ) + getDataWindow().SetControlFont( GetControlFont() ); + else + getDataWindow().SetControlFont(); + break; + default:; + } + } + + + void TableControl::Resize() + { + Control::Resize(); + m_pImpl->onResize(); + } + + + void TableControl::SetModel( const PTableModel& _pModel ) + { + m_pImpl->setModel( _pModel ); + } + + + PTableModel TableControl::GetModel() const + { + return m_pImpl->getModel(); + } + + + sal_Int32 TableControl::GetCurrentRow() const + { + return m_pImpl->getCurrentRow(); + } + + + sal_Int32 TableControl::GetCurrentColumn() const + { + return m_pImpl->getCurrentColumn(); + } + + + void TableControl::GoTo( ColPos _nColumn, RowPos _nRow ) + { + m_pImpl->goTo( _nColumn, _nRow ); + } + + + void TableControl::GoToCell(sal_Int32 _nColPos, sal_Int32 _nRowPos) + { + m_pImpl->goTo( _nColPos, _nRowPos ); + } + + + sal_Int32 TableControl::GetSelectedRowCount() const + { + return sal_Int32( m_pImpl->getSelectedRowCount() ); + } + + + sal_Int32 TableControl::GetSelectedRowIndex( sal_Int32 const i_selectionIndex ) const + { + return m_pImpl->getSelectedRowIndex( i_selectionIndex ); + } + + + bool TableControl::IsRowSelected( sal_Int32 const i_rowIndex ) const + { + return m_pImpl->isRowSelected( i_rowIndex ); + } + + + void TableControl::SelectRow( sal_Int32 const i_rowIndex, bool const i_select ) + { + ENSURE_OR_RETURN_VOID( ( i_rowIndex >= 0 ) && ( i_rowIndex < m_pImpl->getModel()->getRowCount() ), + "TableControl::SelectRow: invalid row index!" ); + + if ( i_select ) + { + if ( !m_pImpl->markRowAsSelected( i_rowIndex ) ) + // nothing to do + return; + } + else + { + m_pImpl->markRowAsDeselected( i_rowIndex ); + } + + m_pImpl->invalidateRowRange( i_rowIndex, i_rowIndex ); + Select(); + } + + + void TableControl::SelectAllRows( bool const i_select ) + { + if ( i_select ) + { + if ( !m_pImpl->markAllRowsAsSelected() ) + // nothing to do + return; + } + else + { + if ( !m_pImpl->markAllRowsAsDeselected() ) + // nothing to do + return; + } + + + Invalidate(); + // TODO: can't we do better than this, and invalidate only the rows which changed? + Select(); + } + + + ITableControl& TableControl::getTableControlInterface() + { + return *m_pImpl; + } + + + SelectionEngine* TableControl::getSelEngine() + { + return m_pImpl->getSelEngine(); + } + + + vcl::Window& TableControl::getDataWindow() + { + return m_pImpl->getDataWindow(); + } + + + Reference< XAccessible > TableControl::CreateAccessible() + { + vcl::Window* pParent = GetAccessibleParentWindow(); + ENSURE_OR_RETURN( pParent, "TableControl::CreateAccessible - parent not found", nullptr ); + + return m_pImpl->getAccessible( *pParent ); + } + + + Reference<XAccessible> TableControl::CreateAccessibleControl( sal_Int32 ) + { + SAL_WARN( "svtools", "TableControl::CreateAccessibleControl: to be overwritten!" ); + return nullptr; + } + + + OUString TableControl::GetAccessibleObjectName( vcl::table::AccessibleTableControlObjType eObjType, sal_Int32 _nRow, sal_Int32 _nCol) const + { + OUString aRetText; + //Window* pWin; + switch( eObjType ) + { + case vcl::table::TCTYPE_GRIDCONTROL: + aRetText = "Grid control"; + break; + case vcl::table::TCTYPE_TABLE: + aRetText = "Grid control"; + break; + case vcl::table::TCTYPE_ROWHEADERBAR: + aRetText = "RowHeaderBar"; + break; + case vcl::table::TCTYPE_COLUMNHEADERBAR: + aRetText = "ColumnHeaderBar"; + break; + case vcl::table::TCTYPE_TABLECELL: + //the name of the cell consists of column name and row name if defined + //if the name is equal to cell content, it'll be read twice + if(GetModel()->hasColumnHeaders()) + { + aRetText = GetColumnName(_nCol) + " , "; + } + if(GetModel()->hasRowHeaders()) + { + aRetText += GetRowName(_nRow) + " , "; + } + //aRetText = GetAccessibleCellText(_nRow, _nCol); + break; + case vcl::table::TCTYPE_ROWHEADERCELL: + aRetText = GetRowName(_nRow); + break; + case vcl::table::TCTYPE_COLUMNHEADERCELL: + aRetText = GetColumnName(_nCol); + break; + default: + OSL_FAIL("GridControl::GetAccessibleName: invalid enum!"); + } + return aRetText; + } + + + OUString TableControl::GetAccessibleObjectDescription( vcl::table::AccessibleTableControlObjType eObjType ) const + { + OUString aRetText; + switch( eObjType ) + { + case vcl::table::TCTYPE_GRIDCONTROL: + aRetText = "Grid control description"; + break; + case vcl::table::TCTYPE_TABLE: + aRetText = "TABLE description"; + break; + case vcl::table::TCTYPE_ROWHEADERBAR: + aRetText = "ROWHEADERBAR description"; + break; + case vcl::table::TCTYPE_COLUMNHEADERBAR: + aRetText = "COLUMNHEADERBAR description"; + break; + case vcl::table::TCTYPE_TABLECELL: + // the description of the cell consists of column name and row name if defined + // if the name is equal to cell content, it'll be read twice + if ( GetModel()->hasColumnHeaders() ) + { + aRetText = GetColumnName( GetCurrentColumn() ) + " , "; + } + if ( GetModel()->hasRowHeaders() ) + { + aRetText += GetRowName( GetCurrentRow() ); + } + break; + case vcl::table::TCTYPE_ROWHEADERCELL: + aRetText = "ROWHEADERCELL description"; + break; + case vcl::table::TCTYPE_COLUMNHEADERCELL: + aRetText = "COLUMNHEADERCELL description"; + break; + } + return aRetText; + } + + + OUString TableControl::GetRowName( sal_Int32 _nIndex) const + { + OUString sRowName; + GetModel()->getRowHeading( _nIndex ) >>= sRowName; + return sRowName; + } + + + OUString TableControl::GetColumnName( sal_Int32 _nIndex) const + { + return GetModel()->getColumnModel(_nIndex)->getName(); + } + + + OUString TableControl::GetAccessibleCellText( sal_Int32 _nRowPos, sal_Int32 _nColPos) const + { + return m_pImpl->getCellContentAsString( _nRowPos, _nColPos ); + } + + + void TableControl::FillAccessibleStateSet( + ::utl::AccessibleStateSetHelper& rStateSet, + vcl::table::AccessibleTableControlObjType eObjType ) const + { + switch( eObjType ) + { + case vcl::table::TCTYPE_GRIDCONTROL: + case vcl::table::TCTYPE_TABLE: + + rStateSet.AddState( AccessibleStateType::FOCUSABLE ); + + if ( m_pImpl->getSelEngine()->GetSelectionMode() == SelectionMode::Multiple ) + rStateSet.AddState( AccessibleStateType::MULTI_SELECTABLE); + + if ( HasChildPathFocus() ) + rStateSet.AddState( AccessibleStateType::FOCUSED ); + + if ( IsActive() ) + rStateSet.AddState( AccessibleStateType::ACTIVE ); + + if ( m_pImpl->getDataWindow().IsEnabled() ) + { + rStateSet.AddState( AccessibleStateType::ENABLED ); + rStateSet.AddState( AccessibleStateType::SENSITIVE ); + } + + if ( IsReallyVisible() ) + rStateSet.AddState( AccessibleStateType::VISIBLE ); + + if ( eObjType == vcl::table::TCTYPE_TABLE ) + rStateSet.AddState( AccessibleStateType::MANAGES_DESCENDANTS ); + break; + + case vcl::table::TCTYPE_ROWHEADERBAR: + rStateSet.AddState( AccessibleStateType::VISIBLE ); + rStateSet.AddState( AccessibleStateType::MANAGES_DESCENDANTS ); + break; + + case vcl::table::TCTYPE_COLUMNHEADERBAR: + rStateSet.AddState( AccessibleStateType::VISIBLE ); + rStateSet.AddState( AccessibleStateType::MANAGES_DESCENDANTS ); + break; + + case vcl::table::TCTYPE_TABLECELL: + { + rStateSet.AddState( AccessibleStateType::FOCUSABLE ); + if ( HasChildPathFocus() ) + rStateSet.AddState( AccessibleStateType::FOCUSED ); + rStateSet.AddState( AccessibleStateType::ACTIVE ); + rStateSet.AddState( AccessibleStateType::TRANSIENT ); + rStateSet.AddState( AccessibleStateType::SELECTABLE); + rStateSet.AddState( AccessibleStateType::VISIBLE ); + rStateSet.AddState( AccessibleStateType::SHOWING ); + if ( IsRowSelected( GetCurrentRow() ) ) + // Hmm? Wouldn't we expect the affected row to be a parameter to this function? + rStateSet.AddState( AccessibleStateType::SELECTED ); + } + break; + + case vcl::table::TCTYPE_ROWHEADERCELL: + rStateSet.AddState( AccessibleStateType::VISIBLE ); + rStateSet.AddState( AccessibleStateType::TRANSIENT ); + break; + + case vcl::table::TCTYPE_COLUMNHEADERCELL: + rStateSet.AddState( AccessibleStateType::VISIBLE ); + break; + } + } + + void TableControl::commitCellEventIfAccessibleAlive( sal_Int16 const i_eventID, const Any& i_newValue, const Any& i_oldValue ) + { + if ( m_pImpl->isAccessibleAlive() ) + m_pImpl->commitCellEvent( i_eventID, i_newValue, i_oldValue ); + } + + void TableControl::commitTableEventIfAccessibleAlive( sal_Int16 const i_eventID, const Any& i_newValue, const Any& i_oldValue ) + { + if ( m_pImpl->isAccessibleAlive() ) + m_pImpl->commitTableEvent( i_eventID, i_newValue, i_oldValue ); + } + + tools::Rectangle TableControl::GetWindowExtentsRelative(const vcl::Window *pRelativeWindow) const + { + return Control::GetWindowExtentsRelative( pRelativeWindow ); + } + + void TableControl::GrabFocus() + { + Control::GrabFocus(); + } + + Reference< XAccessible > TableControl::GetAccessible() + { + return Control::GetAccessible(); + } + + vcl::Window* TableControl::GetAccessibleParentWindow() const + { + return Control::GetAccessibleParentWindow(); + } + + vcl::Window* TableControl::GetWindowInstance() + { + return this; + } + + + bool TableControl::HasRowHeader() + { + return GetModel()->hasRowHeaders(); + } + + + bool TableControl::HasColHeader() + { + return GetModel()->hasColumnHeaders(); + } + + + sal_Int32 TableControl::GetAccessibleControlCount() const + { + // TC_TABLE is always defined, no matter whether empty or not + sal_Int32 count = 1; + if ( GetModel()->hasRowHeaders() ) + ++count; + if ( GetModel()->hasColumnHeaders() ) + ++count; + return count; + } + + + bool TableControl::ConvertPointToControlIndex( sal_Int32& _rnIndex, const Point& _rPoint ) + { + sal_Int32 nRow = m_pImpl->getRowAtPoint( _rPoint ); + sal_Int32 nCol = m_pImpl->getColAtPoint( _rPoint ); + _rnIndex = nRow * GetColumnCount() + nCol; + return nRow >= 0; + } + + + sal_Int32 TableControl::GetRowCount() const + { + return GetModel()->getRowCount(); + } + + + sal_Int32 TableControl::GetColumnCount() const + { + return GetModel()->getColumnCount(); + } + + + bool TableControl::ConvertPointToCellAddress( sal_Int32& _rnRow, sal_Int32& _rnColPos, const Point& _rPoint ) + { + _rnRow = m_pImpl->getRowAtPoint( _rPoint ); + _rnColPos = m_pImpl->getColAtPoint( _rPoint ); + return _rnRow >= 0; + } + + + void TableControl::FillAccessibleStateSetForCell( ::utl::AccessibleStateSetHelper& _rStateSet, sal_Int32 _nRow, sal_uInt16 ) const + { + if ( IsRowSelected( _nRow ) ) + _rStateSet.AddState( AccessibleStateType::SELECTED ); + if ( HasChildPathFocus() ) + _rStateSet.AddState( AccessibleStateType::FOCUSED ); + else // only transient when column is not focused + _rStateSet.AddState( AccessibleStateType::TRANSIENT ); + + _rStateSet.AddState( AccessibleStateType::VISIBLE ); + _rStateSet.AddState( AccessibleStateType::SHOWING ); + _rStateSet.AddState( AccessibleStateType::ENABLED ); + _rStateSet.AddState( AccessibleStateType::SENSITIVE ); + _rStateSet.AddState( AccessibleStateType::ACTIVE ); + } + + + tools::Rectangle TableControl::GetFieldCharacterBounds(sal_Int32,sal_Int32,sal_Int32 nIndex) + { + return GetCharacterBounds(nIndex); + } + + + sal_Int32 TableControl::GetFieldIndexAtPoint(sal_Int32,sal_Int32,const Point& _rPoint) + { + return GetIndexForPoint(_rPoint); + } + + + tools::Rectangle TableControl::calcHeaderRect(bool _bIsColumnBar ) + { + return m_pImpl->calcHeaderRect( !_bIsColumnBar ); + } + + + tools::Rectangle TableControl::calcHeaderCellRect( bool _bIsColumnBar, sal_Int32 nPos ) + { + return m_pImpl->calcHeaderCellRect( _bIsColumnBar, nPos ); + } + + + tools::Rectangle TableControl::calcTableRect() + { + return m_pImpl->calcTableRect(); + } + + + tools::Rectangle TableControl::calcCellRect( sal_Int32 _nRowPos, sal_Int32 _nColPos ) + { + return m_pImpl->calcCellRect( _nRowPos, _nColPos ); + } + + + IMPL_LINK_NOARG(TableControl, ImplSelectHdl, LinkParamNone*, void) + { + Select(); + } + + + void TableControl::Select() + { + ImplCallEventListenersAndHandler( VclEventId::TableRowSelect, nullptr ); + + if ( m_pImpl->isAccessibleAlive() ) + { + m_pImpl->commitAccessibleEvent( AccessibleEventId::SELECTION_CHANGED ); + + m_pImpl->commitTableEvent( AccessibleEventId::ACTIVE_DESCENDANT_CHANGED, Any(), Any() ); + // TODO: why do we notify this when the *selection* changed? Shouldn't we find a better place for this, + // actually, when the active descendant, i.e. the current cell, *really* changed? + } + } + +} // namespace svt::table + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svtools/source/table/tablecontrol_impl.cxx b/svtools/source/table/tablecontrol_impl.cxx new file mode 100644 index 000000000..c82a27276 --- /dev/null +++ b/svtools/source/table/tablecontrol_impl.cxx @@ -0,0 +1,2552 @@ +/* -*- 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 <table/tablecontrol.hxx> +#include <table/defaultinputhandler.hxx> +#include <table/tablemodel.hxx> + +#include "tabledatawindow.hxx" +#include "tablecontrol_impl.hxx" +#include "tablegeometry.hxx" + +#include <com/sun/star/accessibility/XAccessible.hpp> +#include <com/sun/star/accessibility/AccessibleTableModelChange.hpp> +#include <com/sun/star/accessibility/AccessibleEventId.hpp> +#include <com/sun/star/accessibility/AccessibleTableModelChangeType.hpp> + +#include <comphelper/flagguard.hxx> +#include <vcl/accessiblefactory.hxx> +#include <vcl/scrbar.hxx> +#include <vcl/seleng.hxx> +#include <vcl/settings.hxx> +#include <vcl/image.hxx> +#include <tools/diagnose_ex.h> +#include <tools/debug.hxx> + +#include <cstdlib> +#include <numeric> + +#define MIN_COLUMN_WIDTH_PIXEL 4 + + +namespace svt::table +{ + + + using ::com::sun::star::accessibility::AccessibleTableModelChange; + using ::com::sun::star::uno::Any; + using ::com::sun::star::accessibility::XAccessible; + using ::com::sun::star::uno::Reference; + + namespace AccessibleEventId = ::com::sun::star::accessibility::AccessibleEventId; + namespace AccessibleTableModelChangeType = ::com::sun::star::accessibility::AccessibleTableModelChangeType; + + + //= SuppressCursor + + namespace { + + class SuppressCursor + { + private: + ITableControl& m_rTable; + + public: + explicit SuppressCursor( ITableControl& _rTable ) + :m_rTable( _rTable ) + { + m_rTable.hideCursor(); + } + ~SuppressCursor() + { + m_rTable.showCursor(); + } + }; + + + //= EmptyTableModel + + /** default implementation of an ->ITableModel, used as fallback when no + real model is present + + Instances of this class are static in any way, and provide the least + necessary default functionality for a table model. + */ + class EmptyTableModel : public ITableModel + { + public: + EmptyTableModel() + { + } + + // ITableModel overridables + virtual TableSize getColumnCount() const override + { + return 0; + } + virtual TableSize getRowCount() const override + { + return 0; + } + virtual bool hasColumnHeaders() const override + { + return false; + } + virtual bool hasRowHeaders() const override + { + return false; + } + virtual PColumnModel getColumnModel( ColPos ) override + { + OSL_FAIL( "EmptyTableModel::getColumnModel: invalid call!" ); + return PColumnModel(); + } + virtual PTableRenderer getRenderer() const override + { + return PTableRenderer(); + } + virtual PTableInputHandler getInputHandler() const override + { + return PTableInputHandler(); + } + virtual TableMetrics getRowHeight() const override + { + return 5 * 100; + } + virtual TableMetrics getColumnHeaderHeight() const override + { + return 0; + } + virtual TableMetrics getRowHeaderWidth() const override + { + return 0; + } + virtual ScrollbarVisibility getVerticalScrollbarVisibility() const override + { + return ScrollbarShowNever; + } + virtual ScrollbarVisibility getHorizontalScrollbarVisibility() const override + { + return ScrollbarShowNever; + } + virtual void addTableModelListener( const PTableModelListener& ) override {} + virtual void removeTableModelListener( const PTableModelListener& ) override {} + virtual ::std::optional< ::Color > getLineColor() const override + { + return ::std::optional< ::Color >(); + } + virtual ::std::optional< ::Color > getHeaderBackgroundColor() const override + { + return ::std::optional< ::Color >(); + } + virtual ::std::optional< ::Color > getHeaderTextColor() const override + { + return ::std::optional< ::Color >(); + } + virtual ::std::optional< ::Color > getActiveSelectionBackColor() const override + { + return ::std::optional< ::Color >(); + } + virtual ::std::optional< ::Color > getInactiveSelectionBackColor() const override + { + return ::std::optional< ::Color >(); + } + virtual ::std::optional< ::Color > getActiveSelectionTextColor() const override + { + return ::std::optional< ::Color >(); + } + virtual ::std::optional< ::Color > getInactiveSelectionTextColor() const override + { + return ::std::optional< ::Color >(); + } + virtual ::std::optional< ::Color > getTextColor() const override + { + return ::std::optional< ::Color >(); + } + virtual ::std::optional< ::Color > getTextLineColor() const override + { + return ::std::optional< ::Color >(); + } + virtual ::std::optional< ::std::vector< ::Color > > getRowBackgroundColors() const override + { + return ::std::optional< ::std::vector< ::Color > >(); + } + virtual css::style::VerticalAlignment getVerticalAlign() const override + { + return css::style::VerticalAlignment(0); + } + virtual ITableDataSort* getSortAdapter() override + { + return nullptr; + } + virtual bool isEnabled() const override + { + return true; + } + virtual void getCellContent( ColPos const, RowPos const, css::uno::Any& o_cellContent ) override + { + o_cellContent.clear(); + } + virtual void getCellToolTip( ColPos const, RowPos const, css::uno::Any& ) override + { + } + virtual Any getRowHeading( RowPos const ) const override + { + return Any(); + } + }; + + } + + TableControl_Impl::TableControl_Impl( TableControl& _rAntiImpl ) + :m_rAntiImpl ( _rAntiImpl ) + ,m_pModel ( std::make_shared<EmptyTableModel>() ) + ,m_pInputHandler ( ) + ,m_nRowHeightPixel ( 15 ) + ,m_nColHeaderHeightPixel( 0 ) + ,m_nRowHeaderWidthPixel ( 0 ) + ,m_nColumnCount ( 0 ) + ,m_nRowCount ( 0 ) + ,m_nCurColumn ( COL_INVALID ) + ,m_nCurRow ( ROW_INVALID ) + ,m_nLeftColumn ( 0 ) + ,m_nTopRow ( 0 ) + ,m_nCursorHidden ( 1 ) + ,m_pDataWindow ( VclPtr<TableDataWindow>::Create( *this ) ) + ,m_pVScroll ( nullptr ) + ,m_pHScroll ( nullptr ) + ,m_pScrollCorner ( nullptr ) + ,m_aSelectedRows ( ) + ,m_pTableFunctionSet ( new TableFunctionSet( this ) ) + ,m_nAnchor ( -1 ) + ,m_bUpdatingColWidths ( false ) + ,m_pAccessibleTable ( nullptr ) + { + m_pSelEngine.reset( new SelectionEngine( m_pDataWindow.get(), m_pTableFunctionSet.get() ) ); + m_pSelEngine->SetSelectionMode(SelectionMode::Single); + m_pDataWindow->SetPosPixel( Point( 0, 0 ) ); + m_pDataWindow->Show(); + } + + TableControl_Impl::~TableControl_Impl() + { + m_pVScroll.disposeAndClear(); + m_pHScroll.disposeAndClear(); + m_pScrollCorner.disposeAndClear(); + m_pDataWindow.disposeAndClear(); + m_pTableFunctionSet.reset(); + m_pSelEngine.reset(); + } + + void TableControl_Impl::setModel( const PTableModel& _pModel ) + { + SuppressCursor aHideCursor( *this ); + + if ( m_pModel ) + m_pModel->removeTableModelListener( shared_from_this() ); + + m_pModel = _pModel; + if ( !m_pModel) + m_pModel = std::make_shared<EmptyTableModel>(); + + m_pModel->addTableModelListener( shared_from_this() ); + + m_nCurRow = ROW_INVALID; + m_nCurColumn = COL_INVALID; + + // recalc some model-dependent cached info + impl_ni_updateCachedModelValues(); + impl_ni_relayout(); + + // completely invalidate + m_rAntiImpl.Invalidate(); + + // reset cursor to (0,0) + if ( m_nRowCount ) m_nCurRow = 0; + if ( m_nColumnCount ) m_nCurColumn = 0; + } + + + namespace + { + bool lcl_adjustSelectedRows( ::std::vector< RowPos >& io_selectionIndexes, RowPos const i_firstAffectedRowIndex, TableSize const i_offset ) + { + bool didChanges = false; + for (auto & selectionIndex : io_selectionIndexes) + { + if ( selectionIndex < i_firstAffectedRowIndex ) + continue; + selectionIndex += i_offset; + didChanges = true; + } + return didChanges; + } + } + + + void TableControl_Impl::rowsInserted( RowPos i_first, RowPos i_last ) + { + OSL_PRECOND( i_last >= i_first, "TableControl_Impl::rowsInserted: invalid row indexes!" ); + + TableSize const insertedRows = i_last - i_first + 1; + + // adjust selection, if necessary + bool const selectionChanged = lcl_adjustSelectedRows( m_aSelectedRows, i_first, insertedRows ); + + // adjust our cached row count + m_nRowCount = m_pModel->getRowCount(); + + // if the rows have been inserted before the current row, adjust this + if ( i_first <= m_nCurRow ) + goTo( m_nCurColumn, m_nCurRow + insertedRows ); + + // relayout, since the scrollbar need might have changed + impl_ni_relayout(); + + // notify A1YY events + if ( impl_isAccessibleAlive() ) + { + impl_commitAccessibleEvent( AccessibleEventId::TABLE_MODEL_CHANGED, + Any( AccessibleTableModelChange( AccessibleTableModelChangeType::ROWS_INSERTED, i_first, i_last, -1, -1 ) ) + ); + } + + // schedule repaint + invalidateRowRange( i_first, ROW_INVALID ); + + // call selection handlers, if necessary + if ( selectionChanged ) + m_rAntiImpl.Select(); + } + + + void TableControl_Impl::rowsRemoved( RowPos i_first, RowPos i_last ) + { + sal_Int32 firstRemovedRow = i_first; + sal_Int32 lastRemovedRow = i_last; + + // adjust selection, if necessary + bool selectionChanged = false; + if ( i_first == -1 ) + { + selectionChanged = markAllRowsAsDeselected(); + + firstRemovedRow = 0; + lastRemovedRow = m_nRowCount - 1; + } + else + { + ENSURE_OR_RETURN_VOID( i_last >= i_first, "TableControl_Impl::rowsRemoved: illegal indexes!" ); + + for ( sal_Int32 row = i_first; row <= i_last; ++row ) + { + if ( markRowAsDeselected( row ) ) + selectionChanged = true; + } + + if ( lcl_adjustSelectedRows( m_aSelectedRows, i_last + 1, i_first - i_last - 1 ) ) + selectionChanged = true; + } + + // adjust cached row count + m_nRowCount = m_pModel->getRowCount(); + + // adjust the current row, if it is larger than the row count now + if ( m_nCurRow >= m_nRowCount ) + { + if ( m_nRowCount > 0 ) + goTo( m_nCurColumn, m_nRowCount - 1 ); + else + { + m_nCurRow = ROW_INVALID; + m_nTopRow = 0; + } + } + else if ( m_nRowCount == 0 ) + { + m_nTopRow = 0; + } + + + // relayout, since the scrollbar need might have changed + impl_ni_relayout(); + + // notify A11Y events + if ( impl_isAccessibleAlive() ) + { + commitTableEvent( + AccessibleEventId::TABLE_MODEL_CHANGED, + Any( AccessibleTableModelChange( + AccessibleTableModelChangeType::ROWS_REMOVED, + firstRemovedRow, + lastRemovedRow, + -1, + -1 + ) ), + Any() + ); + } + + // schedule a repaint + invalidateRowRange( firstRemovedRow, ROW_INVALID ); + + // call selection handlers, if necessary + if ( selectionChanged ) + m_rAntiImpl.Select(); + } + + + void TableControl_Impl::columnInserted() + { + m_nColumnCount = m_pModel->getColumnCount(); + impl_ni_relayout(); + + m_rAntiImpl.Invalidate(); + } + + + void TableControl_Impl::columnRemoved() + { + m_nColumnCount = m_pModel->getColumnCount(); + + // adjust the current column, if it is larger than the column count now + if ( m_nCurColumn >= m_nColumnCount ) + { + if ( m_nColumnCount > 0 ) + goTo( m_nCurColumn - 1, m_nCurRow ); + else + m_nCurColumn = COL_INVALID; + } + + impl_ni_relayout(); + + m_rAntiImpl.Invalidate(); + } + + + void TableControl_Impl::allColumnsRemoved() + { + m_nColumnCount = m_pModel->getColumnCount(); + impl_ni_relayout(); + + m_rAntiImpl.Invalidate(); + } + + + void TableControl_Impl::cellsUpdated( RowPos const i_firstRow, RowPos const i_lastRow ) + { + invalidateRowRange( i_firstRow, i_lastRow ); + } + + + void TableControl_Impl::tableMetricsChanged() + { + impl_ni_updateCachedTableMetrics(); + impl_ni_relayout(); + m_rAntiImpl.Invalidate(); + } + + + void TableControl_Impl::impl_invalidateColumn( ColPos const i_column ) + { + tools::Rectangle const aAllCellsArea( impl_getAllVisibleCellsArea() ); + + const TableColumnGeometry aColumn( *this, aAllCellsArea, i_column ); + if ( aColumn.isValid() ) + m_rAntiImpl.Invalidate( aColumn.getRect() ); + } + + + void TableControl_Impl::columnChanged( ColPos const i_column, ColumnAttributeGroup const i_attributeGroup ) + { + ColumnAttributeGroup nGroup( i_attributeGroup ); + if ( nGroup & ColumnAttributeGroup::APPEARANCE ) + { + impl_invalidateColumn( i_column ); + nGroup &= ~ColumnAttributeGroup::APPEARANCE; + } + + if ( nGroup & ColumnAttributeGroup::WIDTH ) + { + if ( !m_bUpdatingColWidths ) + { + impl_ni_relayout( i_column ); + invalidate( TableArea::All ); + } + + nGroup &= ~ColumnAttributeGroup::WIDTH; + } + + OSL_ENSURE( ( nGroup == ColumnAttributeGroup::NONE ) || ( i_attributeGroup == ColumnAttributeGroup::ALL ), + "TableControl_Impl::columnChanged: don't know how to handle this change!" ); + } + + + tools::Rectangle TableControl_Impl::impl_getAllVisibleCellsArea() const + { + tools::Rectangle aArea( Point( 0, 0 ), Size( 0, 0 ) ); + + // determine the right-most border of the last column which is + // at least partially visible + aArea.SetRight( m_nRowHeaderWidthPixel ); + if ( !m_aColumnWidths.empty() ) + { + // the number of pixels which are scrolled out of the left hand + // side of the window + const tools::Long nScrolledOutLeft = m_nLeftColumn == 0 ? 0 : m_aColumnWidths[ m_nLeftColumn - 1 ].getEnd(); + + ColumnPositions::const_reverse_iterator loop = m_aColumnWidths.rbegin(); + do + { + aArea.SetRight(loop->getEnd() - nScrolledOutLeft); + ++loop; + } + while ( ( loop != m_aColumnWidths.rend() ) + && ( loop->getEnd() - nScrolledOutLeft >= aArea.Right() ) + ); + } + // so far, aArea.Right() denotes the first pixel *after* the cell area + aArea.AdjustRight( -1 ); + + // determine the last row which is at least partially visible + aArea.SetBottom( + m_nColHeaderHeightPixel + + impl_getVisibleRows( true ) * m_nRowHeightPixel + - 1 ); + + return aArea; + } + + + tools::Rectangle TableControl_Impl::impl_getAllVisibleDataCellArea() const + { + tools::Rectangle aArea( impl_getAllVisibleCellsArea() ); + aArea.SetLeft( m_nRowHeaderWidthPixel ); + aArea.SetTop( m_nColHeaderHeightPixel ); + return aArea; + } + + + void TableControl_Impl::impl_ni_updateCachedTableMetrics() + { + m_nRowHeightPixel = m_rAntiImpl.LogicToPixel(Size(0, m_pModel->getRowHeight()), MapMode(MapUnit::MapAppFont)).Height(); + + m_nColHeaderHeightPixel = 0; + if ( m_pModel->hasColumnHeaders() ) + m_nColHeaderHeightPixel = m_rAntiImpl.LogicToPixel(Size(0, m_pModel->getColumnHeaderHeight()), MapMode(MapUnit::MapAppFont)).Height(); + + m_nRowHeaderWidthPixel = 0; + if ( m_pModel->hasRowHeaders() ) + m_nRowHeaderWidthPixel = m_rAntiImpl.LogicToPixel(Size(m_pModel->getRowHeaderWidth(), 0), MapMode(MapUnit::MapAppFont)).Width(); + } + + + void TableControl_Impl::impl_ni_updateCachedModelValues() + { + m_pInputHandler = m_pModel->getInputHandler(); + if ( !m_pInputHandler ) + m_pInputHandler = std::make_shared<DefaultInputHandler>(); + + m_nColumnCount = m_pModel->getColumnCount(); + if ( m_nLeftColumn >= m_nColumnCount ) + m_nLeftColumn = ( m_nColumnCount > 0 ) ? m_nColumnCount - 1 : 0; + + m_nRowCount = m_pModel->getRowCount(); + if ( m_nTopRow >= m_nRowCount ) + m_nTopRow = ( m_nRowCount > 0 ) ? m_nRowCount - 1 : 0; + + impl_ni_updateCachedTableMetrics(); + } + + + namespace + { + + /// determines whether a scrollbar is needed for the given values + bool lcl_determineScrollbarNeed( tools::Long const i_position, ScrollbarVisibility const i_visibility, + tools::Long const i_availableSpace, tools::Long const i_neededSpace ) + { + if ( i_visibility == ScrollbarShowNever ) + return false; + if ( i_visibility == ScrollbarShowAlways ) + return true; + if ( i_position > 0 ) + return true; + if ( i_availableSpace >= i_neededSpace ) + return false; + return true; + } + + + void lcl_setButtonRepeat( vcl::Window& _rWindow ) + { + AllSettings aSettings = _rWindow.GetSettings(); + MouseSettings aMouseSettings = aSettings.GetMouseSettings(); + + aMouseSettings.SetButtonRepeat( 0 ); + aSettings.SetMouseSettings( aMouseSettings ); + + _rWindow.SetSettings( aSettings, true ); + } + + + bool lcl_updateScrollbar( vcl::Window& _rParent, VclPtr<ScrollBar>& _rpBar, + bool const i_needBar, tools::Long _nVisibleUnits, + tools::Long _nPosition, tools::Long _nRange, + bool _bHorizontal, const Link<ScrollBar*,void>& _rScrollHandler ) + { + // do we currently have the scrollbar? + bool bHaveBar = _rpBar != nullptr; + + // do we need to correct the scrollbar visibility? + if ( bHaveBar && !i_needBar ) + { + if ( _rpBar->IsTracking() ) + _rpBar->EndTracking(); + _rpBar.disposeAndClear(); + } + else if ( !bHaveBar && i_needBar ) + { + _rpBar = VclPtr<ScrollBar>::Create( + + &_rParent, + WB_DRAG | ( _bHorizontal ? WB_HSCROLL : WB_VSCROLL ) + ); + _rpBar->SetScrollHdl( _rScrollHandler ); + // get some speed into the scrolling... + lcl_setButtonRepeat( *_rpBar ); + } + + if ( _rpBar ) + { + _rpBar->SetRange( Range( 0, _nRange ) ); + _rpBar->SetVisibleSize( _nVisibleUnits ); + _rpBar->SetPageSize( _nVisibleUnits ); + _rpBar->SetLineSize( 1 ); + _rpBar->SetThumbPos( _nPosition ); + _rpBar->Show(); + } + + return ( bHaveBar != i_needBar ); + } + + + /** returns the number of rows fitting into the given range, + for the given row height. Partially fitting rows are counted, too, if the + respective parameter says so. + */ + TableSize lcl_getRowsFittingInto( tools::Long _nOverallHeight, tools::Long _nRowHeightPixel, bool _bAcceptPartialRow ) + { + return _bAcceptPartialRow + ? ( _nOverallHeight + ( _nRowHeightPixel - 1 ) ) / _nRowHeightPixel + : _nOverallHeight / _nRowHeightPixel; + } + + + /** returns the number of columns fitting into the given area, + with the first visible column as given. Partially fitting columns are counted, too, + if the respective parameter says so. + */ + TableSize lcl_getColumnsVisibleWithin( const tools::Rectangle& _rArea, ColPos _nFirstVisibleColumn, + const TableControl_Impl& _rControl, bool _bAcceptPartialRow ) + { + TableSize visibleColumns = 0; + TableColumnGeometry aColumn( _rControl, _rArea, _nFirstVisibleColumn ); + while ( aColumn.isValid() ) + { + if ( !_bAcceptPartialRow ) + if ( aColumn.getRect().Right() > _rArea.Right() ) + // this column is only partially visible, and this is not allowed + break; + + aColumn.moveRight(); + ++visibleColumns; + } + return visibleColumns; + } + + } + + + tools::Long TableControl_Impl::impl_ni_calculateColumnWidths( ColPos const i_assumeInflexibleColumnsUpToIncluding, + bool const i_assumeVerticalScrollbar, ::std::vector< tools::Long >& o_newColWidthsPixel ) const + { + // the available horizontal space + tools::Long gridWidthPixel = m_rAntiImpl.GetOutputSizePixel().Width(); + ENSURE_OR_RETURN( !!m_pModel, "TableControl_Impl::impl_ni_calculateColumnWidths: not allowed without a model!", gridWidthPixel ); + if ( m_pModel->hasRowHeaders() && ( gridWidthPixel != 0 ) ) + { + gridWidthPixel -= m_nRowHeaderWidthPixel; + } + + if ( i_assumeVerticalScrollbar && ( m_pModel->getVerticalScrollbarVisibility() != ScrollbarShowNever ) ) + { + tools::Long nScrollbarMetrics = m_rAntiImpl.GetSettings().GetStyleSettings().GetScrollBarSize(); + gridWidthPixel -= nScrollbarMetrics; + } + + // no need to do anything without columns + TableSize const colCount = m_pModel->getColumnCount(); + if ( colCount == 0 ) + return gridWidthPixel; + + // collect some meta data for our columns: + // - their current (pixel) metrics + tools::Long accumulatedCurrentWidth = 0; + ::std::vector< tools::Long > currentColWidths; + currentColWidths.reserve( colCount ); + typedef ::std::vector< ::std::pair< tools::Long, long > > ColumnLimits; + ColumnLimits effectiveColumnLimits; + effectiveColumnLimits.reserve( colCount ); + tools::Long accumulatedMinWidth = 0; + tools::Long accumulatedMaxWidth = 0; + // - their relative flexibility + ::std::vector< ::sal_Int32 > columnFlexibilities; + columnFlexibilities.reserve( colCount ); + tools::Long flexibilityDenominator = 0; + size_t flexibleColumnCount = 0; + for ( ColPos col = 0; col < colCount; ++col ) + { + PColumnModel const pColumn = m_pModel->getColumnModel( col ); + ENSURE_OR_THROW( !!pColumn, "invalid column returned by the model!" ); + + // current width + tools::Long const currentWidth = appFontWidthToPixel( pColumn->getWidth() ); + currentColWidths.push_back( currentWidth ); + + // accumulated width + accumulatedCurrentWidth += currentWidth; + + // flexibility + ::sal_Int32 flexibility = pColumn->getFlexibility(); + OSL_ENSURE( flexibility >= 0, "TableControl_Impl::impl_ni_calculateColumnWidths: a column's flexibility should be non-negative." ); + if ( ( flexibility < 0 ) // normalization + || ( !pColumn->isResizable() ) // column not resizable => no auto-resize + || ( col <= i_assumeInflexibleColumnsUpToIncluding ) // column shall be treated as inflexible => respect this + ) + flexibility = 0; + + // min/max width + tools::Long effectiveMin = currentWidth, effectiveMax = currentWidth; + // if the column is not flexible, it will not be asked for min/max, but we assume the current width as limit then + if ( flexibility > 0 ) + { + tools::Long const minWidth = appFontWidthToPixel( pColumn->getMinWidth() ); + if ( minWidth > 0 ) + effectiveMin = minWidth; + else + effectiveMin = MIN_COLUMN_WIDTH_PIXEL; + + tools::Long const maxWidth = appFontWidthToPixel( pColumn->getMaxWidth() ); + OSL_ENSURE( minWidth <= maxWidth, "TableControl_Impl::impl_ni_calculateColumnWidths: pretty undecided 'bout its width limits, this column!" ); + if ( ( maxWidth > 0 ) && ( maxWidth >= minWidth ) ) + effectiveMax = maxWidth; + else + effectiveMax = gridWidthPixel; // TODO: any better guess here? + + if ( effectiveMin == effectiveMax ) + // if the min and the max are identical, this implies no flexibility at all + flexibility = 0; + } + + columnFlexibilities.push_back( flexibility ); + flexibilityDenominator += flexibility; + if ( flexibility > 0 ) + ++flexibleColumnCount; + + effectiveColumnLimits.emplace_back( effectiveMin, effectiveMax ); + accumulatedMinWidth += effectiveMin; + accumulatedMaxWidth += effectiveMax; + } + + o_newColWidthsPixel = currentColWidths; + if ( flexibilityDenominator == 0 ) + { + // no column is flexible => don't adjust anything + } + else if ( gridWidthPixel > accumulatedCurrentWidth ) + { // we have space to give away ... + tools::Long distributePixel = gridWidthPixel - accumulatedCurrentWidth; + if ( gridWidthPixel > accumulatedMaxWidth ) + { + // ... but the column's maximal widths are still less than we have + // => set them all to max + for ( svt::table::TableSize i = 0; i < colCount; ++i ) + { + o_newColWidthsPixel[i] = effectiveColumnLimits[i].second; + } + } + else + { + bool startOver = false; + do + { + startOver = false; + // distribute the remaining space amongst all columns with a positive flexibility + for ( size_t i=0; i<o_newColWidthsPixel.size() && !startOver; ++i ) + { + tools::Long const columnFlexibility = columnFlexibilities[i]; + if ( columnFlexibility == 0 ) + continue; + + tools::Long newColWidth = currentColWidths[i] + columnFlexibility * distributePixel / flexibilityDenominator; + + if ( newColWidth > effectiveColumnLimits[i].second ) + { // that was too much, we hit the col's maximum + // set the new width to exactly this maximum + newColWidth = effectiveColumnLimits[i].second; + // adjust the flexibility denominator ... + flexibilityDenominator -= columnFlexibility; + columnFlexibilities[i] = 0; + --flexibleColumnCount; + // ... and the remaining width ... + tools::Long const difference = newColWidth - currentColWidths[i]; + distributePixel -= difference; + // ... this way, we ensure that the width not taken up by this column is consumed by the other + // flexible ones (if there are some) + + // and start over with the first column, since there might be earlier columns which need + // to be recalculated now + startOver = true; + } + + o_newColWidthsPixel[i] = newColWidth; + } + } + while ( startOver ); + + // are there pixels left (might be caused by rounding errors)? + distributePixel = gridWidthPixel - ::std::accumulate( o_newColWidthsPixel.begin(), o_newColWidthsPixel.end(), 0 ); + while ( ( distributePixel > 0 ) && ( flexibleColumnCount > 0 ) ) + { + // yes => ignore relative flexibilities, and subsequently distribute single pixels to all flexible + // columns which did not yet reach their maximum. + for ( size_t i=0; ( i < o_newColWidthsPixel.size() ) && ( distributePixel > 0 ); ++i ) + { + if ( columnFlexibilities[i] == 0 ) + continue; + + OSL_ENSURE( o_newColWidthsPixel[i] <= effectiveColumnLimits[i].second, + "TableControl_Impl::impl_ni_calculateColumnWidths: inconsistency!" ); + if ( o_newColWidthsPixel[i] >= effectiveColumnLimits[i].first ) + { + columnFlexibilities[i] = 0; + --flexibleColumnCount; + continue; + } + + ++o_newColWidthsPixel[i]; + --distributePixel; + } + } + } + } + else if ( gridWidthPixel < accumulatedCurrentWidth ) + { // we need to take away some space from the columns which allow it ... + tools::Long takeAwayPixel = accumulatedCurrentWidth - gridWidthPixel; + if ( gridWidthPixel < accumulatedMinWidth ) + { + // ... but the column's minimal widths are still more than we have + // => set them all to min + for ( svt::table::TableSize i = 0; i < colCount; ++i ) + { + o_newColWidthsPixel[i] = effectiveColumnLimits[i].first; + } + } + else + { + bool startOver = false; + do + { + startOver = false; + // take away the space we need from the columns with a positive flexibility + for ( size_t i=0; i<o_newColWidthsPixel.size() && !startOver; ++i ) + { + tools::Long const columnFlexibility = columnFlexibilities[i]; + if ( columnFlexibility == 0 ) + continue; + + tools::Long newColWidth = currentColWidths[i] - columnFlexibility * takeAwayPixel / flexibilityDenominator; + + if ( newColWidth < effectiveColumnLimits[i].first ) + { // that was too much, we hit the col's minimum + // set the new width to exactly this minimum + newColWidth = effectiveColumnLimits[i].first; + // adjust the flexibility denominator ... + flexibilityDenominator -= columnFlexibility; + columnFlexibilities[i] = 0; + --flexibleColumnCount; + // ... and the remaining width ... + tools::Long const difference = currentColWidths[i] - newColWidth; + takeAwayPixel -= difference; + + // and start over with the first column, since there might be earlier columns which need + // to be recalculated now + startOver = true; + } + + o_newColWidthsPixel[i] = newColWidth; + } + } + while ( startOver ); + + // are there pixels left (might be caused by rounding errors)? + takeAwayPixel = ::std::accumulate( o_newColWidthsPixel.begin(), o_newColWidthsPixel.end(), 0 ) - gridWidthPixel; + while ( ( takeAwayPixel > 0 ) && ( flexibleColumnCount > 0 ) ) + { + // yes => ignore relative flexibilities, and subsequently take away pixels from all flexible + // columns which did not yet reach their minimum. + for ( size_t i=0; ( i < o_newColWidthsPixel.size() ) && ( takeAwayPixel > 0 ); ++i ) + { + if ( columnFlexibilities[i] == 0 ) + continue; + + OSL_ENSURE( o_newColWidthsPixel[i] >= effectiveColumnLimits[i].first, + "TableControl_Impl::impl_ni_calculateColumnWidths: inconsistency!" ); + if ( o_newColWidthsPixel[i] <= effectiveColumnLimits[i].first ) + { + columnFlexibilities[i] = 0; + --flexibleColumnCount; + continue; + } + + --o_newColWidthsPixel[i]; + --takeAwayPixel; + } + } + } + } + + return gridWidthPixel; + } + + + void TableControl_Impl::impl_ni_relayout( ColPos const i_assumeInflexibleColumnsUpToIncluding ) + { + ENSURE_OR_RETURN_VOID( !m_bUpdatingColWidths, "TableControl_Impl::impl_ni_relayout: recursive call detected!" ); + + m_aColumnWidths.resize( 0 ); + if ( !m_pModel ) + return; + + ::comphelper::FlagRestorationGuard const aWidthUpdateFlag( m_bUpdatingColWidths, true ); + SuppressCursor aHideCursor( *this ); + + // layouting steps: + + // 1. adjust column widths, leaving space for a vertical scrollbar + // 2. determine need for a vertical scrollbar + // - V-YES: all fine, result from 1. is still valid + // - V-NO: result from 1. is still under consideration + + // 3. determine need for a horizontal scrollbar + // - H-NO: all fine, result from 2. is still valid + // - H-YES: reconsider need for a vertical scrollbar, if result of 2. was V-NO + // - V-YES: all fine, result from 1. is still valid + // - V-NO: redistribute the remaining space (if any) amongst all columns which allow it + + ::std::vector< tools::Long > newWidthsPixel; + tools::Long gridWidthPixel = impl_ni_calculateColumnWidths( i_assumeInflexibleColumnsUpToIncluding, true, newWidthsPixel ); + + // the width/height of a scrollbar, needed several times below + tools::Long const nScrollbarMetrics = m_rAntiImpl.GetSettings().GetStyleSettings().GetScrollBarSize(); + + // determine the playground for the data cells (excluding headers) + // TODO: what if the control is smaller than needed for the headers/scrollbars? + tools::Rectangle aDataCellPlayground( Point( 0, 0 ), m_rAntiImpl.GetOutputSizePixel() ); + aDataCellPlayground.SetLeft( m_nRowHeaderWidthPixel ); + aDataCellPlayground.SetTop( m_nColHeaderHeightPixel ); + + OSL_ENSURE( ( m_nRowCount == m_pModel->getRowCount() ) && ( m_nColumnCount == m_pModel->getColumnCount() ), + "TableControl_Impl::impl_ni_relayout: how is this expected to work with invalid data?" ); + tools::Long const nAllColumnsWidth = ::std::accumulate( newWidthsPixel.begin(), newWidthsPixel.end(), 0 ); + + ScrollbarVisibility const eVertScrollbar = m_pModel->getVerticalScrollbarVisibility(); + ScrollbarVisibility const eHorzScrollbar = m_pModel->getHorizontalScrollbarVisibility(); + + // do we need a vertical scrollbar? + bool bNeedVerticalScrollbar = lcl_determineScrollbarNeed( + m_nTopRow, eVertScrollbar, aDataCellPlayground.GetHeight(), m_nRowHeightPixel * m_nRowCount ); + bool bFirstRoundVScrollNeed = false; + if ( bNeedVerticalScrollbar ) + { + aDataCellPlayground.AdjustRight( -nScrollbarMetrics ); + bFirstRoundVScrollNeed = true; + } + + // do we need a horizontal scrollbar? + bool const bNeedHorizontalScrollbar = lcl_determineScrollbarNeed( + m_nLeftColumn, eHorzScrollbar, aDataCellPlayground.GetWidth(), nAllColumnsWidth ); + if ( bNeedHorizontalScrollbar ) + { + aDataCellPlayground.AdjustBottom( -nScrollbarMetrics ); + + // now that we just found that we need a horizontal scrollbar, + // the need for a vertical one may have changed, since the horizontal + // SB might just occupy enough space so that not all rows do fit + // anymore + if ( !bFirstRoundVScrollNeed ) + { + bNeedVerticalScrollbar = lcl_determineScrollbarNeed( + m_nTopRow, eVertScrollbar, aDataCellPlayground.GetHeight(), m_nRowHeightPixel * m_nRowCount ); + if ( bNeedVerticalScrollbar ) + { + aDataCellPlayground.AdjustRight( -nScrollbarMetrics ); + } + } + } + + // the initial call to impl_ni_calculateColumnWidths assumed that we need a vertical scrollbar. If, by now, + // we know that this is not the case, re-calculate the column widths. + if ( !bNeedVerticalScrollbar ) + gridWidthPixel = impl_ni_calculateColumnWidths( i_assumeInflexibleColumnsUpToIncluding, false, newWidthsPixel ); + + // update the column objects with the new widths we finally calculated + TableSize const colCount = m_pModel->getColumnCount(); + m_aColumnWidths.reserve( colCount ); + tools::Long accumulatedWidthPixel = m_nRowHeaderWidthPixel; + bool anyColumnWidthChanged = false; + for ( ColPos col = 0; col < colCount; ++col ) + { + const tools::Long columnStart = accumulatedWidthPixel; + const tools::Long columnEnd = columnStart + newWidthsPixel[col]; + m_aColumnWidths.emplace_back( columnStart, columnEnd ); + accumulatedWidthPixel = columnEnd; + + // and don't forget to forward this to the column models + PColumnModel const pColumn = m_pModel->getColumnModel( col ); + ENSURE_OR_THROW( !!pColumn, "invalid column returned by the model!" ); + + tools::Long const oldColumnWidthAppFont = pColumn->getWidth(); + tools::Long const newColumnWidthAppFont = pixelWidthToAppFont( newWidthsPixel[col] ); + pColumn->setWidth( newColumnWidthAppFont ); + + anyColumnWidthChanged |= ( oldColumnWidthAppFont != newColumnWidthAppFont ); + } + + // if the column widths changed, ensure everything is repainted + if ( anyColumnWidthChanged ) + invalidate( TableArea::All ); + + // if the column resizing happened to leave some space at the right, but there are columns + // scrolled out to the left, scroll them in + while ( ( m_nLeftColumn > 0 ) + && ( accumulatedWidthPixel - m_aColumnWidths[ m_nLeftColumn - 1 ].getStart() <= gridWidthPixel ) + ) + { + --m_nLeftColumn; + } + + // now adjust the column metrics, since they currently ignore the horizontal scroll position + if ( m_nLeftColumn > 0 ) + { + const tools::Long offsetPixel = m_aColumnWidths[ 0 ].getStart() - m_aColumnWidths[ m_nLeftColumn ].getStart(); + for (auto & columnWidth : m_aColumnWidths) + { + columnWidth.move( offsetPixel ); + } + } + + // show or hide the scrollbars as needed, and position the data window + impl_ni_positionChildWindows( aDataCellPlayground, bNeedVerticalScrollbar, bNeedHorizontalScrollbar ); + } + + + void TableControl_Impl::impl_ni_positionChildWindows( tools::Rectangle const & i_dataCellPlayground, + bool const i_verticalScrollbar, bool const i_horizontalScrollbar ) + { + tools::Long const nScrollbarMetrics = m_rAntiImpl.GetSettings().GetStyleSettings().GetScrollBarSize(); + + // create or destroy the vertical scrollbar, as needed + lcl_updateScrollbar( + m_rAntiImpl, + m_pVScroll, + i_verticalScrollbar, + lcl_getRowsFittingInto( i_dataCellPlayground.GetHeight(), m_nRowHeightPixel, false ), + // visible units + m_nTopRow, // current position + m_nRowCount, // range + false, // vertical + LINK( this, TableControl_Impl, OnScroll ) // scroll handler + ); + + // position it + if ( m_pVScroll ) + { + tools::Rectangle aScrollbarArea( + Point( i_dataCellPlayground.Right() + 1, 0 ), + Size( nScrollbarMetrics, i_dataCellPlayground.Bottom() + 1 ) + ); + m_pVScroll->SetPosSizePixel( + aScrollbarArea.TopLeft(), aScrollbarArea.GetSize() ); + } + + // create or destroy the horizontal scrollbar, as needed + lcl_updateScrollbar( + m_rAntiImpl, + m_pHScroll, + i_horizontalScrollbar, + lcl_getColumnsVisibleWithin( i_dataCellPlayground, m_nLeftColumn, *this, false ), + // visible units + m_nLeftColumn, // current position + m_nColumnCount, // range + true, // horizontal + LINK( this, TableControl_Impl, OnScroll ) // scroll handler + ); + + // position it + if ( m_pHScroll ) + { + TableSize const nVisibleUnits = lcl_getColumnsVisibleWithin( i_dataCellPlayground, m_nLeftColumn, *this, false ); + TableMetrics const nRange = m_nColumnCount; + if( m_nLeftColumn + nVisibleUnits == nRange - 1 ) + { + if ( m_aColumnWidths[ nRange - 1 ].getStart() - m_aColumnWidths[ m_nLeftColumn ].getEnd() + m_aColumnWidths[ nRange-1 ].getWidth() > i_dataCellPlayground.GetWidth() ) + { + m_pHScroll->SetVisibleSize( nVisibleUnits -1 ); + m_pHScroll->SetPageSize( nVisibleUnits - 1 ); + } + } + tools::Rectangle aScrollbarArea( + Point( 0, i_dataCellPlayground.Bottom() + 1 ), + Size( i_dataCellPlayground.Right() + 1, nScrollbarMetrics ) + ); + m_pHScroll->SetPosSizePixel( + aScrollbarArea.TopLeft(), aScrollbarArea.GetSize() ); + } + + // the corner window connecting the two scrollbars in the lower right corner + bool bHaveScrollCorner = nullptr != m_pScrollCorner; + bool bNeedScrollCorner = ( nullptr != m_pHScroll ) && ( nullptr != m_pVScroll ); + if ( bHaveScrollCorner && !bNeedScrollCorner ) + { + m_pScrollCorner.disposeAndClear(); + } + else if ( !bHaveScrollCorner && bNeedScrollCorner ) + { + m_pScrollCorner = VclPtr<ScrollBarBox>::Create( &m_rAntiImpl ); + m_pScrollCorner->SetSizePixel( Size( nScrollbarMetrics, nScrollbarMetrics ) ); + m_pScrollCorner->SetPosPixel( Point( i_dataCellPlayground.Right() + 1, i_dataCellPlayground.Bottom() + 1 ) ); + m_pScrollCorner->Show(); + } + else if(bHaveScrollCorner && bNeedScrollCorner) + { + m_pScrollCorner->SetPosPixel( Point( i_dataCellPlayground.Right() + 1, i_dataCellPlayground.Bottom() + 1 ) ); + m_pScrollCorner->Show(); + } + + // resize the data window + m_pDataWindow->SetSizePixel( Size( + i_dataCellPlayground.GetWidth() + m_nRowHeaderWidthPixel, + i_dataCellPlayground.GetHeight() + m_nColHeaderHeightPixel + ) ); + } + + + void TableControl_Impl::onResize() + { + impl_ni_relayout(); + checkCursorPosition(); + } + + + void TableControl_Impl::doPaintContent(vcl::RenderContext& rRenderContext, const tools::Rectangle& _rUpdateRect) + { + if (!getModel()) + return; + PTableRenderer pRenderer = getModel()->getRenderer(); + DBG_ASSERT(!!pRenderer, "TableDataWindow::doPaintContent: invalid renderer!"); + if (!pRenderer) + return; + + // our current style settings, to be passed to the renderer + const StyleSettings& rStyle = rRenderContext.GetSettings().GetStyleSettings(); + m_nRowCount = m_pModel->getRowCount(); + // the area occupied by all (at least partially) visible cells, including + // headers + tools::Rectangle const aAllCellsWithHeaders( impl_getAllVisibleCellsArea() ); + + // draw the header column area + if (m_pModel->hasColumnHeaders()) + { + TableRowGeometry const aHeaderRow(*this, tools::Rectangle(Point(0, 0), aAllCellsWithHeaders.BottomRight()), ROW_COL_HEADERS); + tools::Rectangle const aColRect(aHeaderRow.getRect()); + pRenderer->PaintHeaderArea(rRenderContext, aColRect, true, false, rStyle); + // Note that strictly, aHeaderRow.getRect() also contains the intersection between column + // and row header area. However, below we go to paint this intersection, again, + // so this hopefully doesn't hurt if we already paint it here. + + for (TableCellGeometry aCell(aHeaderRow, m_nLeftColumn); aCell.isValid(); aCell.moveRight()) + { + if (_rUpdateRect.GetIntersection(aCell.getRect()).IsEmpty()) + continue; + + pRenderer->PaintColumnHeader(aCell.getColumn(), rRenderContext, aCell.getRect(), rStyle); + } + } + // the area occupied by the row header, if any + tools::Rectangle aRowHeaderArea; + if (m_pModel->hasRowHeaders()) + { + aRowHeaderArea = aAllCellsWithHeaders; + aRowHeaderArea.SetRight( m_nRowHeaderWidthPixel - 1 ); + + TableSize const nVisibleRows = impl_getVisibleRows(true); + TableSize nActualRows = nVisibleRows; + if (m_nTopRow + nActualRows > m_nRowCount) + nActualRows = m_nRowCount - m_nTopRow; + aRowHeaderArea.SetBottom( m_nColHeaderHeightPixel + m_nRowHeightPixel * nActualRows - 1 ); + + pRenderer->PaintHeaderArea(rRenderContext, aRowHeaderArea, false, true, rStyle); + // Note that strictly, aRowHeaderArea also contains the intersection between column + // and row header area. However, below we go to paint this intersection, again, + // so this hopefully doesn't hurt if we already paint it here. + + if (m_pModel->hasColumnHeaders()) + { + TableCellGeometry const aIntersection(*this, tools::Rectangle(Point(0, 0), aAllCellsWithHeaders.BottomRight()), + COL_ROW_HEADERS, ROW_COL_HEADERS); + tools::Rectangle const aInters(aIntersection.getRect()); + pRenderer->PaintHeaderArea(rRenderContext, aInters, true, true, rStyle); + } + } + + // draw the table content row by row + TableSize colCount = getModel()->getColumnCount(); + + // paint all rows + tools::Rectangle const aAllDataCellsArea(impl_getAllVisibleDataCellArea()); + for (TableRowGeometry aRowIterator(*this, aAllCellsWithHeaders, getTopRow()); aRowIterator.isValid(); aRowIterator.moveDown()) + { + if (_rUpdateRect.GetIntersection(aRowIterator.getRect() ).IsEmpty()) + continue; + + bool const isControlFocused = m_rAntiImpl.HasControlFocus(); + bool const isSelectedRow = isRowSelected(aRowIterator.getRow()); + + tools::Rectangle const aRect = aRowIterator.getRect().GetIntersection(aAllDataCellsArea); + + // give the renderer a chance to prepare the row + pRenderer->PrepareRow(aRowIterator.getRow(), isControlFocused, isSelectedRow, rRenderContext, aRect, rStyle); + + // paint the row header + if (m_pModel->hasRowHeaders()) + { + const tools::Rectangle aCurrentRowHeader(aRowHeaderArea.GetIntersection(aRowIterator.getRect())); + pRenderer->PaintRowHeader(rRenderContext, aCurrentRowHeader, rStyle); + } + + if (!colCount) + continue; + + // paint all cells in this row + for (TableCellGeometry aCell(aRowIterator, m_nLeftColumn); aCell.isValid(); aCell.moveRight()) + { + pRenderer->PaintCell(aCell.getColumn(), isSelectedRow, isControlFocused, + rRenderContext, aCell.getRect(), rStyle); + } + } + } + + void TableControl_Impl::hideCursor() + { + if ( ++m_nCursorHidden == 1 ) + impl_ni_doSwitchCursor( false ); + } + + + void TableControl_Impl::showCursor() + { + DBG_ASSERT( m_nCursorHidden > 0, "TableControl_Impl::showCursor: cursor not hidden!" ); + if ( --m_nCursorHidden == 0 ) + impl_ni_doSwitchCursor( true ); + } + + + bool TableControl_Impl::dispatchAction( TableControlAction _eAction ) + { + bool bSuccess = false; + bool selectionChanged = false; + + switch ( _eAction ) + { + case cursorDown: + if ( m_pSelEngine->GetSelectionMode() == SelectionMode::Single ) + { + //if other rows already selected, deselect them + if(!m_aSelectedRows.empty()) + { + invalidateSelectedRows(); + m_aSelectedRows.clear(); + } + if ( m_nCurRow < m_nRowCount-1 ) + { + ++m_nCurRow; + m_aSelectedRows.push_back(m_nCurRow); + } + else + m_aSelectedRows.push_back(m_nCurRow); + invalidateRow( m_nCurRow ); + ensureVisible(m_nCurColumn,m_nCurRow); + selectionChanged = true; + bSuccess = true; + } + else + { + if ( m_nCurRow < m_nRowCount - 1 ) + bSuccess = goTo( m_nCurColumn, m_nCurRow + 1 ); + } + break; + + case cursorUp: + if(m_pSelEngine->GetSelectionMode() == SelectionMode::Single) + { + if(!m_aSelectedRows.empty()) + { + invalidateSelectedRows(); + m_aSelectedRows.clear(); + } + if(m_nCurRow>0) + { + --m_nCurRow; + m_aSelectedRows.push_back(m_nCurRow); + invalidateRow( m_nCurRow ); + } + else + { + m_aSelectedRows.push_back(m_nCurRow); + invalidateRow( m_nCurRow ); + } + ensureVisible(m_nCurColumn,m_nCurRow); + selectionChanged = true; + bSuccess = true; + } + else + { + if ( m_nCurRow > 0 ) + bSuccess = goTo( m_nCurColumn, m_nCurRow - 1 ); + } + break; + case cursorLeft: + if ( m_nCurColumn > 0 ) + bSuccess = goTo( m_nCurColumn - 1, m_nCurRow ); + else + if ( ( m_nCurColumn == 0) && ( m_nCurRow > 0 ) ) + bSuccess = goTo( m_nColumnCount - 1, m_nCurRow - 1 ); + break; + + case cursorRight: + if ( m_nCurColumn < m_nColumnCount - 1 ) + bSuccess = goTo( m_nCurColumn + 1, m_nCurRow ); + else + if ( ( m_nCurColumn == m_nColumnCount - 1 ) && ( m_nCurRow < m_nRowCount - 1 ) ) + bSuccess = goTo( 0, m_nCurRow + 1 ); + break; + + case cursorToLineStart: + bSuccess = goTo( 0, m_nCurRow ); + break; + + case cursorToLineEnd: + bSuccess = goTo( m_nColumnCount - 1, m_nCurRow ); + break; + + case cursorToFirstLine: + bSuccess = goTo( m_nCurColumn, 0 ); + break; + + case cursorToLastLine: + bSuccess = goTo( m_nCurColumn, m_nRowCount - 1 ); + break; + + case cursorPageUp: + { + RowPos nNewRow = ::std::max( RowPos(0), m_nCurRow - impl_getVisibleRows( false ) ); + bSuccess = goTo( m_nCurColumn, nNewRow ); + } + break; + + case cursorPageDown: + { + RowPos nNewRow = ::std::min( m_nRowCount - 1, m_nCurRow + impl_getVisibleRows( false ) ); + bSuccess = goTo( m_nCurColumn, nNewRow ); + } + break; + + case cursorTopLeft: + bSuccess = goTo( 0, 0 ); + break; + + case cursorBottomRight: + bSuccess = goTo( m_nColumnCount - 1, m_nRowCount - 1 ); + break; + + case cursorSelectRow: + { + if(m_pSelEngine->GetSelectionMode() == SelectionMode::NONE) + return false; + //pos is the position of the current row in the vector of selected rows, if current row is selected + int pos = getRowSelectedNumber(m_aSelectedRows, m_nCurRow); + //if current row is selected, it should be deselected, when ALT+SPACE are pressed + if(pos>-1) + { + m_aSelectedRows.erase(m_aSelectedRows.begin()+pos); + if(m_aSelectedRows.empty() && m_nAnchor != -1) + m_nAnchor = -1; + } + //else select the row->put it in the vector + else + m_aSelectedRows.push_back(m_nCurRow); + invalidateRow( m_nCurRow ); + selectionChanged = true; + bSuccess = true; + } + break; + case cursorSelectRowUp: + { + if(m_pSelEngine->GetSelectionMode() == SelectionMode::NONE) + return false; + else if(m_pSelEngine->GetSelectionMode() == SelectionMode::Single) + { + //if there are other selected rows, deselect them + return false; + } + else + { + //there are other selected rows + if(!m_aSelectedRows.empty()) + { + //the anchor wasn't set -> a region is not selected, that's why clear all selection + //and select the current row + if(m_nAnchor==-1) + { + invalidateSelectedRows(); + m_aSelectedRows.clear(); + m_aSelectedRows.push_back(m_nCurRow); + invalidateRow( m_nCurRow ); + } + else + { + //a region is already selected, prevRow is last selected row and the row above - nextRow - should be selected + int prevRow = getRowSelectedNumber(m_aSelectedRows, m_nCurRow); + int nextRow = getRowSelectedNumber(m_aSelectedRows, m_nCurRow-1); + if(prevRow>-1) + { + //if m_nCurRow isn't the upper one, can move up, otherwise not + if(m_nCurRow>0) + m_nCurRow--; + else + return true; + //if nextRow already selected, deselect it, otherwise select it + if(nextRow>-1 && m_aSelectedRows[nextRow] == m_nCurRow) + { + m_aSelectedRows.erase(m_aSelectedRows.begin()+prevRow); + invalidateRow( m_nCurRow + 1 ); + } + else + { + m_aSelectedRows.push_back(m_nCurRow); + invalidateRow( m_nCurRow ); + } + } + else + { + if(m_nCurRow>0) + { + m_aSelectedRows.push_back(m_nCurRow); + m_nCurRow--; + m_aSelectedRows.push_back(m_nCurRow); + invalidateSelectedRegion( m_nCurRow+1, m_nCurRow ); + } + } + } + } + else + { + //if nothing is selected and the current row isn't the upper one + //select the current and one row above + //otherwise select only the upper row + if(m_nCurRow>0) + { + m_aSelectedRows.push_back(m_nCurRow); + m_nCurRow--; + m_aSelectedRows.push_back(m_nCurRow); + invalidateSelectedRegion( m_nCurRow+1, m_nCurRow ); + } + else + { + m_aSelectedRows.push_back(m_nCurRow); + invalidateRow( m_nCurRow ); + } + } + m_pSelEngine->SetAnchor(true); + m_nAnchor = m_nCurRow; + ensureVisible(m_nCurColumn, m_nCurRow); + selectionChanged = true; + bSuccess = true; + } + } + break; + case cursorSelectRowDown: + { + if(m_pSelEngine->GetSelectionMode() == SelectionMode::NONE) + bSuccess = false; + else if(m_pSelEngine->GetSelectionMode() == SelectionMode::Single) + { + bSuccess = false; + } + else + { + if(!m_aSelectedRows.empty()) + { + //the anchor wasn't set -> a region is not selected, that's why clear all selection + //and select the current row + if(m_nAnchor==-1) + { + invalidateSelectedRows(); + m_aSelectedRows.clear(); + m_aSelectedRows.push_back(m_nCurRow); + invalidateRow( m_nCurRow ); + } + else + { + //a region is already selected, prevRow is last selected row and the row beneath - nextRow - should be selected + int prevRow = getRowSelectedNumber(m_aSelectedRows, m_nCurRow); + int nextRow = getRowSelectedNumber(m_aSelectedRows, m_nCurRow+1); + if(prevRow>-1) + { + //if m_nCurRow isn't the last one, can move down, otherwise not + if(m_nCurRow<m_nRowCount-1) + m_nCurRow++; + else + return true; + //if next row already selected, deselect it, otherwise select it + if(nextRow>-1 && m_aSelectedRows[nextRow] == m_nCurRow) + { + m_aSelectedRows.erase(m_aSelectedRows.begin()+prevRow); + invalidateRow( m_nCurRow - 1 ); + } + else + { + m_aSelectedRows.push_back(m_nCurRow); + invalidateRow( m_nCurRow ); + } + } + else + { + if(m_nCurRow<m_nRowCount-1) + { + m_aSelectedRows.push_back(m_nCurRow); + m_nCurRow++; + m_aSelectedRows.push_back(m_nCurRow); + invalidateSelectedRegion( m_nCurRow-1, m_nCurRow ); + } + } + } + } + else + { + //there wasn't any selection, select current and row beneath, otherwise only row beneath + if(m_nCurRow<m_nRowCount-1) + { + m_aSelectedRows.push_back(m_nCurRow); + m_nCurRow++; + m_aSelectedRows.push_back(m_nCurRow); + invalidateSelectedRegion( m_nCurRow-1, m_nCurRow ); + } + else + { + m_aSelectedRows.push_back(m_nCurRow); + invalidateRow( m_nCurRow ); + } + } + m_pSelEngine->SetAnchor(true); + m_nAnchor = m_nCurRow; + ensureVisible(m_nCurColumn, m_nCurRow); + selectionChanged = true; + bSuccess = true; + } + } + break; + + case cursorSelectRowAreaTop: + { + if(m_pSelEngine->GetSelectionMode() == SelectionMode::NONE) + bSuccess = false; + else if(m_pSelEngine->GetSelectionMode() == SelectionMode::Single) + bSuccess = false; + else + { + //select the region between the current and the upper row + RowPos iter = m_nCurRow; + invalidateSelectedRegion( m_nCurRow, 0 ); + //put the rows in vector + while(iter>=0) + { + if ( !isRowSelected( iter ) ) + m_aSelectedRows.push_back(iter); + --iter; + } + m_nCurRow = 0; + m_nAnchor = m_nCurRow; + m_pSelEngine->SetAnchor(true); + ensureVisible(m_nCurColumn, 0); + selectionChanged = true; + bSuccess = true; + } + } + break; + + case cursorSelectRowAreaBottom: + { + if(m_pSelEngine->GetSelectionMode() == SelectionMode::NONE) + return false; + else if(m_pSelEngine->GetSelectionMode() == SelectionMode::Single) + return false; + //select the region between the current and the last row + RowPos iter = m_nCurRow; + invalidateSelectedRegion( m_nCurRow, m_nRowCount-1 ); + //put the rows in the vector + while(iter<=m_nRowCount) + { + if ( !isRowSelected( iter ) ) + m_aSelectedRows.push_back(iter); + ++iter; + } + m_nCurRow = m_nRowCount-1; + m_nAnchor = m_nCurRow; + m_pSelEngine->SetAnchor(true); + ensureVisible(m_nCurColumn, m_nRowCount-1); + selectionChanged = true; + bSuccess = true; + } + break; + default: + OSL_FAIL( "TableControl_Impl::dispatchAction: unsupported action!" ); + break; + } + + if ( bSuccess && selectionChanged ) + { + m_rAntiImpl.Select(); + } + + return bSuccess; + } + + + void TableControl_Impl::impl_ni_doSwitchCursor( bool _bShow ) + { + PTableRenderer pRenderer = m_pModel ? m_pModel->getRenderer() : PTableRenderer(); + if ( pRenderer ) + { + tools::Rectangle aCellRect; + impl_getCellRect( m_nCurColumn, m_nCurRow, aCellRect ); + if ( _bShow ) + pRenderer->ShowCellCursor( *m_pDataWindow, aCellRect ); + else + pRenderer->HideCellCursor( *m_pDataWindow ); + } + } + + + void TableControl_Impl::impl_getCellRect( ColPos _nColumn, RowPos _nRow, tools::Rectangle& _rCellRect ) const + { + if ( !m_pModel + || ( COL_INVALID == _nColumn ) + || ( ROW_INVALID == _nRow ) + ) + { + _rCellRect.SetEmpty(); + return; + } + + TableCellGeometry aCell( *this, impl_getAllVisibleCellsArea(), _nColumn, _nRow ); + _rCellRect = aCell.getRect(); + } + + + RowPos TableControl_Impl::getRowAtPoint( const Point& rPoint ) const + { + return impl_getRowForAbscissa( rPoint.Y() ); + } + + + ColPos TableControl_Impl::getColAtPoint( const Point& rPoint ) const + { + return impl_getColumnForOrdinate( rPoint.X() ); + } + + + TableCell TableControl_Impl::hitTest( Point const & i_point ) const + { + TableCell aCell( getColAtPoint( i_point ), getRowAtPoint( i_point ) ); + if ( aCell.nColumn > COL_ROW_HEADERS ) + { + PColumnModel const pColumn = m_pModel->getColumnModel( aCell.nColumn ); + MutableColumnMetrics const & rColInfo( m_aColumnWidths[ aCell.nColumn ] ); + if ( ( rColInfo.getEnd() - 3 <= i_point.X() ) + && ( rColInfo.getEnd() >= i_point.X() ) + && pColumn->isResizable() + ) + { + aCell.eArea = ColumnDivider; + } + } + return aCell; + } + + + ColumnMetrics TableControl_Impl::getColumnMetrics( ColPos const i_column ) const + { + ENSURE_OR_RETURN( ( i_column >= 0 ) && ( i_column < m_pModel->getColumnCount() ), + "TableControl_Impl::getColumnMetrics: illegal column index!", ColumnMetrics() ); + return m_aColumnWidths[ i_column ]; + } + + + PTableModel TableControl_Impl::getModel() const + { + return m_pModel; + } + + + ColPos TableControl_Impl::getCurrentColumn() const + { + return m_nCurColumn; + } + + + RowPos TableControl_Impl::getCurrentRow() const + { + return m_nCurRow; + } + + + ::Size TableControl_Impl::getTableSizePixel() const + { + return m_pDataWindow->GetOutputSizePixel(); + } + + + void TableControl_Impl::setPointer( PointerStyle i_pointer ) + { + m_pDataWindow->SetPointer( i_pointer ); + } + + + void TableControl_Impl::captureMouse() + { + m_pDataWindow->CaptureMouse(); + } + + + void TableControl_Impl::releaseMouse() + { + m_pDataWindow->ReleaseMouse(); + } + + + void TableControl_Impl::invalidate( TableArea const i_what ) + { + switch ( i_what ) + { + case TableArea::ColumnHeaders: + m_pDataWindow->Invalidate( calcHeaderRect( true ) ); + break; + + case TableArea::RowHeaders: + m_pDataWindow->Invalidate( calcHeaderRect( false ) ); + break; + + case TableArea::All: + m_pDataWindow->Invalidate(); + m_pDataWindow->GetParent()->Invalidate( InvalidateFlags::Transparent ); + break; + } + } + + + tools::Long TableControl_Impl::pixelWidthToAppFont( tools::Long const i_pixels ) const + { + return m_pDataWindow->PixelToLogic(Size(i_pixels, 0), MapMode(MapUnit::MapAppFont)).Width(); + } + + + tools::Long TableControl_Impl::appFontWidthToPixel( tools::Long const i_appFontUnits ) const + { + return m_pDataWindow->LogicToPixel(Size(i_appFontUnits, 0), MapMode(MapUnit::MapAppFont)).Width(); + } + + + void TableControl_Impl::hideTracking() + { + m_pDataWindow->HideTracking(); + } + + + void TableControl_Impl::showTracking( tools::Rectangle const & i_location, ShowTrackFlags const i_flags ) + { + m_pDataWindow->ShowTracking( i_location, i_flags ); + } + + + void TableControl_Impl::activateCell( ColPos const i_col, RowPos const i_row ) + { + goTo( i_col, i_row ); + } + + + void TableControl_Impl::invalidateSelectedRegion( RowPos _nPrevRow, RowPos _nCurRow ) + { + // get the visible area of the table control and set the Left and right border of the region to be repainted + tools::Rectangle const aAllCells( impl_getAllVisibleCellsArea() ); + + tools::Rectangle aInvalidateRect; + aInvalidateRect.SetLeft( aAllCells.Left() ); + aInvalidateRect.SetRight( aAllCells.Right() ); + // if only one row is selected + if ( _nPrevRow == _nCurRow ) + { + tools::Rectangle aCellRect; + impl_getCellRect( m_nCurColumn, _nCurRow, aCellRect ); + aInvalidateRect.SetTop( aCellRect.Top() ); + aInvalidateRect.SetBottom( aCellRect.Bottom() ); + } + //if the region is above the current row + else if(_nPrevRow < _nCurRow ) + { + tools::Rectangle aCellRect; + impl_getCellRect( m_nCurColumn, _nPrevRow, aCellRect ); + aInvalidateRect.SetTop( aCellRect.Top() ); + impl_getCellRect( m_nCurColumn, _nCurRow, aCellRect ); + aInvalidateRect.SetBottom( aCellRect.Bottom() ); + } + //if the region is beneath the current row + else + { + tools::Rectangle aCellRect; + impl_getCellRect( m_nCurColumn, _nCurRow, aCellRect ); + aInvalidateRect.SetTop( aCellRect.Top() ); + impl_getCellRect( m_nCurColumn, _nPrevRow, aCellRect ); + aInvalidateRect.SetBottom( aCellRect.Bottom() ); + } + + invalidateRect(aInvalidateRect); + } + + void TableControl_Impl::invalidateRect(const tools::Rectangle &rInvalidateRect) + { + m_pDataWindow->Invalidate( rInvalidateRect, + m_pDataWindow->GetControlBackground().IsTransparent() ? InvalidateFlags::Transparent : InvalidateFlags::NONE ); + } + + + void TableControl_Impl::invalidateSelectedRows() + { + for (auto const& selectedRow : m_aSelectedRows) + { + invalidateRow(selectedRow); + } + } + + + void TableControl_Impl::invalidateRowRange( RowPos const i_firstRow, RowPos const i_lastRow ) + { + RowPos const firstRow = i_firstRow < m_nTopRow ? m_nTopRow : i_firstRow; + RowPos const lastVisibleRow = m_nTopRow + impl_getVisibleRows( true ) - 1; + RowPos const lastRow = ( ( i_lastRow == ROW_INVALID ) || ( i_lastRow > lastVisibleRow ) ) ? lastVisibleRow : i_lastRow; + + tools::Rectangle aInvalidateRect; + + tools::Rectangle const aVisibleCellsArea( impl_getAllVisibleCellsArea() ); + TableRowGeometry aRow( *this, aVisibleCellsArea, firstRow, true ); + while ( aRow.isValid() && ( aRow.getRow() <= lastRow ) ) + { + aInvalidateRect.Union( aRow.getRect() ); + aRow.moveDown(); + } + + if ( i_lastRow == ROW_INVALID ) + aInvalidateRect.SetBottom( m_pDataWindow->GetOutputSizePixel().Height() ); + + invalidateRect(aInvalidateRect); + } + + + void TableControl_Impl::checkCursorPosition() + { + + TableSize nVisibleRows = impl_getVisibleRows(true); + TableSize nVisibleCols = impl_getVisibleColumns(true); + if ( ( m_nTopRow + nVisibleRows > m_nRowCount ) + && ( m_nRowCount >= nVisibleRows ) + ) + { + --m_nTopRow; + } + else + { + m_nTopRow = 0; + } + + if ( ( m_nLeftColumn + nVisibleCols > m_nColumnCount ) + && ( m_nColumnCount >= nVisibleCols ) + ) + { + --m_nLeftColumn; + } + else + { + m_nLeftColumn = 0; + } + + m_pDataWindow->Invalidate(); + } + + + TableSize TableControl_Impl::impl_getVisibleRows( bool _bAcceptPartialRow ) const + { + DBG_ASSERT( m_pDataWindow, "TableControl_Impl::impl_getVisibleRows: no data window!" ); + + return lcl_getRowsFittingInto( + m_pDataWindow->GetOutputSizePixel().Height() - m_nColHeaderHeightPixel, + m_nRowHeightPixel, + _bAcceptPartialRow + ); + } + + + TableSize TableControl_Impl::impl_getVisibleColumns( bool _bAcceptPartialCol ) const + { + DBG_ASSERT( m_pDataWindow, "TableControl_Impl::impl_getVisibleColumns: no data window!" ); + + return lcl_getColumnsVisibleWithin( + tools::Rectangle( Point( 0, 0 ), m_pDataWindow->GetOutputSizePixel() ), + m_nLeftColumn, + *this, + _bAcceptPartialCol + ); + } + + + bool TableControl_Impl::goTo( ColPos _nColumn, RowPos _nRow ) + { + // TODO: give veto listeners a chance + + if ( ( _nColumn < 0 ) || ( _nColumn >= m_nColumnCount ) + || ( _nRow < 0 ) || ( _nRow >= m_nRowCount ) + ) + { + OSL_ENSURE( false, "TableControl_Impl::goTo: invalid row or column index!" ); + return false; + } + + SuppressCursor aHideCursor( *this ); + m_nCurColumn = _nColumn; + m_nCurRow = _nRow; + + // ensure that the new cell is visible + ensureVisible( m_nCurColumn, m_nCurRow ); + return true; + } + + + void TableControl_Impl::ensureVisible( ColPos _nColumn, RowPos _nRow ) + { + DBG_ASSERT( ( _nColumn >= 0 ) && ( _nColumn < m_nColumnCount ) + && ( _nRow >= 0 ) && ( _nRow < m_nRowCount ), + "TableControl_Impl::ensureVisible: invalid coordinates!" ); + + SuppressCursor aHideCursor( *this ); + + if ( _nColumn < m_nLeftColumn ) + impl_scrollColumns( _nColumn - m_nLeftColumn ); + else + { + TableSize nVisibleColumns = impl_getVisibleColumns( false/*bAcceptPartialVisibility*/ ); + if ( _nColumn > m_nLeftColumn + nVisibleColumns - 1 ) + { + impl_scrollColumns( _nColumn - ( m_nLeftColumn + nVisibleColumns - 1 ) ); + // TODO: since not all columns have the same width, this might in theory result + // in the column still not being visible. + } + } + + if ( _nRow < m_nTopRow ) + impl_scrollRows( _nRow - m_nTopRow ); + else + { + TableSize nVisibleRows = impl_getVisibleRows( false/*_bAcceptPartialVisibility*/ ); + if ( _nRow > m_nTopRow + nVisibleRows - 1 ) + impl_scrollRows( _nRow - ( m_nTopRow + nVisibleRows - 1 ) ); + } + } + + + OUString TableControl_Impl::getCellContentAsString( RowPos const i_row, ColPos const i_col ) + { + Any aCellValue; + m_pModel->getCellContent( i_col, i_row, aCellValue ); + + OUString sCellStringContent; + m_pModel->getRenderer()->GetFormattedCellString( aCellValue, sCellStringContent ); + + return sCellStringContent; + } + + + TableSize TableControl_Impl::impl_ni_ScrollRows( TableSize _nRowDelta ) + { + // compute new top row + RowPos nNewTopRow = + ::std::max( + ::std::min( static_cast<RowPos>( m_nTopRow + _nRowDelta ), static_cast<RowPos>( m_nRowCount - 1 ) ), + RowPos(0) + ); + + RowPos nOldTopRow = m_nTopRow; + m_nTopRow = nNewTopRow; + + // if updates are enabled currently, scroll the viewport + if ( m_nTopRow != nOldTopRow ) + { + SuppressCursor aHideCursor( *this ); + // TODO: call an onStartScroll at our listener (or better an own onStartScroll, + // which hides the cursor and then calls the listener) + // Same for onEndScroll + + // scroll the view port, if possible + tools::Long nPixelDelta = m_nRowHeightPixel * ( m_nTopRow - nOldTopRow ); + + tools::Rectangle aDataArea( Point( 0, m_nColHeaderHeightPixel ), m_pDataWindow->GetOutputSizePixel() ); + + if ( m_pDataWindow->GetBackground().IsScrollable() + && std::abs( nPixelDelta ) < aDataArea.GetHeight() + ) + { + m_pDataWindow->Scroll( 0, static_cast<tools::Long>(-nPixelDelta), aDataArea, ScrollFlags::Clip | ScrollFlags::Update | ScrollFlags::Children); + } + else + { + m_pDataWindow->Invalidate( InvalidateFlags::Update ); + m_pDataWindow->GetParent()->Invalidate( InvalidateFlags::Transparent ); + } + + // update the position at the vertical scrollbar + if ( m_pVScroll != nullptr ) + m_pVScroll->SetThumbPos( m_nTopRow ); + } + + // The scroll bar availability might change when we scrolled. + // For instance, imagine a view with 10 rows, if which 5 fit into the window, numbered 1 to 10. + // Now let + // - the user scroll to row number 6, so the last 5 rows are visible + // - somebody remove the last 4 rows + // - the user scroll to row number 5 being the top row, so the last two rows are visible + // - somebody remove row number 6 + // - the user scroll to row number 1 + // => in this case, the need for the scrollbar vanishes immediately. + if ( m_nTopRow == 0 ) + m_rAntiImpl.PostUserEvent( LINK( this, TableControl_Impl, OnUpdateScrollbars ) ); + + return static_cast<TableSize>( m_nTopRow - nOldTopRow ); + } + + + TableSize TableControl_Impl::impl_scrollRows( TableSize const i_rowDelta ) + { + return impl_ni_ScrollRows( i_rowDelta ); + } + + + TableSize TableControl_Impl::impl_ni_ScrollColumns( TableSize _nColumnDelta ) + { + // compute new left column + const ColPos nNewLeftColumn = + ::std::max( + ::std::min( static_cast<ColPos>( m_nLeftColumn + _nColumnDelta ), static_cast<ColPos>( m_nColumnCount - 1 ) ), + ColPos(0) + ); + + const ColPos nOldLeftColumn = m_nLeftColumn; + m_nLeftColumn = nNewLeftColumn; + + // if updates are enabled currently, scroll the viewport + if ( m_nLeftColumn != nOldLeftColumn ) + { + SuppressCursor aHideCursor( *this ); + // TODO: call an onStartScroll at our listener (or better an own onStartScroll, + // which hides the cursor and then calls the listener) + // Same for onEndScroll + + // scroll the view port, if possible + const tools::Rectangle aDataArea( Point( m_nRowHeaderWidthPixel, 0 ), m_pDataWindow->GetOutputSizePixel() ); + + tools::Long nPixelDelta = + m_aColumnWidths[ nOldLeftColumn ].getStart() + - m_aColumnWidths[ m_nLeftColumn ].getStart(); + + // update our column positions + // Do this *before* scrolling, as ScrollFlags::Update will trigger a paint, which already needs the correct + // information in m_aColumnWidths + for (auto & columnWidth : m_aColumnWidths) + { + columnWidth.move(nPixelDelta); + } + + // scroll the window content (if supported and possible), or invalidate the complete window + if ( m_pDataWindow->GetBackground().IsScrollable() + && std::abs( nPixelDelta ) < aDataArea.GetWidth() + ) + { + m_pDataWindow->Scroll( nPixelDelta, 0, aDataArea, ScrollFlags::Clip | ScrollFlags::Update ); + } + else + { + m_pDataWindow->Invalidate( InvalidateFlags::Update ); + m_pDataWindow->GetParent()->Invalidate( InvalidateFlags::Transparent ); + } + + // update the position at the horizontal scrollbar + if ( m_pHScroll != nullptr ) + m_pHScroll->SetThumbPos( m_nLeftColumn ); + } + + // The scroll bar availability might change when we scrolled. This is because we do not hide + // the scrollbar when it is, in theory, unnecessary, but currently at a position > 0. In this case, it will + // be auto-hidden when it's scrolled back to pos 0. + if ( m_nLeftColumn == 0 ) + m_rAntiImpl.PostUserEvent( LINK( this, TableControl_Impl, OnUpdateScrollbars ) ); + + return static_cast<TableSize>( m_nLeftColumn - nOldLeftColumn ); + } + + + TableSize TableControl_Impl::impl_scrollColumns( TableSize const i_columnDelta ) + { + return impl_ni_ScrollColumns( i_columnDelta ); + } + + + SelectionEngine* TableControl_Impl::getSelEngine() + { + return m_pSelEngine.get(); + } + + bool TableControl_Impl::isRowSelected( RowPos i_row ) const + { + return ::std::find( m_aSelectedRows.begin(), m_aSelectedRows.end(), i_row ) != m_aSelectedRows.end(); + } + + + RowPos TableControl_Impl::getSelectedRowIndex( size_t const i_selectionIndex ) const + { + if ( i_selectionIndex < m_aSelectedRows.size() ) + return m_aSelectedRows[ i_selectionIndex ]; + return ROW_INVALID; + } + + + int TableControl_Impl::getRowSelectedNumber(const ::std::vector<RowPos>& selectedRows, RowPos current) + { + std::vector<RowPos>::const_iterator it = ::std::find(selectedRows.begin(),selectedRows.end(),current); + if ( it != selectedRows.end() ) + { + return it - selectedRows.begin(); + } + return -1; + } + + + ColPos TableControl_Impl::impl_getColumnForOrdinate( tools::Long const i_ordinate ) const + { + if ( ( m_aColumnWidths.empty() ) || ( i_ordinate < 0 ) ) + return COL_INVALID; + + if ( i_ordinate < m_nRowHeaderWidthPixel ) + return COL_ROW_HEADERS; + + ColumnPositions::const_iterator lowerBound = ::std::lower_bound( + m_aColumnWidths.begin(), + m_aColumnWidths.end(), + MutableColumnMetrics(i_ordinate+1, i_ordinate+1), + ColumnInfoPositionLess() + ); + if ( lowerBound == m_aColumnWidths.end() ) + { + // point is *behind* the start of the last column ... + if ( i_ordinate < m_aColumnWidths.rbegin()->getEnd() ) + // ... but still before its end + return m_nColumnCount - 1; + return COL_INVALID; + } + return lowerBound - m_aColumnWidths.begin(); + } + + + RowPos TableControl_Impl::impl_getRowForAbscissa( tools::Long const i_abscissa ) const + { + if ( i_abscissa < 0 ) + return ROW_INVALID; + + if ( i_abscissa < m_nColHeaderHeightPixel ) + return ROW_COL_HEADERS; + + tools::Long const abscissa = i_abscissa - m_nColHeaderHeightPixel; + tools::Long const row = m_nTopRow + abscissa / m_nRowHeightPixel; + return row < m_pModel->getRowCount() ? row : ROW_INVALID; + } + + + bool TableControl_Impl::markRowAsDeselected( RowPos const i_rowIndex ) + { + ::std::vector< RowPos >::iterator selPos = ::std::find( m_aSelectedRows.begin(), m_aSelectedRows.end(), i_rowIndex ); + if ( selPos == m_aSelectedRows.end() ) + return false; + + m_aSelectedRows.erase( selPos ); + return true; + } + + + bool TableControl_Impl::markRowAsSelected( RowPos const i_rowIndex ) + { + if ( isRowSelected( i_rowIndex ) ) + return false; + + SelectionMode const eSelMode = getSelEngine()->GetSelectionMode(); + switch ( eSelMode ) + { + case SelectionMode::Single: + if ( !m_aSelectedRows.empty() ) + { + OSL_ENSURE( m_aSelectedRows.size() == 1, "TableControl::markRowAsSelected: SingleSelection with more than one selected element?" ); + m_aSelectedRows[0] = i_rowIndex; + break; + } + [[fallthrough]]; + + case SelectionMode::Multiple: + m_aSelectedRows.push_back( i_rowIndex ); + break; + + default: + OSL_ENSURE( false, "TableControl_Impl::markRowAsSelected: unsupported selection mode!" ); + return false; + } + + return true; + } + + + bool TableControl_Impl::markAllRowsAsDeselected() + { + if ( m_aSelectedRows.empty() ) + return false; + + m_aSelectedRows.clear(); + return true; + } + + + bool TableControl_Impl::markAllRowsAsSelected() + { + SelectionMode const eSelMode = getSelEngine()->GetSelectionMode(); + ENSURE_OR_RETURN_FALSE( eSelMode == SelectionMode::Multiple, "TableControl_Impl::markAllRowsAsSelected: unsupported selection mode!" ); + + if ( m_aSelectedRows.size() == size_t( m_pModel->getRowCount() ) ) + { + #if OSL_DEBUG_LEVEL > 0 + for ( TableSize row = 0; row < m_pModel->getRowCount(); ++row ) + { + OSL_ENSURE( isRowSelected( row ), "TableControl_Impl::markAllRowsAsSelected: inconsistency in the selected rows!" ); + } + #endif + // already all rows marked as selected + return false; + } + + m_aSelectedRows.clear(); + for ( RowPos i=0; i < m_pModel->getRowCount(); ++i ) + m_aSelectedRows.push_back(i); + + return true; + } + + + void TableControl_Impl::commitAccessibleEvent( sal_Int16 const i_eventID ) + { + impl_commitAccessibleEvent( i_eventID, Any() ); + } + + + void TableControl_Impl::commitCellEvent( sal_Int16 const i_eventID, const Any& i_newValue, const Any& i_oldValue ) + { + if ( impl_isAccessibleAlive() ) + m_pAccessibleTable->commitCellEvent( i_eventID, i_newValue, i_oldValue ); + } + + + void TableControl_Impl::commitTableEvent( sal_Int16 const i_eventID, const Any& i_newValue, const Any& i_oldValue ) + { + if ( impl_isAccessibleAlive() ) + m_pAccessibleTable->commitTableEvent( i_eventID, i_newValue, i_oldValue ); + } + + + tools::Rectangle TableControl_Impl::calcHeaderRect(bool bColHeader) + { + tools::Rectangle const aRectTableWithHeaders( impl_getAllVisibleCellsArea() ); + Size const aSizeTableWithHeaders( aRectTableWithHeaders.GetSize() ); + if ( bColHeader ) + return tools::Rectangle( aRectTableWithHeaders.TopLeft(), Size( aSizeTableWithHeaders.Width(), m_nColHeaderHeightPixel ) ); + else + return tools::Rectangle( aRectTableWithHeaders.TopLeft(), Size( m_nRowHeaderWidthPixel, aSizeTableWithHeaders.Height() ) ); + } + + + tools::Rectangle TableControl_Impl::calcHeaderCellRect( bool bColHeader, sal_Int32 nPos ) + { + tools::Rectangle const aHeaderRect = calcHeaderRect( bColHeader ); + TableCellGeometry const aGeometry( + *this, aHeaderRect, + bColHeader ? nPos : COL_ROW_HEADERS, + bColHeader ? ROW_COL_HEADERS : nPos + ); + return aGeometry.getRect(); + } + + + tools::Rectangle TableControl_Impl::calcTableRect() const + { + return impl_getAllVisibleDataCellArea(); + } + + + tools::Rectangle TableControl_Impl::calcCellRect( sal_Int32 nRow, sal_Int32 nCol ) const + { + tools::Rectangle aCellRect; + impl_getCellRect( nRow, nCol, aCellRect ); + return aCellRect; + } + + + IMPL_LINK_NOARG( TableControl_Impl, OnUpdateScrollbars, void*, void ) + { + // TODO: can't we simply use lcl_updateScrollbar here, so the scrollbars ranges are updated, instead of + // doing a complete re-layout? + impl_ni_relayout(); + } + + + IMPL_LINK( TableControl_Impl, OnScroll, ScrollBar*, _pScrollbar, void ) + { + DBG_ASSERT( ( _pScrollbar == m_pVScroll ) || ( _pScrollbar == m_pHScroll ), + "TableControl_Impl::OnScroll: where did this come from?" ); + + if ( _pScrollbar == m_pVScroll ) + impl_ni_ScrollRows( _pScrollbar->GetDelta() ); + else + impl_ni_ScrollColumns( _pScrollbar->GetDelta() ); + } + + + Reference< XAccessible > TableControl_Impl::getAccessible( vcl::Window& i_parentWindow ) + { + DBG_TESTSOLARMUTEX(); + if ( m_pAccessibleTable == nullptr ) + { + Reference< XAccessible > const xAccParent = i_parentWindow.GetAccessible(); + if ( xAccParent.is() ) + { + m_pAccessibleTable = m_aFactoryAccess.getFactory().createAccessibleTableControl( + xAccParent, m_rAntiImpl + ); + } + } + + Reference< XAccessible > xAccessible; + if ( m_pAccessibleTable ) + xAccessible = m_pAccessibleTable->getMyself(); + return xAccessible; + } + + + void TableControl_Impl::disposeAccessible() + { + if ( m_pAccessibleTable ) + m_pAccessibleTable->DisposeAccessImpl(); + m_pAccessibleTable = nullptr; + } + + + bool TableControl_Impl::impl_isAccessibleAlive() const + { + return ( nullptr != m_pAccessibleTable ) && m_pAccessibleTable->isAlive(); + } + + + void TableControl_Impl::impl_commitAccessibleEvent( sal_Int16 const i_eventID, Any const & i_newValue ) + { + if ( impl_isAccessibleAlive() ) + m_pAccessibleTable->commitEvent( i_eventID, i_newValue ); + } + + + //= TableFunctionSet + + + TableFunctionSet::TableFunctionSet(TableControl_Impl* _pTableControl) + :m_pTableControl( _pTableControl) + ,m_nCurrentRow( ROW_INVALID ) + { + } + + TableFunctionSet::~TableFunctionSet() + { + } + + void TableFunctionSet::BeginDrag() + { + } + + void TableFunctionSet::CreateAnchor() + { + m_pTableControl->setAnchor( m_pTableControl->getCurRow() ); + } + + + void TableFunctionSet::DestroyAnchor() + { + m_pTableControl->setAnchor( ROW_INVALID ); + } + + + void TableFunctionSet::SetCursorAtPoint(const Point& rPoint, bool bDontSelectAtCursor) + { + // newRow is the row which includes the point, getCurRow() is the last selected row, before the mouse click + RowPos newRow = m_pTableControl->getRowAtPoint( rPoint ); + if ( newRow == ROW_COL_HEADERS ) + newRow = m_pTableControl->getTopRow(); + + ColPos newCol = m_pTableControl->getColAtPoint( rPoint ); + if ( newCol == COL_ROW_HEADERS ) + newCol = m_pTableControl->getLeftColumn(); + + if ( ( newRow == ROW_INVALID ) || ( newCol == COL_INVALID ) ) + return; + + if ( bDontSelectAtCursor ) + { + if ( m_pTableControl->getSelectedRowCount() > 1 ) + m_pTableControl->getSelEngine()->AddAlways(true); + } + else if ( m_pTableControl->getAnchor() == m_pTableControl->getCurRow() ) + { + //selected region lies above the last selection + if( m_pTableControl->getCurRow() >= newRow) + { + //put selected rows in vector + while ( m_pTableControl->getAnchor() >= newRow ) + { + m_pTableControl->markRowAsSelected( m_pTableControl->getAnchor() ); + m_pTableControl->setAnchor( m_pTableControl->getAnchor() - 1 ); + } + m_pTableControl->setAnchor( m_pTableControl->getAnchor() + 1 ); + } + //selected region lies beneath the last selected row + else + { + while ( m_pTableControl->getAnchor() <= newRow ) + { + m_pTableControl->markRowAsSelected( m_pTableControl->getAnchor() ); + m_pTableControl->setAnchor( m_pTableControl->getAnchor() + 1 ); + } + m_pTableControl->setAnchor( m_pTableControl->getAnchor() - 1 ); + } + m_pTableControl->invalidateSelectedRegion( m_pTableControl->getCurRow(), newRow ); + } + //no region selected + else + { + if ( !m_pTableControl->hasRowSelection() ) + m_pTableControl->markRowAsSelected( newRow ); + else + { + if ( m_pTableControl->getSelEngine()->GetSelectionMode() == SelectionMode::Single ) + { + DeselectAll(); + m_pTableControl->markRowAsSelected( newRow ); + } + else + { + m_pTableControl->markRowAsSelected( newRow ); + } + } + if ( m_pTableControl->getSelectedRowCount() > 1 && m_pTableControl->getSelEngine()->GetSelectionMode() != SelectionMode::Single ) + m_pTableControl->getSelEngine()->AddAlways(true); + + m_pTableControl->invalidateRow( newRow ); + } + m_pTableControl->goTo( newCol, newRow ); + } + + bool TableFunctionSet::IsSelectionAtPoint( const Point& rPoint ) + { + m_pTableControl->getSelEngine()->AddAlways(false); + if ( !m_pTableControl->hasRowSelection() ) + return false; + else + { + RowPos curRow = m_pTableControl->getRowAtPoint( rPoint ); + m_pTableControl->setAnchor( ROW_INVALID ); + bool selected = m_pTableControl->isRowSelected( curRow ); + m_nCurrentRow = curRow; + return selected; + } + } + + void TableFunctionSet::DeselectAtPoint( const Point& ) + { + m_pTableControl->invalidateRow( m_nCurrentRow ); + m_pTableControl->markRowAsDeselected( m_nCurrentRow ); + } + + + void TableFunctionSet::DeselectAll() + { + if ( m_pTableControl->hasRowSelection() ) + { + for ( size_t i=0; i<m_pTableControl->getSelectedRowCount(); ++i ) + { + RowPos const rowIndex = m_pTableControl->getSelectedRowIndex(i); + m_pTableControl->invalidateRow( rowIndex ); + } + + m_pTableControl->markAllRowsAsDeselected(); + } + } + + +} // namespace svt::table + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svtools/source/table/tablecontrol_impl.hxx b/svtools/source/table/tablecontrol_impl.hxx new file mode 100644 index 000000000..727dea92b --- /dev/null +++ b/svtools/source/table/tablecontrol_impl.hxx @@ -0,0 +1,479 @@ +/* -*- 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 <table/tablemodel.hxx> +#include <table/tablecontrolinterface.hxx> + +#include <vcl/svtaccessiblefactory.hxx> +#include <vcl/accessibletable.hxx> + +#include <vcl/seleng.hxx> + +#include <vector> + +class ScrollBar; +class ScrollBarBox; + +namespace svt::table +{ + struct MutableColumnMetrics : public ColumnMetrics + { + MutableColumnMetrics() + :ColumnMetrics() + { + } + + MutableColumnMetrics( tools::Long const i_startPixel, tools::Long const i_endPixel ) + :ColumnMetrics( i_startPixel, i_endPixel ) + { + } + + tools::Long getStart() const { return nStartPixel; } + tools::Long getEnd() const { return nEndPixel; } + + void move( tools::Long const i_offset ) { nStartPixel += i_offset; nEndPixel += i_offset; } + + tools::Long getWidth() const { return nEndPixel - nStartPixel; } + }; + + struct ColumnInfoPositionLess + { + bool operator()( MutableColumnMetrics const& i_lhs, MutableColumnMetrics const& i_rhs ) + { + return i_lhs.getEnd() < i_rhs.getStart(); + } + }; + + typedef ::std::vector< MutableColumnMetrics > ColumnPositions; + + class TableControl; + class TableDataWindow; + class TableFunctionSet; + + + //= TableControl_Impl + + class TableControl_Impl :public ITableControl + ,public ITableModelListener + { + friend class TableGeometry; + friend class TableRowGeometry; + friend class TableColumnGeometry; + friend class SuspendInvariants; + + private: + /// the control whose impl-instance we implement + TableControl& m_rAntiImpl; + /// the model of the table control + PTableModel m_pModel; + /// the input handler to use, usually the input handler as provided by ->m_pModel + PTableInputHandler m_pInputHandler; + /// info about the widths of our columns + ColumnPositions m_aColumnWidths; + + /// the height of a single row in the table, measured in pixels + tools::Long m_nRowHeightPixel; + /// the height of the column header row in the table, measured in pixels + tools::Long m_nColHeaderHeightPixel; + /// the width of the row header column in the table, measured in pixels + tools::Long m_nRowHeaderWidthPixel; + + /// the number of columns in the table control. Cached model value. + TableSize m_nColumnCount; + + /// the number of rows in the table control. Cached model value. + TableSize m_nRowCount; + + ColPos m_nCurColumn; + RowPos m_nCurRow; + ColPos m_nLeftColumn; + RowPos m_nTopRow; + + sal_Int32 m_nCursorHidden; + + /** the window to contain all data content, including header bars + + The window's upper left corner is at position (0,0), relative to the + table control, which is the direct parent of the data window. + */ + VclPtr<TableDataWindow> m_pDataWindow; + /// the vertical scrollbar, if any + VclPtr<ScrollBar> m_pVScroll; + /// the horizontal scrollbar, if any + VclPtr<ScrollBar> m_pHScroll; + VclPtr<ScrollBarBox> m_pScrollCorner; + //selection engine - for determining selection range, e.g. single, multiple + std::unique_ptr<SelectionEngine> m_pSelEngine; + //vector which contains the selected rows + std::vector<RowPos> m_aSelectedRows; + //part of selection engine + std::unique_ptr<TableFunctionSet> m_pTableFunctionSet; + //part of selection engine + RowPos m_nAnchor; + bool m_bUpdatingColWidths; + + vcl::AccessibleFactoryAccess m_aFactoryAccess; + vcl::table::IAccessibleTableControl* m_pAccessibleTable; + + public: + void setModel( const PTableModel& _pModel ); + + const PTableInputHandler& getInputHandler() const { return m_pInputHandler; } + + RowPos getCurRow() const { return m_nCurRow; } + + RowPos getAnchor() const { return m_nAnchor; } + void setAnchor( RowPos const i_anchor ) { m_nAnchor = i_anchor; } + + RowPos getTopRow() const { return m_nTopRow; } + ColPos getLeftColumn() const { return m_nLeftColumn; } + + const TableControl& getAntiImpl() const { return m_rAntiImpl; } + TableControl& getAntiImpl() { return m_rAntiImpl; } + + public: + explicit TableControl_Impl( TableControl& _rAntiImpl ); + virtual ~TableControl_Impl() override; + + /** to be called when the anti-impl instance has been resized + */ + void onResize(); + + /** paints the table control content which intersects with the given rectangle + */ + void doPaintContent(vcl::RenderContext& rRenderContext, const tools::Rectangle& _rUpdateRect); + + /** moves the cursor to the cell with the given coordinates + + To ease the caller's code, the coordinates must not necessarily denote a + valid position. If they don't, <FALSE/> will be returned. + */ + bool goTo( ColPos _nColumn, RowPos _nRow ); + + /** ensures that the given coordinate is visible + @param _nColumn + the column position which should be visible. Must be non-negative, and smaller + than the column count. + @param _nRow + the row position which should be visibleMust be non-negative, and smaller + than the row count. + */ + void ensureVisible( ColPos _nColumn, RowPos _nRow ); + + /** retrieves the content of the given cell, converted to a string + */ + OUString getCellContentAsString( RowPos const i_row, ColPos const i_col ); + + /** returns the position of the current row in the selection vector */ + static int getRowSelectedNumber(const ::std::vector<RowPos>& selectedRows, RowPos current); + + void invalidateRect(const tools::Rectangle &rInvalidateRect); + + /** ??? */ + void invalidateSelectedRegion( RowPos _nPrevRow, RowPos _nCurRow ); + + /** invalidates the part of the data window which is covered by the given rows + @param i_firstRow + the index of the first row to include in the invalidation + @param i_lastRow + the index of the last row to include in the invalidation, or ROW_INVALID if the invalidation + should happen down to the bottom of the data window. + */ + void invalidateRowRange( RowPos const i_firstRow, RowPos const i_lastRow ); + + /** invalidates the part of the data window which is covered by the given row + */ + void invalidateRow( RowPos const i_row ) { invalidateRowRange( i_row, i_row ); } + + /** invalidates all selected rows + */ + void invalidateSelectedRows(); + + void checkCursorPosition(); + + bool hasRowSelection() const { return !m_aSelectedRows.empty(); } + size_t getSelectedRowCount() const { return m_aSelectedRows.size(); } + RowPos getSelectedRowIndex( size_t const i_selectionIndex ) const; + + /** removes the given row index from m_aSelectedRows + + @return + <TRUE/> if and only if the row was previously marked as selected + */ + bool markRowAsDeselected( RowPos const i_rowIndex ); + + /** marks the given row as selected, by putting it into m_aSelectedRows + @return + <TRUE/> if and only if the row was previously <em>not</em> marked as selected + */ + bool markRowAsSelected( RowPos const i_rowIndex ); + + /** marks all rows as deselected + @return + <TRUE/> if and only if the selection actually changed by this operation + */ + bool markAllRowsAsDeselected(); + + /** marks all rows as selected + @return + <FALSE/> if and only if all rows were selected already. + */ + bool markAllRowsAsSelected(); + + void commitAccessibleEvent( sal_Int16 const i_eventID ); + void commitCellEvent( sal_Int16 const i_eventID, const css::uno::Any& i_newValue, const css::uno::Any& i_oldValue ); + void commitTableEvent( sal_Int16 const i_eventID, const css::uno::Any& i_newValue, const css::uno::Any& i_oldValue ); + + // ITableControl + virtual void hideCursor() override; + virtual void showCursor() override; + virtual bool dispatchAction( TableControlAction _eAction ) override; + virtual SelectionEngine* getSelEngine() override; + virtual PTableModel getModel() const override; + virtual ColPos getCurrentColumn() const override; + virtual RowPos getCurrentRow() const override; + virtual void activateCell( ColPos const i_col, RowPos const i_row ) override; + virtual ::Size getTableSizePixel() const override; + virtual void setPointer( PointerStyle i_pointer ) override; + virtual void captureMouse() override; + virtual void releaseMouse() override; + virtual void invalidate( TableArea const i_what ) override; + virtual tools::Long pixelWidthToAppFont( tools::Long const i_pixels ) const override; + virtual void hideTracking() override; + virtual void showTracking( tools::Rectangle const & i_location, ShowTrackFlags const i_flags ) override; + RowPos getRowAtPoint( const Point& rPoint ) const; + ColPos getColAtPoint( const Point& rPoint ) const; + virtual TableCell hitTest( const Point& rPoint ) const override; + virtual ColumnMetrics getColumnMetrics( ColPos const i_column ) const override; + virtual bool isRowSelected( RowPos i_row ) const override; + + + tools::Long appFontWidthToPixel( tools::Long const i_appFontUnits ) const; + + TableDataWindow& getDataWindow() { return *m_pDataWindow; } + const TableDataWindow& getDataWindow() const { return *m_pDataWindow; } + ScrollBar* getHorzScrollbar() { return m_pHScroll; } + ScrollBar* getVertScrollbar() { return m_pVScroll; } + + tools::Rectangle calcHeaderRect( bool bColHeader ); + tools::Rectangle calcHeaderCellRect( bool bColHeader, sal_Int32 nPos ); + tools::Rectangle calcTableRect() const; + tools::Rectangle calcCellRect( sal_Int32 nRow, sal_Int32 nCol ) const; + + // A11Y + css::uno::Reference< css::accessibility::XAccessible > + getAccessible( vcl::Window& i_parentWindow ); + void disposeAccessible(); + + bool isAccessibleAlive() const { return impl_isAccessibleAlive(); } + + // ITableModelListener + virtual void rowsInserted( RowPos first, RowPos last ) override; + virtual void rowsRemoved( RowPos first, RowPos last ) override; + virtual void columnInserted() override; + virtual void columnRemoved() override; + virtual void allColumnsRemoved() override; + virtual void cellsUpdated( RowPos const i_firstRow, RowPos const i_lastRow ) override; + virtual void columnChanged( ColPos const i_column, ColumnAttributeGroup const i_attributeGroup ) override; + virtual void tableMetricsChanged() override; + + private: + bool impl_isAccessibleAlive() const; + void impl_commitAccessibleEvent( + sal_Int16 const i_eventID, + css::uno::Any const & i_newValue + ); + + /** toggles the cursor visibility + + The method is not bound to the classes public invariants, as it's used in + situations where the they must not necessarily be fulfilled. + */ + void impl_ni_doSwitchCursor( bool _bOn ); + + /** returns the number of visible rows. + + @param _bAcceptPartialRow + specifies whether a possible only partially visible last row is + counted, too. + */ + TableSize impl_getVisibleRows( bool _bAcceptPartialRow ) const; + + /** returns the number of visible columns + + The value may change with different horizontal scroll positions, as + different columns have different widths. For instance, if your control is + 100 pixels wide, and has three columns of width 50, 50, 100, respectively, + then this method will return either "2" or "1", depending on which column + is the first visible one. + + @param _bAcceptPartialRow + specifies whether a possible only partially visible last row is + counted, too. + */ + TableSize impl_getVisibleColumns( bool _bAcceptPartialCol ) const; + + /** determines the rectangle occupied by the given cell + */ + void impl_getCellRect( ColPos _nColumn, RowPos _nRow, tools::Rectangle& _rCellRect ) const; + + /** updates all cached model values + + The method is not bound to the classes public invariants, as it's used in + situations where the they must not necessarily be fulfilled. + */ + void impl_ni_updateCachedModelValues(); + + /** updates the cached table metrics (row height etc.) + */ + void impl_ni_updateCachedTableMetrics(); + + /** does a relayout of the table control + + Column widths, and consequently the availability of the vertical and horizontal scrollbar, are updated + with a call to this method. + + @param i_assumeInflexibleColumnsUpToIncluding + the index of a column up to which all columns should be considered as inflexible, or + <code>COL_INVALID</code>. + */ + void impl_ni_relayout( ColPos const i_assumeInflexibleColumnsUpToIncluding = COL_INVALID ); + + /** calculates the new width of our columns, taking into account their min and max widths, and their relative + flexibility. + + @param i_assumeInflexibleColumnsUpToIncluding + the index of a column up to which all columns should be considered as inflexible, or + <code>COL_INVALID</code>. + + @param i_assumeVerticalScrollbar + controls whether or not we should assume the presence of a vertical scrollbar. If <true/>, and + if the model has a VerticalScrollbarVisibility != ScrollbarShowNever, the method will leave + space for a vertical scrollbar. + + @return + the overall width of the grid, which is available for columns + */ + tools::Long impl_ni_calculateColumnWidths( + ColPos const i_assumeInflexibleColumnsUpToIncluding, + bool const i_assumeVerticalScrollbar, + ::std::vector< tools::Long >& o_newColWidthsPixel + ) const; + + /** positions all child windows, e.g. the both scrollbars, the corner window, and the data window + */ + void impl_ni_positionChildWindows( + tools::Rectangle const & i_dataCellPlayground, + bool const i_verticalScrollbar, + bool const i_horizontalScrollbar + ); + + /** scrolls the view by the given number of rows + + The method is not bound to the classes public invariants, as it's used in + situations where the they must not necessarily be fulfilled. + + @return + the number of rows by which the viewport was scrolled. This may differ + from the given numbers to scroll in case the begin or the end of the + row range were reached. + */ + TableSize impl_ni_ScrollRows( TableSize _nRowDelta ); + + /** equivalent to impl_ni_ScrollRows, but checks the instances invariants beforehand (in a non-product build only) + */ + TableSize impl_scrollRows( TableSize const i_rowDelta ); + + /** scrolls the view by the given number of columns + + The method is not bound to the classes public invariants, as it's used in + situations where the they must not necessarily be fulfilled. + + @return + the number of columns by which the viewport was scrolled. This may differ + from the given numbers to scroll in case the begin or the end of the + column range were reached. + */ + TableSize impl_ni_ScrollColumns( TableSize _nColumnDelta ); + + /** equivalent to impl_ni_ScrollColumns, but checks the instances invariants beforehand (in a non-product build only) + */ + TableSize impl_scrollColumns( TableSize const i_columnDelta ); + + /** retrieves the area occupied by the totality of (at least partially) visible cells + + The returned area includes row and column headers. Also, it takes into + account the fact that there might be less columns than would normally + find room in the control. + + As a result of respecting the partial visibility of rows and columns, + the returned area might be larger than the data window's output size. + */ + tools::Rectangle impl_getAllVisibleCellsArea() const; + + /** retrieves the area occupied by all (at least partially) visible data cells. + + Effectively, the returned area is the same as returned by ->impl_getAllVisibleCellsArea, + minus the row and column header areas. + */ + tools::Rectangle impl_getAllVisibleDataCellArea() const; + + /** retrieves the column which covers the given ordinate + */ + ColPos impl_getColumnForOrdinate( tools::Long const i_ordinate ) const; + + /** retrieves the row which covers the given abscissa + */ + RowPos impl_getRowForAbscissa( tools::Long const i_abscissa ) const; + + /// invalidates the window area occupied by the given column + void impl_invalidateColumn( ColPos const i_column ); + + DECL_LINK( OnScroll, ScrollBar*, void ); + DECL_LINK( OnUpdateScrollbars, void*, void ); + }; + + //see seleng.hxx, seleng.cxx, FunctionSet overridables, part of selection engine + class TableFunctionSet : public FunctionSet + { + private: + TableControl_Impl* m_pTableControl; + RowPos m_nCurrentRow; + + public: + explicit TableFunctionSet(TableControl_Impl* _pTableControl); + virtual ~TableFunctionSet() override; + + virtual void BeginDrag() override; + virtual void CreateAnchor() override; + virtual void DestroyAnchor() override; + virtual void SetCursorAtPoint(const Point& rPoint, bool bDontSelectAtCursor = false) override; + virtual bool IsSelectionAtPoint( const Point& rPoint ) override; + virtual void DeselectAtPoint( const Point& rPoint ) override; + virtual void DeselectAll() override; + }; + + +} // namespace svt::table + + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svtools/source/table/tabledatawindow.cxx b/svtools/source/table/tabledatawindow.cxx new file mode 100644 index 000000000..06e6902b0 --- /dev/null +++ b/svtools/source/table/tabledatawindow.cxx @@ -0,0 +1,200 @@ +/* -*- 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 <table/tablecontrol.hxx> + +#include "tabledatawindow.hxx" +#include "tablecontrol_impl.hxx" +#include "tablegeometry.hxx" + +#include <vcl/help.hxx> +#include <vcl/settings.hxx> +#include <vcl/commandevent.hxx> + +namespace svt::table +{ + using css::uno::Any; + + TableDataWindow::TableDataWindow( TableControl_Impl& _rTableControl ) + :Window( &_rTableControl.getAntiImpl() ) + ,m_rTableControl( _rTableControl ) + { + // by default, use the background as determined by the style settings + const Color aWindowColor( GetSettings().GetStyleSettings().GetFieldColor() ); + SetBackground( Wallpaper( aWindowColor ) ); + GetOutDev()->SetFillColor( aWindowColor ); + } + + TableDataWindow::~TableDataWindow() + { + disposeOnce(); + } + + void TableDataWindow::dispose() + { + impl_hideTipWindow(); + Window::dispose(); + } + + void TableDataWindow::Paint( vcl::RenderContext& rRenderContext, const tools::Rectangle& rUpdateRect ) + { + m_rTableControl.doPaintContent(rRenderContext, rUpdateRect); + } + + void TableDataWindow::RequestHelp( const HelpEvent& rHEvt ) + { + HelpEventMode const nHelpMode = rHEvt.GetMode(); + if ( IsMouseCaptured() + || !( nHelpMode & HelpEventMode::QUICK ) + ) + { + Window::RequestHelp( rHEvt ); + return; + } + + OUString sHelpText; + QuickHelpFlags nHelpStyle = QuickHelpFlags::NONE; + + Point const aMousePos( ScreenToOutputPixel( rHEvt.GetMousePosPixel() ) ); + RowPos const hitRow = m_rTableControl.getRowAtPoint( aMousePos ); + ColPos const hitCol = m_rTableControl.getColAtPoint( aMousePos ); + + PTableModel const pTableModel( m_rTableControl.getModel() ); + if ( ( hitCol >= 0 ) && ( hitCol < pTableModel->getColumnCount() ) ) + { + if ( hitRow == ROW_COL_HEADERS ) + { + sHelpText = pTableModel->getColumnModel( hitCol )->getHelpText(); + } + else if ( ( hitRow >= 0 ) && ( hitRow < pTableModel->getRowCount() ) ) + { + Any aCellToolTip; + pTableModel->getCellToolTip( hitCol, hitRow, aCellToolTip ); + if ( !aCellToolTip.hasValue() ) + { + // use the cell content + pTableModel->getCellContent( hitCol, hitRow, aCellToolTip ); + + // use the cell content as tool tip only if it doesn't fit into the cell. + tools::Rectangle const aWindowRect( Point( 0, 0 ), GetOutputSizePixel() ); + TableCellGeometry const aCell( m_rTableControl, aWindowRect, hitCol, hitRow ); + tools::Rectangle const aCellRect( aCell.getRect() ); + + PTableRenderer const pRenderer = pTableModel->getRenderer(); + if ( pRenderer->FitsIntoCell( aCellToolTip, *GetOutDev(), aCellRect ) ) + aCellToolTip.clear(); + } + + pTableModel->getRenderer()->GetFormattedCellString( aCellToolTip, sHelpText ); + + if ( sHelpText.indexOf( '\n' ) >= 0 ) + nHelpStyle = QuickHelpFlags::TipStyleBalloon; + } + } + + if ( !sHelpText.isEmpty() ) + { + // hide the standard (singleton) help window, so we do not have two help windows open at the same time + Help::HideBalloonAndQuickHelp(); + + tools::Rectangle const aControlScreenRect( + OutputToScreenPixel( Point( 0, 0 ) ), + GetOutputSizePixel() + ); + + Help::ShowQuickHelp(this, aControlScreenRect, sHelpText, nHelpStyle); + } + else + { + impl_hideTipWindow(); + Window::RequestHelp( rHEvt ); + } + } + + void TableDataWindow::impl_hideTipWindow() + { + Help::HideBalloonAndQuickHelp(); + } + + void TableDataWindow::MouseMove( const MouseEvent& rMEvt ) + { + if ( rMEvt.IsLeaveWindow() ) + impl_hideTipWindow(); + + if ( !m_rTableControl.getInputHandler()->MouseMove( m_rTableControl, rMEvt ) ) + { + Window::MouseMove( rMEvt ); + } + } + + void TableDataWindow::MouseButtonDown( const MouseEvent& rMEvt ) + { + impl_hideTipWindow(); + + Point const aPoint = rMEvt.GetPosPixel(); + RowPos const hitRow = m_rTableControl.getRowAtPoint( aPoint ); + bool const wasRowSelected = m_rTableControl.isRowSelected( hitRow ); + size_t const nPrevSelRowCount = m_rTableControl.getSelectedRowCount(); + + if ( !m_rTableControl.getInputHandler()->MouseButtonDown( m_rTableControl, rMEvt ) ) + { + Window::MouseButtonDown( rMEvt ); + return; + } + + bool const isRowSelected = m_rTableControl.isRowSelected( hitRow ); + size_t const nCurSelRowCount = m_rTableControl.getSelectedRowCount(); + if ( isRowSelected != wasRowSelected || nCurSelRowCount != nPrevSelRowCount ) + { + m_aSelectHdl.Call( nullptr ); + } + } + + + void TableDataWindow::MouseButtonUp( const MouseEvent& rMEvt ) + { + if ( !m_rTableControl.getInputHandler()->MouseButtonUp( m_rTableControl, rMEvt ) ) + Window::MouseButtonUp( rMEvt ); + + m_rTableControl.getAntiImpl().GrabFocus(); + } + + + bool TableDataWindow::EventNotify(NotifyEvent& rNEvt ) + { + bool bDone = false; + if ( rNEvt.GetType() == MouseNotifyEvent::COMMAND ) + { + const CommandEvent& rCEvt = *rNEvt.GetCommandEvent(); + if ( rCEvt.GetCommand() == CommandEventId::Wheel ) + { + const CommandWheelData* pData = rCEvt.GetWheelData(); + if( !pData->GetModifier() && ( pData->GetMode() == CommandWheelMode::SCROLL ) ) + { + bDone = HandleScrollCommand( rCEvt, m_rTableControl.getHorzScrollbar(), m_rTableControl.getVertScrollbar() ); + } + } + } + return bDone || Window::EventNotify( rNEvt ); + } + +} // namespace svt::table + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svtools/source/table/tabledatawindow.hxx b/svtools/source/table/tabledatawindow.hxx new file mode 100644 index 000000000..e42a05493 --- /dev/null +++ b/svtools/source/table/tabledatawindow.hxx @@ -0,0 +1,66 @@ +/* -*- 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 <vcl/window.hxx> + + +namespace svt::table +{ + class TableControl_Impl; + class TableFunctionSet; + + /** the window containing the content area (including headers) of + a table control + */ + class TableDataWindow : public vcl::Window + { + friend class TableFunctionSet; + private: + TableControl_Impl& m_rTableControl; + Link<LinkParamNone*,void> m_aSelectHdl; + + public: + explicit TableDataWindow( TableControl_Impl& _rTableControl ); + virtual ~TableDataWindow() override; + virtual void dispose() override; + + void SetSelectHdl(const Link<LinkParamNone*,void>& rLink) + { + m_aSelectHdl = rLink; + } + + // Window overridables + virtual void Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) override; + virtual void MouseMove( const MouseEvent& rMEvt) override; + virtual void MouseButtonDown( const MouseEvent& rMEvt) override; + virtual void MouseButtonUp( const MouseEvent& rMEvt) override; + virtual bool EventNotify(NotifyEvent& rNEvt) override; + virtual void RequestHelp( const HelpEvent& rHEvt ) override; + + private: + static void impl_hideTipWindow(); + }; + +} // namespace svt::table + + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svtools/source/table/tablegeometry.cxx b/svtools/source/table/tablegeometry.cxx new file mode 100644 index 000000000..5b18826c5 --- /dev/null +++ b/svtools/source/table/tablegeometry.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 "tablegeometry.hxx" +#include "tablecontrol_impl.hxx" + + +namespace svt::table +{ + + + //= TableRowGeometry + + + TableRowGeometry::TableRowGeometry( TableControl_Impl const & _rControl, tools::Rectangle const & _rBoundaries, + RowPos const _nRow, bool const i_allowVirtualRows ) + :TableGeometry( _rControl, _rBoundaries ) + ,m_nRowPos( _nRow ) + ,m_bAllowVirtualRows( i_allowVirtualRows ) + { + if ( m_nRowPos == ROW_COL_HEADERS ) + { + m_aRect.SetTop( 0 ); + m_aRect.SetBottom( m_rControl.m_nColHeaderHeightPixel - 1 ); + } + else + { + impl_initRect(); + } + } + + + void TableRowGeometry::impl_initRect() + { + if ( ( m_nRowPos >= m_rControl.m_nTopRow ) && impl_isValidRow( m_nRowPos ) ) + { + m_aRect.SetTop( m_rControl.m_nColHeaderHeightPixel + ( m_nRowPos - m_rControl.m_nTopRow ) * m_rControl.m_nRowHeightPixel ); + m_aRect.SetBottom( m_aRect.Top() + m_rControl.m_nRowHeightPixel - 1 ); + } + else + m_aRect.SetEmpty(); + } + + + bool TableRowGeometry::impl_isValidRow( RowPos const i_row ) const + { + return m_bAllowVirtualRows || ( i_row < m_rControl.m_pModel->getRowCount() ); + } + + + bool TableRowGeometry::moveDown() + { + if ( m_nRowPos == ROW_COL_HEADERS ) + { + m_nRowPos = m_rControl.m_nTopRow; + impl_initRect(); + } + else + { + if ( impl_isValidRow( ++m_nRowPos ) ) + m_aRect.Move( 0, m_rControl.m_nRowHeightPixel ); + else + m_aRect.SetEmpty(); + } + return isValid(); + } + + + //= TableColumnGeometry + + + TableColumnGeometry::TableColumnGeometry( TableControl_Impl const & _rControl, tools::Rectangle const & _rBoundaries, + ColPos const _nCol ) + :TableGeometry( _rControl, _rBoundaries ) + ,m_nColPos( _nCol ) + { + if ( m_nColPos == COL_ROW_HEADERS ) + { + m_aRect.SetLeft( 0 ); + m_aRect.SetRight( m_rControl.m_nRowHeaderWidthPixel - 1 ); + } + else + { + impl_initRect(); + } + } + + + void TableColumnGeometry::impl_initRect() + { + ColPos nLeftColumn = m_rControl.m_nLeftColumn; + if ( ( m_nColPos >= nLeftColumn ) && impl_isValidColumn( m_nColPos ) ) + { + m_aRect.SetLeft( m_rControl.m_nRowHeaderWidthPixel ); + // TODO: take into account any possibly frozen columns + + for ( ColPos col = nLeftColumn; col < m_nColPos; ++col ) + m_aRect.AdjustLeft(m_rControl.m_aColumnWidths[ col ].getWidth() ); + m_aRect.SetRight( m_aRect.Left() + m_rControl.m_aColumnWidths[ m_nColPos ].getWidth() - 1 ); + } + else + m_aRect.SetEmpty(); + } + + + bool TableColumnGeometry::impl_isValidColumn( ColPos const i_column ) const + { + return i_column < ColPos( m_rControl.m_aColumnWidths.size() ); + } + + + bool TableColumnGeometry::moveRight() + { + if ( m_nColPos == COL_ROW_HEADERS ) + { + m_nColPos = m_rControl.m_nLeftColumn; + impl_initRect(); + } + else + { + if ( impl_isValidColumn( ++m_nColPos ) ) + { + m_aRect.SetLeft( m_aRect.Right() + 1 ); + m_aRect.AdjustRight(m_rControl.m_aColumnWidths[ m_nColPos ].getWidth() ); + } + else + m_aRect.SetEmpty(); + } + + return isValid(); + } + + +} // namespace svt::table + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svtools/source/table/tablegeometry.hxx b/svtools/source/table/tablegeometry.hxx new file mode 100644 index 000000000..9fb6d03f0 --- /dev/null +++ b/svtools/source/table/tablegeometry.hxx @@ -0,0 +1,158 @@ +/* -*- 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 <table/tabletypes.hxx> + +#include <tools/gen.hxx> + + +namespace svt::table +{ + + + class TableControl_Impl; + + + //= TableGeometry + + class TableGeometry + { + protected: + const TableControl_Impl& m_rControl; + const tools::Rectangle& m_rBoundaries; + tools::Rectangle m_aRect; + + protected: + TableGeometry( + const TableControl_Impl& _rControl, + const tools::Rectangle& _rBoundaries + ) + :m_rControl( _rControl ) + ,m_rBoundaries( _rBoundaries ) + ,m_aRect( _rBoundaries ) + { + } + + public: + // attribute access + const TableControl_Impl& getControl() const { return m_rControl; } + + // status + const tools::Rectangle& getRect() const { return m_aRect; } + bool isValid() const { return !m_aRect.GetIntersection( m_rBoundaries ).IsEmpty(); } + }; + + + //= TableRowGeometry + + class TableRowGeometry final : public TableGeometry + { + public: + TableRowGeometry( + TableControl_Impl const & _rControl, + tools::Rectangle const & _rBoundaries, + RowPos const _nRow, + bool const i_allowVirtualRows = false + // allow rows >= getRowCount()? + ); + + // status + RowPos getRow() const { return m_nRowPos; } + // operations + bool moveDown(); + + private: + void impl_initRect(); + bool impl_isValidRow( RowPos const i_row ) const; + + RowPos m_nRowPos; + bool m_bAllowVirtualRows; + }; + + + //= TableColumnGeometry + + class TableColumnGeometry final : public TableGeometry + { + public: + TableColumnGeometry( + TableControl_Impl const & _rControl, + tools::Rectangle const & _rBoundaries, + ColPos const _nCol + ); + + // status + ColPos getCol() const { return m_nColPos; } + // operations + bool moveRight(); + + private: + void impl_initRect(); + bool impl_isValidColumn( ColPos const i_column ) const; + + ColPos m_nColPos; + }; + + + //= TableCellGeometry + + /** a helper representing geometry information of a cell + */ + class TableCellGeometry + { + private: + TableRowGeometry m_aRow; + TableColumnGeometry m_aCol; + + public: + TableCellGeometry( + TableControl_Impl const & _rControl, + tools::Rectangle const & _rBoundaries, + ColPos const _nCol, + RowPos const _nRow + ) + :m_aRow( _rControl, _rBoundaries, _nRow, false/*allowVirtualCells*/ ) + ,m_aCol( _rControl, _rBoundaries, _nCol ) + { + } + + TableCellGeometry( + const TableRowGeometry& _rRow, + ColPos _nCol + ) + :m_aRow( _rRow ) + ,m_aCol( _rRow.getControl(), _rRow.getRect(), _nCol ) + { + } + + tools::Rectangle getRect() const { return m_aRow.getRect().GetIntersection( m_aCol.getRect() ); } + ColPos getColumn() const { return m_aCol.getCol(); } + bool isValid() const { return !getRect().IsEmpty(); } + + bool moveRight() {return m_aCol.moveRight(); } + }; + + +} // namespace svt::table + + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |